agno 2.1.7__py3-none-any.whl → 2.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
agno/team/team.py CHANGED
@@ -862,6 +862,9 @@ class Team:
862
862
  run_response: TeamRunOutput,
863
863
  run_input: TeamRunInput,
864
864
  session: TeamSession,
865
+ session_state: Optional[Dict[str, Any]] = None,
866
+ dependencies: Optional[Dict[str, Any]] = None,
867
+ metadata: Optional[Dict[str, Any]] = None,
865
868
  user_id: Optional[str] = None,
866
869
  debug_mode: Optional[bool] = None,
867
870
  **kwargs: Any,
@@ -876,6 +879,9 @@ class Team:
876
879
  "team": self,
877
880
  "session": session,
878
881
  "user_id": user_id,
882
+ "metadata": metadata,
883
+ "session_state": session_state,
884
+ "dependencies": dependencies,
879
885
  "debug_mode": debug_mode or self.debug_mode,
880
886
  }
881
887
  all_args.update(kwargs)
@@ -918,6 +924,9 @@ class Team:
918
924
  run_response: TeamRunOutput,
919
925
  run_input: TeamRunInput,
920
926
  session: TeamSession,
927
+ session_state: Optional[Dict[str, Any]] = None,
928
+ dependencies: Optional[Dict[str, Any]] = None,
929
+ metadata: Optional[Dict[str, Any]] = None,
921
930
  user_id: Optional[str] = None,
922
931
  debug_mode: Optional[bool] = None,
923
932
  **kwargs: Any,
@@ -932,6 +941,9 @@ class Team:
932
941
  "team": self,
933
942
  "session": session,
934
943
  "user_id": user_id,
944
+ "session_state": session_state,
945
+ "dependencies": dependencies,
946
+ "metadata": metadata,
935
947
  "debug_mode": debug_mode or self.debug_mode,
936
948
  }
937
949
  all_args.update(kwargs)
@@ -977,6 +989,9 @@ class Team:
977
989
  hooks: Optional[List[Callable[..., Any]]],
978
990
  run_output: TeamRunOutput,
979
991
  session: TeamSession,
992
+ session_state: Optional[Dict[str, Any]] = None,
993
+ dependencies: Optional[Dict[str, Any]] = None,
994
+ metadata: Optional[Dict[str, Any]] = None,
980
995
  user_id: Optional[str] = None,
981
996
  debug_mode: Optional[bool] = None,
982
997
  **kwargs: Any,
@@ -991,6 +1006,9 @@ class Team:
991
1006
  "team": self,
992
1007
  "session": session,
993
1008
  "user_id": user_id,
1009
+ "session_state": session_state,
1010
+ "dependencies": dependencies,
1011
+ "metadata": metadata,
994
1012
  "debug_mode": debug_mode or self.debug_mode,
995
1013
  }
996
1014
  all_args.update(kwargs)
@@ -1014,6 +1032,9 @@ class Team:
1014
1032
  run_output: TeamRunOutput,
1015
1033
  session: TeamSession,
1016
1034
  user_id: Optional[str] = None,
1035
+ session_state: Optional[Dict[str, Any]] = None,
1036
+ dependencies: Optional[Dict[str, Any]] = None,
1037
+ metadata: Optional[Dict[str, Any]] = None,
1017
1038
  debug_mode: Optional[bool] = None,
1018
1039
  **kwargs: Any,
1019
1040
  ) -> None:
@@ -1027,6 +1048,9 @@ class Team:
1027
1048
  "team": self,
1028
1049
  "session": session,
1029
1050
  "user_id": user_id,
1051
+ "session_state": session_state,
1052
+ "dependencies": dependencies,
1053
+ "metadata": metadata,
1030
1054
  "debug_mode": debug_mode or self.debug_mode,
1031
1055
  }
1032
1056
  all_args.update(kwargs)
@@ -1091,6 +1115,9 @@ class Team:
1091
1115
  run_response=run_response,
1092
1116
  run_input=run_input,
1093
1117
  session=session,
1118
+ session_state=session_state,
1119
+ dependencies=dependencies,
1120
+ metadata=metadata,
1094
1121
  user_id=user_id,
1095
1122
  debug_mode=debug_mode,
1096
1123
  **kwargs,
@@ -1182,26 +1209,29 @@ class Team:
1182
1209
  else:
1183
1210
  self._scrub_media_from_run_output(run_response)
1184
1211
 
1185
- run_response.status = RunStatus.completed
1186
-
1187
1212
  # Parse team response model
1188
1213
  self._convert_response_to_structured_format(run_response=run_response)
1189
1214
 
1190
- # Set the run duration
1191
- if run_response.metrics:
1192
- run_response.metrics.stop_timer()
1193
-
1194
1215
  # 6. Execute post-hooks after output is generated but before response is returned
1195
1216
  if self.post_hooks is not None:
1196
1217
  self._execute_post_hooks(
1197
1218
  hooks=self.post_hooks, # type: ignore
1198
1219
  run_output=run_response,
1199
1220
  session=session,
1221
+ session_state=session_state,
1222
+ dependencies=dependencies,
1223
+ metadata=metadata,
1200
1224
  user_id=user_id,
1201
1225
  debug_mode=debug_mode,
1202
1226
  **kwargs,
1203
1227
  )
1204
1228
 
1229
+ run_response.status = RunStatus.completed
1230
+
1231
+ # Set the run duration
1232
+ if run_response.metrics:
1233
+ run_response.metrics.stop_timer()
1234
+
1205
1235
  # 7. Add the RunOutput to Team Session
1206
1236
  session.upsert_run(run_response=run_response)
1207
1237
 
@@ -1278,6 +1308,9 @@ class Team:
1278
1308
  run_response=run_response,
1279
1309
  run_input=run_input,
1280
1310
  session=session,
1311
+ session_state=session_state,
1312
+ dependencies=dependencies,
1313
+ metadata=metadata,
1281
1314
  user_id=user_id,
1282
1315
  debug_mode=debug_mode,
1283
1316
  **kwargs,
@@ -1396,14 +1429,25 @@ class Team:
1396
1429
  session=session, run_response=run_response, stream_intermediate_steps=stream_intermediate_steps
1397
1430
  )
1398
1431
 
1399
- run_response.status = RunStatus.completed
1432
+ # Execute post-hooks after output is generated but before response is returned
1433
+ if self.post_hooks is not None:
1434
+ self._execute_post_hooks(
1435
+ hooks=self.post_hooks, # type: ignore
1436
+ run_output=run_response,
1437
+ session_state=session_state,
1438
+ dependencies=dependencies,
1439
+ metadata=metadata,
1440
+ session=session,
1441
+ user_id=user_id,
1442
+ debug_mode=debug_mode,
1443
+ **kwargs,
1444
+ )
1400
1445
 
1446
+ run_response.status = RunStatus.completed
1401
1447
  # Set the run duration
1402
1448
  if run_response.metrics:
1403
1449
  run_response.metrics.stop_timer()
1404
1450
 
1405
- # TODO: For now we don't run post-hooks during streaming
1406
-
1407
1451
  # 5. Add the run to Team Session
1408
1452
  session.upsert_run(run_response=run_response)
1409
1453
 
@@ -1799,6 +1843,9 @@ class Team:
1799
1843
 
1800
1844
  register_run(run_response.run_id) # type: ignore
1801
1845
 
1846
+ if dependencies is not None:
1847
+ await self._aresolve_run_dependencies(dependencies=dependencies)
1848
+
1802
1849
  # 1. Read or create session. Reads from the database if provided.
1803
1850
  if self._has_async_db():
1804
1851
  team_session = await self._aread_or_create_session(session_id=session_id, user_id=user_id)
@@ -1820,6 +1867,9 @@ class Team:
1820
1867
  session=team_session,
1821
1868
  user_id=user_id,
1822
1869
  debug_mode=debug_mode,
1870
+ session_state=session_state,
1871
+ dependencies=dependencies,
1872
+ metadata=metadata,
1823
1873
  **kwargs,
1824
1874
  )
1825
1875
 
@@ -1907,21 +1957,35 @@ class Team:
1907
1957
  else:
1908
1958
  self._scrub_media_from_run_output(run_response)
1909
1959
 
1910
- # 9. Add the run to memory
1911
- team_session.upsert_run(run_response=run_response)
1960
+ # 11. Parse team response model
1961
+ self._convert_response_to_structured_format(run_response=run_response)
1912
1962
 
1913
- # 10. Calculate session metrics
1914
- self._update_session_metrics(session=team_session)
1963
+ # Execute post-hooks after output is generated but before response is returned
1964
+ if self.post_hooks is not None:
1965
+ await self._aexecute_post_hooks(
1966
+ hooks=self.post_hooks, # type: ignore
1967
+ run_output=run_response,
1968
+ session=team_session,
1969
+ user_id=user_id,
1970
+ debug_mode=debug_mode,
1971
+ session_state=session_state,
1972
+ dependencies=dependencies,
1973
+ metadata=metadata,
1974
+ **kwargs,
1975
+ )
1915
1976
 
1916
1977
  run_response.status = RunStatus.completed
1917
1978
 
1918
- # 11. Parse team response model
1919
- self._convert_response_to_structured_format(run_response=run_response)
1920
-
1921
1979
  # Set the run duration
1922
1980
  if run_response.metrics:
1923
1981
  run_response.metrics.stop_timer()
1924
1982
 
1983
+ # 9. Add the run to memory
1984
+ team_session.upsert_run(run_response=run_response)
1985
+
1986
+ # 10. Calculate session metrics
1987
+ self._update_session_metrics(session=team_session)
1988
+
1925
1989
  # 12. Update Team Memory
1926
1990
  async for _ in self._amake_memories_and_summaries(
1927
1991
  run_response=run_response,
@@ -1944,17 +2008,6 @@ class Team:
1944
2008
  # Log Team Telemetry
1945
2009
  await self._alog_team_telemetry(session_id=team_session.session_id, run_id=run_response.run_id)
1946
2010
 
1947
- # 15. Execute post-hooks after output is generated but before response is returned
1948
- if self.post_hooks is not None:
1949
- await self._aexecute_post_hooks(
1950
- hooks=self.post_hooks, # type: ignore
1951
- run_output=run_response,
1952
- session=team_session,
1953
- user_id=user_id,
1954
- debug_mode=debug_mode,
1955
- **kwargs,
1956
- )
1957
-
1958
2011
  log_debug(f"Team Run End: {run_response.run_id}", center=True, symbol="*")
1959
2012
 
1960
2013
  cleanup_run(run_response.run_id) # type: ignore
@@ -2005,7 +2058,7 @@ class Team:
2005
2058
 
2006
2059
  # 1. Resolve dependencies
2007
2060
  if dependencies is not None:
2008
- self._resolve_run_dependencies(dependencies=dependencies)
2061
+ await self._aresolve_run_dependencies(dependencies=dependencies)
2009
2062
 
2010
2063
  # 2. Read or create session. Reads from the database if provided.
2011
2064
  if self._has_async_db():
@@ -2028,6 +2081,9 @@ class Team:
2028
2081
  session=team_session,
2029
2082
  user_id=user_id,
2030
2083
  debug_mode=debug_mode,
2084
+ session_state=session_state,
2085
+ dependencies=dependencies,
2086
+ metadata=metadata,
2031
2087
  **kwargs,
2032
2088
  )
2033
2089
  async for pre_hook_event in pre_hook_iterator:
@@ -2143,6 +2199,24 @@ class Team:
2143
2199
  ):
2144
2200
  yield event
2145
2201
 
2202
+ # Execute post-hooks after output is generated but before response is returned
2203
+ if self.post_hooks is not None:
2204
+ self._execute_post_hooks(
2205
+ hooks=self.post_hooks, # type: ignore
2206
+ run_output=run_response,
2207
+ session_state=session_state,
2208
+ dependencies=dependencies,
2209
+ metadata=metadata,
2210
+ session=team_session,
2211
+ user_id=user_id,
2212
+ debug_mode=debug_mode,
2213
+ **kwargs,
2214
+ )
2215
+
2216
+ # Set the run duration
2217
+ if run_response.metrics:
2218
+ run_response.metrics.stop_timer()
2219
+
2146
2220
  run_response.status = RunStatus.completed
2147
2221
 
2148
2222
  # 10. Add the run to memory
@@ -4,7 +4,7 @@ import uuid
4
4
  from functools import wraps
5
5
  from os import getenv
6
6
  from pathlib import Path
7
- from typing import Any, Dict, List, Optional
7
+ from typing import Any, Dict, List, Optional, cast
8
8
 
9
9
  from agno.tools import Toolkit
10
10
  from agno.utils.log import log_debug, log_error, log_info
@@ -164,8 +164,10 @@ class GoogleCalendarTools(Toolkit):
164
164
  )
165
165
 
166
166
  try:
167
+ service = cast(Resource, self.service)
168
+
167
169
  events_result = (
168
- self.service.events() # type: ignore
170
+ service.events()
169
171
  .list(
170
172
  calendarId=self.calendar_id,
171
173
  timeMin=start_date,
@@ -194,6 +196,7 @@ class GoogleCalendarTools(Toolkit):
194
196
  timezone: Optional[str] = "UTC",
195
197
  attendees: Optional[List[str]] = None,
196
198
  add_google_meet_link: Optional[bool] = False,
199
+ notify_attendees: Optional[bool] = False,
197
200
  ) -> str:
198
201
  """
199
202
  Create a new event in the Google Calendar.
@@ -207,6 +210,7 @@ class GoogleCalendarTools(Toolkit):
207
210
  timezone (Optional[str]): Timezone for the event (default: UTC)
208
211
  attendees (Optional[List[str]]): List of email addresses of the attendees
209
212
  add_google_meet_link (Optional[bool]): Whether to add a Google Meet video link to the event
213
+ notify_attendees (Optional[bool]): Whether to send email notifications to attendees (default: False)
210
214
 
211
215
  Returns:
212
216
  str: JSON string containing the created Google Calendar event or error message
@@ -241,12 +245,18 @@ class GoogleCalendarTools(Toolkit):
241
245
  # Remove None values
242
246
  event = {k: v for k, v in event.items() if v is not None}
243
247
 
248
+ # Determine sendUpdates value based on notify_attendees parameter
249
+ send_updates = "all" if notify_attendees and attendees else "none"
250
+
251
+ service = cast(Resource, self.service)
252
+
244
253
  event_result = (
245
- self.service.events() # type: ignore
254
+ service.events()
246
255
  .insert(
247
256
  calendarId=self.calendar_id,
248
257
  body=event,
249
258
  conferenceDataVersion=1 if add_google_meet_link else 0,
259
+ sendUpdates=send_updates,
250
260
  )
251
261
  .execute()
252
262
  )
@@ -267,6 +277,7 @@ class GoogleCalendarTools(Toolkit):
267
277
  end_date: Optional[str] = None,
268
278
  timezone: Optional[str] = None,
269
279
  attendees: Optional[List[str]] = None,
280
+ notify_attendees: Optional[bool] = False,
270
281
  ) -> str:
271
282
  """
272
283
  Update an existing event in the Google Calendar.
@@ -280,13 +291,16 @@ class GoogleCalendarTools(Toolkit):
280
291
  end_date (Optional[str]): New end date and time in ISO format (YYYY-MM-DDTHH:MM:SS)
281
292
  timezone (Optional[str]): New timezone for the event
282
293
  attendees (Optional[List[str]]): Updated list of attendee email addresses
294
+ notify_attendees (Optional[bool]): Whether to send email notifications to attendees (default: False)
283
295
 
284
296
  Returns:
285
297
  str: JSON string containing the updated Google Calendar event or error message
286
298
  """
287
299
  try:
300
+ service = cast(Resource, self.service)
301
+
288
302
  # First get the existing event to preserve its structure
289
- event = self.service.events().get(calendarId=self.calendar_id, eventId=event_id).execute() # type: ignore
303
+ event = service.events().get(calendarId=self.calendar_id, eventId=event_id).execute()
290
304
 
291
305
  # Update only the fields that are provided
292
306
  if title is not None:
@@ -317,9 +331,18 @@ class GoogleCalendarTools(Toolkit):
317
331
  except ValueError:
318
332
  return json.dumps({"error": f"Invalid end datetime format: {end_date}. Use ISO format."})
319
333
 
334
+ # Determine sendUpdates value based on notify_attendees parameter
335
+ send_updates = "all" if notify_attendees and attendees else "none"
336
+
320
337
  # Update the event
338
+
321
339
  updated_event = (
322
- self.service.events().update(calendarId=self.calendar_id, eventId=event_id, body=event).execute() # type: ignore
340
+ service.events().update(
341
+ calendarId=self.calendar_id,
342
+ eventId=event_id,
343
+ body=event,
344
+ sendUpdates=send_updates
345
+ ).execute()
323
346
  )
324
347
 
325
348
  log_debug(f"Event {event_id} updated successfully.")
@@ -329,18 +352,28 @@ class GoogleCalendarTools(Toolkit):
329
352
  return json.dumps({"error": f"An error occurred: {error}"})
330
353
 
331
354
  @authenticate
332
- def delete_event(self, event_id: str) -> str:
355
+ def delete_event(self, event_id: str, notify_attendees: Optional[bool] = True) -> str:
333
356
  """
334
357
  Delete an event from the Google Calendar.
335
358
 
336
359
  Args:
337
360
  event_id (str): ID of the event to delete
361
+ notify_attendees (Optional[bool]): Whether to send email notifications to attendees (default: False)
338
362
 
339
363
  Returns:
340
364
  str: JSON string containing success message or error message
341
365
  """
342
366
  try:
343
- self.service.events().delete(calendarId=self.calendar_id, eventId=event_id).execute() # type: ignore
367
+ # Determine sendUpdates value based on notify_attendees parameter
368
+ send_updates = "all" if notify_attendees else "none"
369
+
370
+ service = cast(Resource, self.service)
371
+
372
+ service.events().delete(
373
+ calendarId=self.calendar_id,
374
+ eventId=event_id,
375
+ sendUpdates=send_updates
376
+ ).execute()
344
377
 
345
378
  log_debug(f"Event {event_id} deleted successfully.")
346
379
  return json.dumps({"success": True, "message": f"Event {event_id} deleted successfully."})
@@ -366,6 +399,8 @@ class GoogleCalendarTools(Toolkit):
366
399
  str: JSON string containing all Google Calendar events or error message
367
400
  """
368
401
  try:
402
+ service = cast(Resource, self.service)
403
+
369
404
  params = {
370
405
  "calendarId": self.calendar_id,
371
406
  "maxResults": min(max_results, 100),
@@ -412,7 +447,7 @@ class GoogleCalendarTools(Toolkit):
412
447
  if page_token:
413
448
  params["pageToken"] = page_token
414
449
 
415
- events_result = self.service.events().list(**params).execute() # type: ignore
450
+ events_result = service.events().list(**params).execute()
416
451
  all_events.extend(events_result.get("items", []))
417
452
 
418
453
  page_token = events_result.get("nextPageToken")
agno/tools/jira.py CHANGED
@@ -22,6 +22,7 @@ class JiraTools(Toolkit):
22
22
  enable_create_issue: bool = True,
23
23
  enable_search_issues: bool = True,
24
24
  enable_add_comment: bool = True,
25
+ enable_add_worklog: bool = True,
25
26
  all: bool = False,
26
27
  **kwargs,
27
28
  ):
@@ -55,6 +56,8 @@ class JiraTools(Toolkit):
55
56
  tools.append(self.search_issues)
56
57
  if enable_add_comment or all:
57
58
  tools.append(self.add_comment)
59
+ if enable_add_worklog or all:
60
+ tools.append(self.add_worklog)
58
61
 
59
62
  super().__init__(name="jira_tools", tools=tools, **kwargs)
60
63
 
@@ -148,3 +151,20 @@ class JiraTools(Toolkit):
148
151
  except Exception as e:
149
152
  logger.error(f"Error adding comment to issue {issue_key}: {e}")
150
153
  return json.dumps({"error": str(e)})
154
+
155
+ def add_worklog(self, issue_key: str, time_spent: str, comment: Optional[str] = None) -> str:
156
+ """
157
+ Adds a worklog entry to log time spent on a specific Jira issue.
158
+
159
+ :param issue_key: The key of the issue to log work against (e.g., 'PROJ-123').
160
+ :param time_spent: The amount of time spent. Use Jira's format, e.g., '2h', '30m', '1d 4h'.
161
+ :param comment: An optional comment describing the work done.
162
+ :return: A JSON string indicating success or containing an error message.
163
+ """
164
+ try:
165
+ self.jira.add_worklog(issue=issue_key, timeSpent=time_spent, comment=comment)
166
+ log_debug(f"Worklog of '{time_spent}' added to issue {issue_key}")
167
+ return json.dumps({"status": "success", "issue_key": issue_key, "time_spent": time_spent})
168
+ except Exception as e:
169
+ logger.error(f"Error adding worklog to issue {issue_key}: {e}")
170
+ return json.dumps({"error": str(e)})
agno/workflow/step.py CHANGED
@@ -5,6 +5,7 @@ from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterator, List
5
5
  from uuid import uuid4
6
6
 
7
7
  from pydantic import BaseModel
8
+ from typing_extensions import TypeGuard
8
9
 
9
10
  from agno.agent import Agent
10
11
  from agno.media import Audio, Image, Video
@@ -191,9 +192,8 @@ class Step:
191
192
  session_state: Optional[Dict[str, Any]] = None,
192
193
  ) -> Any:
193
194
  """Call custom async function with session_state support if the function accepts it"""
194
- import inspect
195
195
 
196
- if inspect.isasyncgenfunction(func):
196
+ if _is_async_generator_function(func):
197
197
  if session_state is not None and self._function_has_session_state_param():
198
198
  return func(step_input, session_state)
199
199
  else:
@@ -231,16 +231,16 @@ class Step:
231
231
  try:
232
232
  response: Union[RunOutput, TeamRunOutput, StepOutput]
233
233
  if self._executor_type == "function":
234
- if inspect.iscoroutinefunction(self.active_executor) or inspect.isasyncgenfunction(
235
- self.active_executor
236
- ):
234
+ if _is_async_callable(self.active_executor) or _is_async_generator_function(self.active_executor):
237
235
  raise ValueError("Cannot use async function with synchronous execution")
238
- if inspect.isgeneratorfunction(self.active_executor):
236
+ if _is_generator_function(self.active_executor):
239
237
  content = ""
240
238
  final_response = None
241
239
  try:
242
240
  for chunk in self._call_custom_function(
243
- self.active_executor, step_input, session_state_copy
241
+ self.active_executor,
242
+ step_input,
243
+ session_state_copy, # type: ignore[arg-type]
244
244
  ): # type: ignore
245
245
  if (
246
246
  hasattr(chunk, "content")
@@ -368,9 +368,7 @@ class Step:
368
368
  return False
369
369
 
370
370
  try:
371
- from inspect import signature
372
-
373
- sig = signature(self.active_executor) # type: ignore
371
+ sig = inspect.signature(self.active_executor) # type: ignore
374
372
  return "session_state" in sig.parameters
375
373
  except Exception:
376
374
  return False
@@ -449,12 +447,10 @@ class Step:
449
447
  if self._executor_type == "function":
450
448
  log_debug(f"Executing function executor for step: {self.name}")
451
449
 
452
- if inspect.iscoroutinefunction(self.active_executor) or inspect.isasyncgenfunction(
453
- self.active_executor
454
- ):
450
+ if _is_async_callable(self.active_executor) or _is_async_generator_function(self.active_executor):
455
451
  raise ValueError("Cannot use async function with synchronous execution")
456
452
 
457
- if inspect.isgeneratorfunction(self.active_executor):
453
+ if _is_generator_function(self.active_executor):
458
454
  log_debug("Function returned iterable, streaming events")
459
455
  content = ""
460
456
  try:
@@ -652,17 +648,17 @@ class Step:
652
648
  for attempt in range(self.max_retries + 1):
653
649
  try:
654
650
  if self._executor_type == "function":
655
- import inspect
656
-
657
- if inspect.isgeneratorfunction(self.active_executor) or inspect.isasyncgenfunction(
651
+ if _is_generator_function(self.active_executor) or _is_async_generator_function(
658
652
  self.active_executor
659
653
  ):
660
654
  content = ""
661
655
  final_response = None
662
656
  try:
663
- if inspect.isgeneratorfunction(self.active_executor):
657
+ if _is_generator_function(self.active_executor):
664
658
  iterator = self._call_custom_function(
665
- self.active_executor, step_input, session_state_copy
659
+ self.active_executor,
660
+ step_input,
661
+ session_state_copy, # type: ignore[arg-type]
666
662
  ) # type: ignore
667
663
  for chunk in iterator: # type: ignore
668
664
  if (
@@ -676,9 +672,11 @@ class Step:
676
672
  if isinstance(chunk, StepOutput):
677
673
  final_response = chunk
678
674
  else:
679
- if inspect.isasyncgenfunction(self.active_executor):
675
+ if _is_async_generator_function(self.active_executor):
680
676
  iterator = await self._acall_custom_function(
681
- self.active_executor, step_input, session_state_copy
677
+ self.active_executor,
678
+ step_input,
679
+ session_state_copy, # type: ignore[arg-type]
682
680
  ) # type: ignore
683
681
  async for chunk in iterator: # type: ignore
684
682
  if (
@@ -705,7 +703,7 @@ class Step:
705
703
  else:
706
704
  response = StepOutput(content=content)
707
705
  else:
708
- if inspect.iscoroutinefunction(self.active_executor):
706
+ if _is_async_callable(self.active_executor):
709
707
  result = await self._acall_custom_function(
710
708
  self.active_executor, step_input, session_state_copy
711
709
  ) # type: ignore
@@ -854,14 +852,15 @@ class Step:
854
852
 
855
853
  if self._executor_type == "function":
856
854
  log_debug(f"Executing async function executor for step: {self.name}")
857
- import inspect
858
855
 
859
856
  # Check if the function is an async generator
860
- if inspect.isasyncgenfunction(self.active_executor):
857
+ if _is_async_generator_function(self.active_executor):
861
858
  content = ""
862
859
  # It's an async generator - iterate over it
863
860
  iterator = await self._acall_custom_function(
864
- self.active_executor, step_input, session_state_copy
861
+ self.active_executor,
862
+ step_input,
863
+ session_state_copy, # type: ignore[arg-type]
865
864
  ) # type: ignore
866
865
  async for event in iterator: # type: ignore
867
866
  if (
@@ -885,14 +884,14 @@ class Step:
885
884
  yield enriched_event # type: ignore[misc]
886
885
  if not final_response:
887
886
  final_response = StepOutput(content=content)
888
- elif inspect.iscoroutinefunction(self.active_executor):
887
+ elif _is_async_callable(self.active_executor):
889
888
  # It's a regular async function - await it
890
889
  result = await self._acall_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
891
890
  if isinstance(result, StepOutput):
892
891
  final_response = result
893
892
  else:
894
893
  final_response = StepOutput(content=str(result))
895
- elif inspect.isgeneratorfunction(self.active_executor):
894
+ elif _is_generator_function(self.active_executor):
896
895
  content = ""
897
896
  # It's a regular generator function - iterate over it
898
897
  iterator = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
@@ -1252,3 +1251,28 @@ class Step:
1252
1251
  continue
1253
1252
 
1254
1253
  return videos
1254
+
1255
+
1256
+ def _is_async_callable(obj: Any) -> TypeGuard[Callable[..., Any]]:
1257
+ """Checks if obj is an async callable (coroutine function or callable with async __call__)"""
1258
+ return inspect.iscoroutinefunction(obj) or (callable(obj) and inspect.iscoroutinefunction(obj.__call__))
1259
+
1260
+
1261
+ def _is_generator_function(obj: Any) -> TypeGuard[Callable[..., Any]]:
1262
+ """Checks if obj is a generator function, including callable class instances with generator __call__ methods"""
1263
+ if inspect.isgeneratorfunction(obj):
1264
+ return True
1265
+ # Check if it's a callable class instance with a generator __call__ method
1266
+ if callable(obj) and hasattr(obj, "__call__"):
1267
+ return inspect.isgeneratorfunction(obj.__call__)
1268
+ return False
1269
+
1270
+
1271
+ def _is_async_generator_function(obj: Any) -> TypeGuard[Callable[..., Any]]:
1272
+ """Checks if obj is an async generator function, including callable class instances"""
1273
+ if inspect.isasyncgenfunction(obj):
1274
+ return True
1275
+ # Check if it's a callable class instance with an async generator __call__ method
1276
+ if callable(obj) and hasattr(obj, "__call__"):
1277
+ return inspect.isasyncgenfunction(obj.__call__)
1278
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agno
3
- Version: 2.1.7
3
+ Version: 2.1.8
4
4
  Summary: Agno: a lightweight library for building Multi-Agent Systems
5
5
  Author-email: Ashpreet Bedi <ashpreet@agno.com>
6
6
  Project-URL: homepage, https://agno.com