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/team/team.py CHANGED
@@ -112,6 +112,9 @@ class Team:
112
112
  parent_team_id: Optional[str] = None
113
113
  # The workflow this team belongs to
114
114
  workflow_id: Optional[str] = None
115
+ # Set when this team is part of a workflow.
116
+ workflow_session_id: Optional[str] = None
117
+
115
118
  role: Optional[str] = None
116
119
 
117
120
  # --- User settings ---
@@ -127,9 +130,13 @@ class Team:
127
130
  session_name: Optional[str] = None
128
131
  # Session state (stored in the database to persist across runs)
129
132
  session_state: Optional[Dict[str, Any]] = None
133
+ # If True, cache the session in memory
134
+ cache_session: bool = True
130
135
 
131
136
  # Team session state (shared between team leaders and team members)
132
137
  team_session_state: Optional[Dict[str, Any]] = None
138
+ workflow_session_state: Optional[Dict[str, Any]] = None
139
+
133
140
  # If True, add the session state variables in the user and system messages
134
141
  add_state_in_messages: bool = False
135
142
 
@@ -304,7 +311,9 @@ class Team:
304
311
  session_name: Optional[str] = None,
305
312
  session_state: Optional[Dict[str, Any]] = None,
306
313
  team_session_state: Optional[Dict[str, Any]] = None,
314
+ workflow_session_state: Optional[Dict[str, Any]] = None,
307
315
  add_state_in_messages: bool = False,
316
+ cache_session: bool = True,
308
317
  description: Optional[str] = None,
309
318
  instructions: Optional[Union[str, List[str], Callable]] = None,
310
319
  expected_output: Optional[str] = None,
@@ -381,8 +390,11 @@ class Team:
381
390
  self.session_name = session_name
382
391
  self.session_state = session_state
383
392
  self.team_session_state = team_session_state
393
+ self.workflow_session_state = workflow_session_state
384
394
  self.add_state_in_messages = add_state_in_messages
385
395
 
396
+ self.cache_session = cache_session
397
+
386
398
  self.description = description
387
399
  self.instructions = instructions
388
400
  self.expected_output = expected_output
@@ -472,7 +484,7 @@ class Team:
472
484
  self.full_team_session_metrics: Optional[SessionMetrics] = None
473
485
 
474
486
  self.run_id: Optional[str] = None
475
- self.run_input: Optional[Union[str, List, Dict]] = None
487
+ self.run_input: Optional[Union[str, List, Dict, BaseModel]] = None
476
488
  self.run_messages: Optional[RunMessages] = None
477
489
  self.run_response: Optional[TeamRunResponse] = None
478
490
 
@@ -549,6 +561,12 @@ class Team:
549
561
  else:
550
562
  merge_dictionaries(member.team_session_state, self.team_session_state)
551
563
 
564
+ if self.workflow_session_state is not None:
565
+ if member.workflow_session_state is None:
566
+ member.workflow_session_state = self.workflow_session_state
567
+ else:
568
+ merge_dictionaries(member.workflow_session_state, self.workflow_session_state)
569
+
552
570
  if isinstance(member, Agent):
553
571
  member.team_id = self.team_id
554
572
  member.set_agent_id()
@@ -587,6 +605,8 @@ class Team:
587
605
 
588
606
  def _reset_session(self) -> None:
589
607
  self.session_name = None
608
+ self.session_state = None
609
+ self.team_session_state = None
590
610
  self.session_metrics = None
591
611
  self.session_state = None
592
612
  self.images = None
@@ -658,11 +678,15 @@ class Team:
658
678
  self.session_state["current_user_id"] = user_id
659
679
  if self.team_session_state is not None:
660
680
  self.team_session_state["current_user_id"] = user_id
681
+ if self.workflow_session_state is not None:
682
+ self.workflow_session_state["current_user_id"] = user_id
661
683
 
662
684
  if session_id is not None:
663
685
  self.session_state["current_session_id"] = session_id
664
686
  if self.team_session_state is not None:
665
687
  self.team_session_state["current_session_id"] = session_id
688
+ if self.workflow_session_state is not None:
689
+ self.workflow_session_state["current_user_id"] = user_id
666
690
 
667
691
  def _reset_session_state(self) -> None:
668
692
  """Reset the session state for the agent."""
@@ -710,15 +734,12 @@ class Team:
710
734
 
711
735
  self._initialize_session_state(user_id=user_id, session_id=session_id)
712
736
 
713
- # Read existing session from storage
714
- self.read_from_storage(session_id=session_id)
715
-
716
737
  return session_id, user_id
717
738
 
718
739
  @overload
719
740
  def run(
720
741
  self,
721
- message: Union[str, List, Dict, Message],
742
+ message: Union[str, List, Dict, Message, BaseModel],
722
743
  *,
723
744
  stream: Literal[False] = False,
724
745
  stream_intermediate_steps: Optional[bool] = None,
@@ -737,7 +758,7 @@ class Team:
737
758
  @overload
738
759
  def run(
739
760
  self,
740
- message: Union[str, List, Dict, Message],
761
+ message: Union[str, List, Dict, Message, BaseModel],
741
762
  *,
742
763
  stream: Literal[True] = True,
743
764
  stream_intermediate_steps: Optional[bool] = None,
@@ -755,7 +776,7 @@ class Team:
755
776
 
756
777
  def run(
757
778
  self,
758
- message: Union[str, List, Dict, Message],
779
+ message: Union[str, List, Dict, Message, BaseModel],
759
780
  *,
760
781
  stream: Optional[bool] = None,
761
782
  stream_intermediate_steps: Optional[bool] = None,
@@ -780,6 +801,9 @@ class Team:
780
801
  # Initialize Team
781
802
  self.initialize_team(session_id=session_id)
782
803
 
804
+ # Read existing session from storage
805
+ self.read_from_storage(session_id=session_id)
806
+
783
807
  # Initialize Knowledge Filters
784
808
  effective_filters = knowledge_filters
785
809
 
@@ -1119,7 +1143,7 @@ class Team:
1119
1143
  @overload
1120
1144
  async def arun(
1121
1145
  self,
1122
- message: Union[str, List, Dict, Message],
1146
+ message: Union[str, List, Dict, Message, BaseModel],
1123
1147
  *,
1124
1148
  stream: Literal[False] = False,
1125
1149
  stream_intermediate_steps: Optional[bool] = None,
@@ -1138,7 +1162,7 @@ class Team:
1138
1162
  @overload
1139
1163
  async def arun(
1140
1164
  self,
1141
- message: Union[str, List, Dict, Message],
1165
+ message: Union[str, List, Dict, Message, BaseModel],
1142
1166
  *,
1143
1167
  stream: Literal[True] = True,
1144
1168
  stream_intermediate_steps: Optional[bool] = None,
@@ -1156,7 +1180,7 @@ class Team:
1156
1180
 
1157
1181
  async def arun(
1158
1182
  self,
1159
- message: Union[str, List, Dict, Message],
1183
+ message: Union[str, List, Dict, Message, BaseModel],
1160
1184
  *,
1161
1185
  stream: Optional[bool] = None,
1162
1186
  stream_intermediate_steps: Optional[bool] = None,
@@ -1172,14 +1196,17 @@ class Team:
1172
1196
  **kwargs: Any,
1173
1197
  ) -> Union[TeamRunResponse, AsyncIterator[Union[RunResponseEvent, TeamRunResponseEvent]]]:
1174
1198
  """Run the Team asynchronously and return the response."""
1175
-
1176
1199
  session_id, user_id = self._initialize_session(
1177
1200
  session_id=session_id, user_id=user_id, session_state=session_state
1178
1201
  )
1179
1202
  log_debug(f"Session ID: {session_id}", center=True)
1180
1203
 
1204
+ # Initialize Team
1181
1205
  self.initialize_team(session_id=session_id)
1182
1206
 
1207
+ # Read existing session from storage
1208
+ self.read_from_storage(session_id=session_id)
1209
+
1183
1210
  effective_filters = knowledge_filters
1184
1211
 
1185
1212
  # When filters are passed manually
@@ -1611,7 +1638,7 @@ class Team:
1611
1638
  self.memory.add_team_run(team_run) # type: ignore
1612
1639
 
1613
1640
  elif isinstance(self.memory, Memory):
1614
- # Add AgentRun to memory
1641
+ # Add run to memory
1615
1642
  self.memory.add_run(session_id=session_id, run=run_response)
1616
1643
 
1617
1644
  def _update_memory(
@@ -2388,7 +2415,7 @@ class Team:
2388
2415
 
2389
2416
  def print_response(
2390
2417
  self,
2391
- message: Optional[Union[List, Dict, str, Message]] = None,
2418
+ message: Optional[Union[List, Dict, str, Message, BaseModel]] = None,
2392
2419
  *,
2393
2420
  stream: bool = False,
2394
2421
  stream_intermediate_steps: bool = False,
@@ -2458,7 +2485,7 @@ class Team:
2458
2485
 
2459
2486
  def _print_response(
2460
2487
  self,
2461
- message: Optional[Union[List, Dict, str, Message]] = None,
2488
+ message: Optional[Union[List, Dict, str, Message, BaseModel]] = None,
2462
2489
  console: Optional[Any] = None,
2463
2490
  show_message: bool = True,
2464
2491
  show_reasoning: bool = True,
@@ -2742,7 +2769,7 @@ class Team:
2742
2769
 
2743
2770
  def _print_response_stream(
2744
2771
  self,
2745
- message: Optional[Union[List, Dict, str, Message]] = None,
2772
+ message: Optional[Union[List, Dict, str, Message, BaseModel]] = None,
2746
2773
  console: Optional[Any] = None,
2747
2774
  show_message: bool = True,
2748
2775
  show_reasoning: bool = True,
@@ -3259,7 +3286,7 @@ class Team:
3259
3286
 
3260
3287
  async def aprint_response(
3261
3288
  self,
3262
- message: Optional[Union[List, Dict, str, Message]] = None,
3289
+ message: Optional[Union[List, Dict, str, Message, BaseModel]] = None,
3263
3290
  *,
3264
3291
  stream: bool = False,
3265
3292
  stream_intermediate_steps: bool = False,
@@ -3329,7 +3356,7 @@ class Team:
3329
3356
 
3330
3357
  async def _aprint_response(
3331
3358
  self,
3332
- message: Optional[Union[List, Dict, str, Message]] = None,
3359
+ message: Optional[Union[List, Dict, str, Message, BaseModel]] = None,
3333
3360
  console: Optional[Any] = None,
3334
3361
  show_message: bool = True,
3335
3362
  show_reasoning: bool = True,
@@ -3611,7 +3638,7 @@ class Team:
3611
3638
 
3612
3639
  async def _aprint_response_stream(
3613
3640
  self,
3614
- message: Optional[Union[List, Dict, str, Message]] = None,
3641
+ message: Optional[Union[List, Dict, str, Message, BaseModel]] = None,
3615
3642
  console: Optional[Any] = None,
3616
3643
  show_message: bool = True,
3617
3644
  show_reasoning: bool = True,
@@ -4803,7 +4830,7 @@ class Team:
4803
4830
  user_id: Optional[str] = None,
4804
4831
  async_mode: bool = False,
4805
4832
  knowledge_filters: Optional[Dict[str, Any]] = None,
4806
- message: Optional[Union[str, List, Dict, Message]] = None,
4833
+ message: Optional[Union[str, List, Dict, Message, BaseModel]] = None,
4807
4834
  images: Optional[Sequence[Image]] = None,
4808
4835
  videos: Optional[Sequence[Video]] = None,
4809
4836
  audio: Optional[Sequence[Audio]] = None,
@@ -5299,7 +5326,7 @@ class Team:
5299
5326
  *,
5300
5327
  session_id: str,
5301
5328
  user_id: Optional[str] = None,
5302
- message: Optional[Union[str, List, Dict, Message]] = None,
5329
+ message: Optional[Union[str, List, Dict, Message, BaseModel]] = None,
5303
5330
  audio: Optional[Sequence[Audio]] = None,
5304
5331
  images: Optional[Sequence[Image]] = None,
5305
5332
  videos: Optional[Sequence[Video]] = None,
@@ -5381,7 +5408,7 @@ class Team:
5381
5408
 
5382
5409
  def _get_user_message(
5383
5410
  self,
5384
- message: Optional[Union[str, List, Dict, Message]] = None,
5411
+ message: Optional[Union[str, List, Dict, Message, BaseModel]] = None,
5385
5412
  user_id: Optional[str] = None,
5386
5413
  audio: Optional[Sequence[Audio]] = None,
5387
5414
  images: Optional[Sequence[Image]] = None,
@@ -5399,6 +5426,8 @@ class Team:
5399
5426
  message_str = message
5400
5427
  elif callable(message):
5401
5428
  message_str = message(agent=self)
5429
+ elif isinstance(message, BaseModel):
5430
+ message_str = message.model_dump_json(indent=2, exclude_none=True)
5402
5431
  else:
5403
5432
  raise Exception("message must be a string or a callable when add_references is True")
5404
5433
 
@@ -5544,6 +5573,7 @@ class Team:
5544
5573
  format_variables = ChainMap(
5545
5574
  self.session_state or {},
5546
5575
  self.team_session_state or {},
5576
+ self.workflow_session_state or {},
5547
5577
  self.context or {},
5548
5578
  self.extra_data or {},
5549
5579
  {"user_id": user_id} if user_id is not None else {},
@@ -5816,6 +5846,23 @@ class Team:
5816
5846
  else:
5817
5847
  merge_dictionaries(self.team_session_state, member_agent.team_session_state)
5818
5848
 
5849
+ def _update_workflow_session_state(self, member_agent: Union[Agent, "Team"]) -> None:
5850
+ """Update workflow session state from either an Agent or nested Team member"""
5851
+ # Get member state safely
5852
+ member_state = getattr(member_agent, "workflow_session_state", None)
5853
+
5854
+ # Only proceed if member has valid state
5855
+ if member_state is not None and isinstance(member_state, dict):
5856
+ # Initialize team state if needed
5857
+ if self.workflow_session_state is None:
5858
+ self.workflow_session_state = {}
5859
+
5860
+ # Only merge if both are dictionaries and member state is not empty
5861
+ if isinstance(self.workflow_session_state, dict) and member_state:
5862
+ from agno.utils.merge_dict import merge_dictionaries
5863
+
5864
+ merge_dictionaries(self.workflow_session_state, member_state)
5865
+
5819
5866
  def get_run_member_agents_function(
5820
5867
  self,
5821
5868
  session_id: str,
@@ -5944,6 +5991,8 @@ class Team:
5944
5991
  # Update team session state
5945
5992
  self._update_team_session_state(member_agent)
5946
5993
 
5994
+ self._update_workflow_session_state(member_agent)
5995
+
5947
5996
  # Update the team media
5948
5997
  self._update_team_media(member_agent.run_response) # type: ignore
5949
5998
 
@@ -6023,6 +6072,8 @@ class Team:
6023
6072
  # Update team session state
6024
6073
  self._update_team_session_state(current_agent)
6025
6074
 
6075
+ self._update_workflow_session_state(current_agent)
6076
+
6026
6077
  # Update the team media
6027
6078
  self._update_team_media(agent.run_response)
6028
6079
 
@@ -6259,6 +6310,8 @@ class Team:
6259
6310
  # Update team session state
6260
6311
  self._update_team_session_state(member_agent)
6261
6312
 
6313
+ self._update_workflow_session_state(member_agent)
6314
+
6262
6315
  # Update the team media
6263
6316
  self._update_team_media(member_agent.run_response) # type: ignore
6264
6317
 
@@ -6389,6 +6442,8 @@ class Team:
6389
6442
  # Update team session state
6390
6443
  self._update_team_session_state(member_agent)
6391
6444
 
6445
+ self._update_workflow_session_state(member_agent)
6446
+
6392
6447
  # Update the team media
6393
6448
  self._update_team_media(member_agent.run_response) # type: ignore
6394
6449
 
@@ -6627,6 +6682,8 @@ class Team:
6627
6682
  # Update team session state
6628
6683
  self._update_team_session_state(member_agent)
6629
6684
 
6685
+ self._update_workflow_session_state(member_agent)
6686
+
6630
6687
  # Update the team media
6631
6688
  self._update_team_media(member_agent.run_response) # type: ignore
6632
6689
 
@@ -6756,6 +6813,8 @@ class Team:
6756
6813
  # Update team session state
6757
6814
  self._update_team_session_state(member_agent)
6758
6815
 
6816
+ self._update_workflow_session_state(member_agent)
6817
+
6759
6818
  # Update the team media
6760
6819
  self._update_team_media(member_agent.run_response) # type: ignore
6761
6820
 
@@ -6797,6 +6856,11 @@ class Team:
6797
6856
  self.team_session = cast(
6798
6857
  TeamSession, self.storage.upsert(session=self._get_team_session(session_id=session_id, user_id=user_id))
6799
6858
  )
6859
+
6860
+ # Remove session from memory
6861
+ if not self.cache_session:
6862
+ if self.memory is not None and self.memory.runs is not None and session_id in self.memory.runs:
6863
+ self.memory.runs.pop(session_id) # type: ignore
6800
6864
  return self.team_session
6801
6865
 
6802
6866
  def rename_session(self, session_name: str, session_id: Optional[str] = None) -> None:
@@ -6872,6 +6936,20 @@ class Team:
6872
6936
  # Update the current team_session_state
6873
6937
  self.team_session_state = team_session_state_from_db
6874
6938
 
6939
+ if "workflow_session_state" in session.session_data:
6940
+ workflow_session_state_from_db = session.session_data.get("workflow_session_state")
6941
+ if (
6942
+ workflow_session_state_from_db is not None
6943
+ and isinstance(workflow_session_state_from_db, dict)
6944
+ and len(workflow_session_state_from_db) > 0
6945
+ ):
6946
+ # If the workflow_session_state is already set, merge the workflow_session_state from the database with the current workflow_session_state
6947
+ if self.workflow_session_state is not None and len(self.workflow_session_state) > 0:
6948
+ # This updates workflow_session_state_from_db
6949
+ merge_dictionaries(workflow_session_state_from_db, self.workflow_session_state)
6950
+ # Update the current workflow_session_state
6951
+ self.workflow_session_state = workflow_session_state_from_db
6952
+
6875
6953
  # Get the session_metrics from the database
6876
6954
  if "session_metrics" in session.session_data:
6877
6955
  session_metrics_from_db = session.session_data.get("session_metrics")
@@ -6960,6 +7038,7 @@ class Team:
6960
7038
  self.memory.runs[session.session_id] = []
6961
7039
  for run in session.memory["runs"]:
6962
7040
  run_session_id = run["session_id"]
7041
+
6963
7042
  if "team_id" in run:
6964
7043
  self.memory.runs[run_session_id].append(TeamRunResponse.from_dict(run))
6965
7044
  else:
@@ -7645,6 +7724,8 @@ class Team:
7645
7724
  session_data["session_state"] = self.session_state
7646
7725
  if self.team_session_state is not None and len(self.team_session_state) > 0:
7647
7726
  session_data["team_session_state"] = self.team_session_state
7727
+ if self.workflow_session_state is not None and len(self.workflow_session_state) > 0:
7728
+ session_data["workflow_session_state"] = self.workflow_session_state
7648
7729
  if self.session_metrics is not None:
7649
7730
  session_data["session_metrics"] = asdict(self.session_metrics) if self.session_metrics is not None else None
7650
7731
  if self.images is not None:
@@ -7672,11 +7753,13 @@ class Team:
7672
7753
  run_responses = self.memory.runs.get(session_id)
7673
7754
  if run_responses is not None:
7674
7755
  memory_dict["runs"] = [rr.to_dict() for rr in run_responses]
7756
+
7675
7757
  return TeamSession(
7676
7758
  session_id=session_id,
7677
7759
  team_id=self.team_id,
7678
7760
  user_id=user_id,
7679
7761
  team_session_id=self.team_session_id,
7762
+ workflow_session_id=self.workflow_session_id,
7680
7763
  memory=memory_dict,
7681
7764
  team_data=self._get_team_data(),
7682
7765
  session_data=self._get_session_data(),
@@ -7701,6 +7784,7 @@ class Team:
7701
7784
  run_id=self.run_id, # type: ignore
7702
7785
  run_data=run_data,
7703
7786
  team_session_id=team_session.team_session_id,
7787
+ workflow_session_id=self.workflow_session_id,
7704
7788
  session_id=team_session.session_id,
7705
7789
  team_data=team_session.to_dict() if self.monitoring else team_session.telemetry_data(),
7706
7790
  ),
agno/tools/decorator.py CHANGED
@@ -9,6 +9,49 @@ F = TypeVar("F", bound=Callable[..., Any])
9
9
  ToolConfig = TypeVar("ToolConfig", bound=Dict[str, Any])
10
10
 
11
11
 
12
+ def _is_async_function(func: Callable) -> bool:
13
+ """
14
+ Check if a function is async, even when wrapped by decorators like @staticmethod.
15
+
16
+ This function tries to detect async functions by:
17
+ 1. Checking the function directly with inspect functions
18
+ 2. Looking at the original function if it's wrapped
19
+ 3. Checking the function's code object for async indicators
20
+ """
21
+ from inspect import iscoroutine, iscoroutinefunction
22
+
23
+ # First, try the standard inspect functions
24
+ if iscoroutinefunction(func) or iscoroutine(func):
25
+ return True
26
+
27
+ # If the function has a __wrapped__ attribute, check the original function
28
+ if hasattr(func, "__wrapped__"):
29
+ original_func = func.__wrapped__
30
+ if iscoroutinefunction(original_func) or iscoroutine(original_func):
31
+ return True
32
+
33
+ # Check if the function has CO_COROUTINE flag in its code object
34
+ try:
35
+ if hasattr(func, "__code__") and func.__code__.co_flags & 0x80: # CO_COROUTINE flag
36
+ return True
37
+ except (AttributeError, TypeError):
38
+ pass
39
+
40
+ # For static methods, try to get the original function
41
+ try:
42
+ if hasattr(func, "__func__"):
43
+ original_func = func.__func__
44
+ if iscoroutinefunction(original_func) or iscoroutine(original_func):
45
+ return True
46
+ # Check the code object of the original function
47
+ if hasattr(original_func, "__code__") and original_func.__code__.co_flags & 0x80:
48
+ return True
49
+ except (AttributeError, TypeError):
50
+ pass
51
+
52
+ return False
53
+
54
+
12
55
  @overload
13
56
  def tool() -> Callable[[F], Function]: ...
14
57
 
@@ -125,7 +168,7 @@ def tool(*args, **kwargs) -> Union[Function, Callable[[F], Function]]:
125
168
  )
126
169
 
127
170
  def decorator(func: F) -> Function:
128
- from inspect import isasyncgenfunction, iscoroutine, iscoroutinefunction
171
+ from inspect import isasyncgenfunction
129
172
 
130
173
  @wraps(func)
131
174
  def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
@@ -163,7 +206,7 @@ def tool(*args, **kwargs) -> Union[Function, Callable[[F], Function]]:
163
206
  # Choose appropriate wrapper based on function type
164
207
  if isasyncgenfunction(func):
165
208
  wrapper = async_gen_wrapper
166
- elif iscoroutinefunction(func) or iscoroutine(func):
209
+ elif _is_async_function(func):
167
210
  wrapper = async_wrapper
168
211
  else:
169
212
  wrapper = sync_wrapper
agno/tools/function.py CHANGED
@@ -151,7 +151,7 @@ class Function(BaseModel):
151
151
  param_type_hints = {
152
152
  name: type_hints.get(name)
153
153
  for name in sig.parameters
154
- if name != "return" and name not in ["agent", "team"]
154
+ if name != "return" and name not in ["agent", "team", "self"]
155
155
  }
156
156
 
157
157
  # Parse docstring for parameters
@@ -177,7 +177,9 @@ class Function(BaseModel):
177
177
  # If strict=True mark all fields as required
178
178
  # See: https://platform.openai.com/docs/guides/structured-outputs/supported-schemas#all-fields-must-be-required
179
179
  if strict:
180
- parameters["required"] = [name for name in parameters["properties"] if name not in ["agent", "team"]]
180
+ parameters["required"] = [
181
+ name for name in parameters["properties"] if name not in ["agent", "team", "self"]
182
+ ]
181
183
  else:
182
184
  # Mark a field as required if it has no default value (this would include optional fields)
183
185
  parameters["required"] = [
@@ -235,7 +237,7 @@ class Function(BaseModel):
235
237
  # log_info(f"Type hints for {self.name}: {type_hints}")
236
238
 
237
239
  # Filter out return type and only process parameters
238
- excluded_params = ["return", "agent", "team"]
240
+ excluded_params = ["return", "agent", "team", "self"]
239
241
  if self.requires_user_input and self.user_input_fields:
240
242
  if len(self.user_input_fields) == 0:
241
243
  excluded_params.extend(list(type_hints.keys()))
@@ -337,7 +339,9 @@ class Function(BaseModel):
337
339
 
338
340
  def process_schema_for_strict(self):
339
341
  self.parameters["additionalProperties"] = False
340
- self.parameters["required"] = [name for name in self.parameters["properties"] if name not in ["agent", "team"]]
342
+ self.parameters["required"] = [
343
+ name for name in self.parameters["properties"] if name not in ["agent", "team", "self"]
344
+ ]
341
345
 
342
346
  def _get_cache_key(self, entrypoint_args: Dict[str, Any], call_args: Optional[Dict[str, Any]] = None) -> str:
343
347
  """Generate a cache key based on function name and arguments."""
@@ -554,7 +558,7 @@ class FunctionCall(BaseModel):
554
558
  from functools import reduce
555
559
  from inspect import iscoroutinefunction
556
560
 
557
- def execute_entrypoint():
561
+ def execute_entrypoint(name, func, args):
558
562
  """Execute the entrypoint function."""
559
563
  arguments = entrypoint_args.copy()
560
564
  if self.arguments is not None:
@@ -572,9 +576,9 @@ class FunctionCall(BaseModel):
572
576
  # Pass the inner function as next_func to the hook
573
577
  # The hook will call next_func to continue the chain
574
578
  def next_func(**kwargs):
575
- return inner_func()
579
+ return inner_func(name, func, kwargs)
576
580
 
577
- hook_args = self._build_hook_args(hook, name, func, args)
581
+ hook_args = self._build_hook_args(hook, name, next_func, args)
578
582
 
579
583
  return hook(**hook_args)
580
584
 
@@ -716,7 +720,7 @@ class FunctionCall(BaseModel):
716
720
  from functools import reduce
717
721
  from inspect import isasyncgen, isasyncgenfunction, iscoroutinefunction
718
722
 
719
- async def execute_entrypoint_async():
723
+ async def execute_entrypoint_async(name, func, args):
720
724
  """Execute the entrypoint function asynchronously."""
721
725
  arguments = entrypoint_args.copy()
722
726
  if self.arguments is not None:
@@ -729,7 +733,7 @@ class FunctionCall(BaseModel):
729
733
  result = await result
730
734
  return result
731
735
 
732
- def execute_entrypoint():
736
+ def execute_entrypoint(name, func, args):
733
737
  """Execute the entrypoint function synchronously."""
734
738
  arguments = entrypoint_args.copy()
735
739
  if self.arguments is not None:
@@ -750,11 +754,11 @@ class FunctionCall(BaseModel):
750
754
  # The hook will call next_func to continue the chain
751
755
  async def next_func(**kwargs):
752
756
  if iscoroutinefunction(inner_func):
753
- return await inner_func()
757
+ return await inner_func(name, func, kwargs)
754
758
  else:
755
- return inner_func()
759
+ return inner_func(name, func, kwargs)
756
760
 
757
- hook_args = self._build_hook_args(hook, name, func, args)
761
+ hook_args = self._build_hook_args(hook, name, next_func, args)
758
762
 
759
763
  if iscoroutinefunction(hook):
760
764
  return await hook(**hook_args)
agno/utils/log.py CHANGED
@@ -7,6 +7,7 @@ from rich.text import Text
7
7
 
8
8
  LOGGER_NAME = "agno"
9
9
  TEAM_LOGGER_NAME = f"{LOGGER_NAME}-team"
10
+ WORKFLOW_LOGGER_NAME = f"{LOGGER_NAME}-workflow"
10
11
 
11
12
  # Define custom styles for different log sources
12
13
  LOG_STYLES = {
@@ -18,6 +19,10 @@ LOG_STYLES = {
18
19
  "debug": "magenta",
19
20
  "info": "steel_blue1",
20
21
  },
22
+ "workflow": {
23
+ "debug": "sandy_brown",
24
+ "info": "orange3",
25
+ },
21
26
  }
22
27
 
23
28
 
@@ -92,6 +97,7 @@ def build_logger(logger_name: str, source_type: Optional[str] = None) -> Any:
92
97
 
93
98
  agent_logger: AgnoLogger = build_logger(LOGGER_NAME, source_type="agent")
94
99
  team_logger: AgnoLogger = build_logger(TEAM_LOGGER_NAME, source_type="team")
100
+ workflow_logger: AgnoLogger = build_logger(WORKFLOW_LOGGER_NAME, source_type="workflow")
95
101
 
96
102
  # Set the default logger to the agent logger
97
103
  logger: AgnoLogger = agent_logger
@@ -146,6 +152,12 @@ def use_agent_logger():
146
152
  logger = agent_logger
147
153
 
148
154
 
155
+ def use_workflow_logger():
156
+ """Switch the default logger to use workflow_logger"""
157
+ global logger
158
+ logger = workflow_logger
159
+
160
+
149
161
  def log_debug(msg, center: bool = False, symbol: str = "*", log_level: Literal[1, 2] = 1, *args, **kwargs):
150
162
  global logger
151
163
  global debug_on
agno/utils/message.py CHANGED
@@ -1,13 +1,17 @@
1
1
  from typing import Dict, List, Union
2
2
 
3
+ from pydantic import BaseModel
4
+
3
5
  from agno.models.message import Message
4
6
 
5
7
 
6
- def get_text_from_message(message: Union[List, Dict, str, Message]) -> str:
8
+ def get_text_from_message(message: Union[List, Dict, str, Message, BaseModel]) -> str:
7
9
  """Return the user texts from the message"""
8
10
 
9
11
  if isinstance(message, str):
10
12
  return message
13
+ if isinstance(message, BaseModel):
14
+ return message.model_dump_json(indent=2, exclude_none=True)
11
15
  if isinstance(message, list):
12
16
  text_messages = []
13
17
  if len(message) == 0: