agno 2.1.10__py3-none-any.whl → 2.2.1__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 (45) hide show
  1. agno/agent/agent.py +1594 -1248
  2. agno/knowledge/knowledge.py +11 -0
  3. agno/knowledge/reader/pptx_reader.py +101 -0
  4. agno/knowledge/reader/reader_factory.py +14 -0
  5. agno/knowledge/types.py +1 -0
  6. agno/models/anthropic/claude.py +2 -2
  7. agno/models/base.py +4 -4
  8. agno/models/ollama/chat.py +7 -2
  9. agno/os/app.py +1 -1
  10. agno/os/interfaces/a2a/router.py +2 -2
  11. agno/os/interfaces/agui/router.py +2 -2
  12. agno/os/router.py +7 -7
  13. agno/os/routers/evals/schemas.py +31 -31
  14. agno/os/routers/health.py +6 -2
  15. agno/os/routers/knowledge/schemas.py +49 -47
  16. agno/os/routers/memory/schemas.py +16 -16
  17. agno/os/routers/metrics/schemas.py +16 -16
  18. agno/os/routers/session/session.py +382 -7
  19. agno/os/schema.py +254 -231
  20. agno/os/utils.py +1 -1
  21. agno/run/agent.py +54 -1
  22. agno/run/team.py +48 -0
  23. agno/run/workflow.py +15 -5
  24. agno/session/summary.py +45 -13
  25. agno/session/team.py +90 -5
  26. agno/team/team.py +1130 -849
  27. agno/utils/agent.py +372 -0
  28. agno/utils/events.py +144 -2
  29. agno/utils/message.py +60 -0
  30. agno/utils/print_response/agent.py +10 -6
  31. agno/utils/print_response/team.py +6 -4
  32. agno/utils/print_response/workflow.py +7 -5
  33. agno/utils/team.py +9 -8
  34. agno/workflow/condition.py +17 -9
  35. agno/workflow/loop.py +18 -10
  36. agno/workflow/parallel.py +14 -6
  37. agno/workflow/router.py +16 -8
  38. agno/workflow/step.py +14 -6
  39. agno/workflow/steps.py +14 -6
  40. agno/workflow/workflow.py +331 -123
  41. {agno-2.1.10.dist-info → agno-2.2.1.dist-info}/METADATA +63 -23
  42. {agno-2.1.10.dist-info → agno-2.2.1.dist-info}/RECORD +45 -43
  43. {agno-2.1.10.dist-info → agno-2.2.1.dist-info}/WHEEL +0 -0
  44. {agno-2.1.10.dist-info → agno-2.2.1.dist-info}/licenses/LICENSE +0 -0
  45. {agno-2.1.10.dist-info → agno-2.2.1.dist-info}/top_level.txt +0 -0
agno/utils/agent.py ADDED
@@ -0,0 +1,372 @@
1
+ from asyncio import Future, Task
2
+ from typing import AsyncIterator, Iterator, List, Optional, Sequence, Union
3
+
4
+ from agno.media import Audio, File, Image, Video
5
+ from agno.models.message import Message
6
+ from agno.run.agent import RunEvent, RunInput, RunOutput, RunOutputEvent
7
+ from agno.run.team import RunOutputEvent as TeamRunOutputEvent
8
+ from agno.run.team import TeamRunOutput
9
+ from agno.session import AgentSession, TeamSession
10
+ from agno.utils.events import (
11
+ create_memory_update_completed_event,
12
+ create_memory_update_started_event,
13
+ create_team_memory_update_completed_event,
14
+ create_team_memory_update_started_event,
15
+ handle_event,
16
+ )
17
+ from agno.utils.log import log_debug, log_warning
18
+
19
+
20
+ async def await_for_background_tasks(
21
+ memory_task: Optional[Task] = None,
22
+ cultural_knowledge_task: Optional[Task] = None,
23
+ ) -> None:
24
+ if memory_task is not None:
25
+ try:
26
+ await memory_task
27
+ except Exception as e:
28
+ log_warning(f"Error in memory creation: {str(e)}")
29
+
30
+ if cultural_knowledge_task is not None:
31
+ try:
32
+ await cultural_knowledge_task
33
+ except Exception as e:
34
+ log_warning(f"Error in cultural knowledge creation: {str(e)}")
35
+
36
+
37
+ def wait_for_background_tasks(
38
+ memory_future: Optional[Future] = None, cultural_knowledge_future: Optional[Future] = None
39
+ ) -> None:
40
+ if memory_future is not None:
41
+ try:
42
+ memory_future.result()
43
+ except Exception as e:
44
+ log_warning(f"Error in memory creation: {str(e)}")
45
+
46
+ # Wait for cultural knowledge creation
47
+ if cultural_knowledge_future is not None:
48
+ try:
49
+ cultural_knowledge_future.result()
50
+ except Exception as e:
51
+ log_warning(f"Error in cultural knowledge creation: {str(e)}")
52
+
53
+
54
+ async def await_for_background_tasks_stream(
55
+ run_response: Union[RunOutput, TeamRunOutput],
56
+ memory_task: Optional[Task] = None,
57
+ cultural_knowledge_task: Optional[Task] = None,
58
+ stream_events: bool = False,
59
+ events_to_skip: Optional[List[RunEvent]] = None,
60
+ store_events: bool = False,
61
+ ) -> AsyncIterator[RunOutputEvent]:
62
+ if memory_task is not None:
63
+ if stream_events:
64
+ if isinstance(run_response, TeamRunOutput):
65
+ yield handle_event( # type: ignore
66
+ create_team_memory_update_started_event(from_run_response=run_response),
67
+ run_response,
68
+ events_to_skip=events_to_skip, # type: ignore
69
+ store_events=store_events,
70
+ )
71
+ else:
72
+ yield handle_event( # type: ignore
73
+ create_memory_update_started_event(from_run_response=run_response),
74
+ run_response,
75
+ events_to_skip=events_to_skip, # type: ignore
76
+ store_events=store_events,
77
+ )
78
+ try:
79
+ await memory_task
80
+ except Exception as e:
81
+ log_warning(f"Error in memory creation: {str(e)}")
82
+ if stream_events:
83
+ if isinstance(run_response, TeamRunOutput):
84
+ yield handle_event( # type: ignore
85
+ create_team_memory_update_completed_event(from_run_response=run_response),
86
+ run_response,
87
+ events_to_skip=events_to_skip, # type: ignore
88
+ store_events=store_events,
89
+ )
90
+ else:
91
+ yield handle_event( # type: ignore
92
+ create_memory_update_completed_event(from_run_response=run_response),
93
+ run_response,
94
+ events_to_skip=events_to_skip, # type: ignore
95
+ store_events=store_events,
96
+ )
97
+
98
+ if cultural_knowledge_task is not None:
99
+ try:
100
+ await cultural_knowledge_task
101
+ except Exception as e:
102
+ log_warning(f"Error in cultural knowledge creation: {str(e)}")
103
+
104
+
105
+ def wait_for_background_tasks_stream(
106
+ run_response: Union[TeamRunOutput, RunOutput],
107
+ memory_future: Optional[Future] = None,
108
+ cultural_knowledge_future: Optional[Future] = None,
109
+ stream_events: bool = False,
110
+ events_to_skip: Optional[List[RunEvent]] = None,
111
+ store_events: bool = False,
112
+ ) -> Iterator[Union[RunOutputEvent, TeamRunOutputEvent]]:
113
+ if memory_future is not None:
114
+ if stream_events:
115
+ if isinstance(run_response, TeamRunOutput):
116
+ yield handle_event( # type: ignore
117
+ create_team_memory_update_started_event(from_run_response=run_response),
118
+ run_response,
119
+ events_to_skip=events_to_skip, # type: ignore
120
+ store_events=store_events,
121
+ )
122
+ else:
123
+ yield handle_event( # type: ignore
124
+ create_memory_update_started_event(from_run_response=run_response),
125
+ run_response,
126
+ events_to_skip=events_to_skip, # type: ignore
127
+ store_events=store_events,
128
+ )
129
+ try:
130
+ memory_future.result()
131
+ except Exception as e:
132
+ log_warning(f"Error in memory creation: {str(e)}")
133
+ if stream_events:
134
+ if isinstance(run_response, TeamRunOutput):
135
+ yield handle_event( # type: ignore
136
+ create_team_memory_update_completed_event(from_run_response=run_response),
137
+ run_response,
138
+ events_to_skip=events_to_skip, # type: ignore
139
+ store_events=store_events,
140
+ )
141
+ else:
142
+ yield handle_event( # type: ignore
143
+ create_memory_update_completed_event(from_run_response=run_response),
144
+ run_response,
145
+ events_to_skip=events_to_skip, # type: ignore
146
+ store_events=store_events,
147
+ )
148
+
149
+ # Wait for cultural knowledge creation
150
+ if cultural_knowledge_future is not None:
151
+ # TODO: Add events
152
+ try:
153
+ cultural_knowledge_future.result()
154
+ except Exception as e:
155
+ log_warning(f"Error in cultural knowledge creation: {str(e)}")
156
+
157
+
158
+ def collect_joint_images(
159
+ run_input: Optional[RunInput] = None,
160
+ session: Optional[Union[AgentSession, TeamSession]] = None,
161
+ ) -> Optional[Sequence[Image]]:
162
+ """Collect images from input, session history, and current run response."""
163
+ joint_images: List[Image] = []
164
+
165
+ # 1. Add images from current input
166
+ if run_input and run_input.images:
167
+ joint_images.extend(run_input.images)
168
+ log_debug(f"Added {len(run_input.images)} input images to joint list")
169
+
170
+ # 2. Add images from session history (from both input and generated sources)
171
+ try:
172
+ if session and session.runs:
173
+ for historical_run in session.runs:
174
+ # Add generated images from previous runs
175
+ if historical_run.images:
176
+ joint_images.extend(historical_run.images)
177
+ log_debug(
178
+ f"Added {len(historical_run.images)} generated images from historical run {historical_run.run_id}"
179
+ )
180
+
181
+ # Add input images from previous runs
182
+ if historical_run.input and historical_run.input.images:
183
+ joint_images.extend(historical_run.input.images)
184
+ log_debug(
185
+ f"Added {len(historical_run.input.images)} input images from historical run {historical_run.run_id}"
186
+ )
187
+ except Exception as e:
188
+ log_debug(f"Could not access session history for images: {e}")
189
+
190
+ if joint_images:
191
+ log_debug(f"Images Available to Model: {len(joint_images)} images")
192
+ return joint_images if joint_images else None
193
+
194
+
195
+ def collect_joint_videos(
196
+ run_input: Optional[RunInput] = None,
197
+ session: Optional[Union[AgentSession, TeamSession]] = None,
198
+ ) -> Optional[Sequence[Video]]:
199
+ """Collect videos from input, session history, and current run response."""
200
+ joint_videos: List[Video] = []
201
+
202
+ # 1. Add videos from current input
203
+ if run_input and run_input.videos:
204
+ joint_videos.extend(run_input.videos)
205
+ log_debug(f"Added {len(run_input.videos)} input videos to joint list")
206
+
207
+ # 2. Add videos from session history (from both input and generated sources)
208
+ try:
209
+ if session and session.runs:
210
+ for historical_run in session.runs:
211
+ # Add generated videos from previous runs
212
+ if historical_run.videos:
213
+ joint_videos.extend(historical_run.videos)
214
+ log_debug(
215
+ f"Added {len(historical_run.videos)} generated videos from historical run {historical_run.run_id}"
216
+ )
217
+
218
+ # Add input videos from previous runs
219
+ if historical_run.input and historical_run.input.videos:
220
+ joint_videos.extend(historical_run.input.videos)
221
+ log_debug(
222
+ f"Added {len(historical_run.input.videos)} input videos from historical run {historical_run.run_id}"
223
+ )
224
+ except Exception as e:
225
+ log_debug(f"Could not access session history for videos: {e}")
226
+
227
+ if joint_videos:
228
+ log_debug(f"Videos Available to Model: {len(joint_videos)} videos")
229
+ return joint_videos if joint_videos else None
230
+
231
+
232
+ def collect_joint_audios(
233
+ run_input: Optional[RunInput] = None,
234
+ session: Optional[Union[AgentSession, TeamSession]] = None,
235
+ ) -> Optional[Sequence[Audio]]:
236
+ """Collect audios from input, session history, and current run response."""
237
+ joint_audios: List[Audio] = []
238
+
239
+ # 1. Add audios from current input
240
+ if run_input and run_input.audios:
241
+ joint_audios.extend(run_input.audios)
242
+ log_debug(f"Added {len(run_input.audios)} input audios to joint list")
243
+
244
+ # 2. Add audios from session history (from both input and generated sources)
245
+ try:
246
+ if session and session.runs:
247
+ for historical_run in session.runs:
248
+ # Add generated audios from previous runs
249
+ if historical_run.audio:
250
+ joint_audios.extend(historical_run.audio)
251
+ log_debug(
252
+ f"Added {len(historical_run.audio)} generated audios from historical run {historical_run.run_id}"
253
+ )
254
+
255
+ # Add input audios from previous runs
256
+ if historical_run.input and historical_run.input.audios:
257
+ joint_audios.extend(historical_run.input.audios)
258
+ log_debug(
259
+ f"Added {len(historical_run.input.audios)} input audios from historical run {historical_run.run_id}"
260
+ )
261
+ except Exception as e:
262
+ log_debug(f"Could not access session history for audios: {e}")
263
+
264
+ if joint_audios:
265
+ log_debug(f"Audios Available to Model: {len(joint_audios)} audios")
266
+ return joint_audios if joint_audios else None
267
+
268
+
269
+ def collect_joint_files(
270
+ run_input: Optional[RunInput] = None,
271
+ ) -> Optional[Sequence[File]]:
272
+ """Collect files from input and session history."""
273
+ from agno.utils.log import log_debug
274
+
275
+ joint_files: List[File] = []
276
+
277
+ # 1. Add files from current input
278
+ if run_input and run_input.files:
279
+ joint_files.extend(run_input.files)
280
+
281
+ # TODO: Files aren't stored in session history yet and dont have a FileArtifact
282
+
283
+ if joint_files:
284
+ log_debug(f"Files Available to Model: {len(joint_files)} files")
285
+
286
+ return joint_files if joint_files else None
287
+
288
+
289
+ def scrub_media_from_run_output(run_response: Union[RunOutput, TeamRunOutput]) -> None:
290
+ """
291
+ Completely remove all media from RunOutput when store_media=False.
292
+ This includes media in input, output artifacts, and all messages.
293
+ """
294
+ # 1. Scrub RunInput media
295
+ if run_response.input is not None:
296
+ run_response.input.images = []
297
+ run_response.input.videos = []
298
+ run_response.input.audios = []
299
+ run_response.input.files = []
300
+
301
+ # 3. Scrub media from all messages
302
+ if run_response.messages:
303
+ for message in run_response.messages:
304
+ scrub_media_from_message(message)
305
+
306
+ # 4. Scrub media from additional_input messages if any
307
+ if run_response.additional_input:
308
+ for message in run_response.additional_input:
309
+ scrub_media_from_message(message)
310
+
311
+ # 5. Scrub media from reasoning_messages if any
312
+ if run_response.reasoning_messages:
313
+ for message in run_response.reasoning_messages:
314
+ scrub_media_from_message(message)
315
+
316
+
317
+ def scrub_media_from_message(message: Message) -> None:
318
+ """Remove all media from a Message object."""
319
+ # Input media
320
+ message.images = None
321
+ message.videos = None
322
+ message.audio = None
323
+ message.files = None
324
+
325
+ # Output media
326
+ message.audio_output = None
327
+ message.image_output = None
328
+ message.video_output = None
329
+
330
+
331
+ def scrub_tool_results_from_run_output(run_response: Union[RunOutput, TeamRunOutput]) -> None:
332
+ """
333
+ Remove all tool-related data from RunOutput when store_tool_messages=False.
334
+ This removes both the tool call and its corresponding result to maintain API consistency.
335
+ """
336
+ if not run_response.messages:
337
+ return
338
+
339
+ # Step 1: Collect all tool_call_ids from tool result messages
340
+ tool_call_ids_to_remove = set()
341
+ for message in run_response.messages:
342
+ if message.role == "tool" and message.tool_call_id:
343
+ tool_call_ids_to_remove.add(message.tool_call_id)
344
+
345
+ # Step 2: Remove tool result messages (role="tool")
346
+ run_response.messages = [msg for msg in run_response.messages if msg.role != "tool"]
347
+
348
+ # Step 3: Remove assistant messages that made those tool calls
349
+ filtered_messages = []
350
+ for message in run_response.messages:
351
+ # Check if this assistant message made any of the tool calls we're removing
352
+ should_remove = False
353
+ if message.role == "assistant" and message.tool_calls:
354
+ for tool_call in message.tool_calls:
355
+ if tool_call.get("id") in tool_call_ids_to_remove:
356
+ should_remove = True
357
+ break
358
+
359
+ if not should_remove:
360
+ filtered_messages.append(message)
361
+
362
+ run_response.messages = filtered_messages
363
+
364
+
365
+ def scrub_history_messages_from_run_output(run_response: Union[RunOutput, TeamRunOutput]) -> None:
366
+ """
367
+ Remove all history messages from TeamRunOutput when store_history_messages=False.
368
+ This removes messages that were loaded from the team's memory.
369
+ """
370
+ # Remove messages with from_history=True
371
+ if run_response.messages:
372
+ run_response.messages = [msg for msg in run_response.messages if not msg.from_history]
agno/utils/events.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, List, Optional
1
+ from typing import Any, Dict, List, Optional, Union
2
2
 
3
3
  from agno.media import Audio, Image
4
4
  from agno.models.message import Citations
@@ -11,6 +11,8 @@ from agno.run.agent import (
11
11
  OutputModelResponseStartedEvent,
12
12
  ParserModelResponseCompletedEvent,
13
13
  ParserModelResponseStartedEvent,
14
+ PostHookCompletedEvent,
15
+ PostHookStartedEvent,
14
16
  PreHookCompletedEvent,
15
17
  PreHookStartedEvent,
16
18
  ReasoningCompletedEvent,
@@ -18,13 +20,18 @@ from agno.run.agent import (
18
20
  ReasoningStepEvent,
19
21
  RunCancelledEvent,
20
22
  RunCompletedEvent,
23
+ RunContentCompletedEvent,
21
24
  RunContentEvent,
22
25
  RunContinuedEvent,
23
26
  RunErrorEvent,
27
+ RunEvent,
24
28
  RunInput,
25
29
  RunOutput,
30
+ RunOutputEvent,
26
31
  RunPausedEvent,
27
32
  RunStartedEvent,
33
+ SessionSummaryCompletedEvent,
34
+ SessionSummaryStartedEvent,
28
35
  ToolCallCompletedEvent,
29
36
  ToolCallStartedEvent,
30
37
  )
@@ -34,6 +41,8 @@ from agno.run.team import OutputModelResponseCompletedEvent as TeamOutputModelRe
34
41
  from agno.run.team import OutputModelResponseStartedEvent as TeamOutputModelResponseStartedEvent
35
42
  from agno.run.team import ParserModelResponseCompletedEvent as TeamParserModelResponseCompletedEvent
36
43
  from agno.run.team import ParserModelResponseStartedEvent as TeamParserModelResponseStartedEvent
44
+ from agno.run.team import PostHookCompletedEvent as TeamPostHookCompletedEvent
45
+ from agno.run.team import PostHookStartedEvent as TeamPostHookStartedEvent
37
46
  from agno.run.team import PreHookCompletedEvent as TeamPreHookCompletedEvent
38
47
  from agno.run.team import PreHookStartedEvent as TeamPreHookStartedEvent
39
48
  from agno.run.team import ReasoningCompletedEvent as TeamReasoningCompletedEvent
@@ -41,12 +50,16 @@ from agno.run.team import ReasoningStartedEvent as TeamReasoningStartedEvent
41
50
  from agno.run.team import ReasoningStepEvent as TeamReasoningStepEvent
42
51
  from agno.run.team import RunCancelledEvent as TeamRunCancelledEvent
43
52
  from agno.run.team import RunCompletedEvent as TeamRunCompletedEvent
53
+ from agno.run.team import RunContentCompletedEvent as TeamRunContentCompletedEvent
44
54
  from agno.run.team import RunContentEvent as TeamRunContentEvent
45
55
  from agno.run.team import RunErrorEvent as TeamRunErrorEvent
46
56
  from agno.run.team import RunStartedEvent as TeamRunStartedEvent
47
- from agno.run.team import TeamRunInput, TeamRunOutput
57
+ from agno.run.team import SessionSummaryCompletedEvent as TeamSessionSummaryCompletedEvent
58
+ from agno.run.team import SessionSummaryStartedEvent as TeamSessionSummaryStartedEvent
59
+ from agno.run.team import TeamRunEvent, TeamRunInput, TeamRunOutput, TeamRunOutputEvent
48
60
  from agno.run.team import ToolCallCompletedEvent as TeamToolCallCompletedEvent
49
61
  from agno.run.team import ToolCallStartedEvent as TeamToolCallStartedEvent
62
+ from agno.session.summary import SessionSummary
50
63
 
51
64
 
52
65
  def create_team_run_started_event(from_run_response: TeamRunOutput) -> TeamRunStartedEvent:
@@ -242,6 +255,54 @@ def create_team_pre_hook_completed_event(
242
255
  )
243
256
 
244
257
 
258
+ def create_post_hook_started_event(
259
+ from_run_response: RunOutput, post_hook_name: Optional[str] = None
260
+ ) -> PostHookStartedEvent:
261
+ return PostHookStartedEvent(
262
+ session_id=from_run_response.session_id,
263
+ agent_id=from_run_response.agent_id, # type: ignore
264
+ agent_name=from_run_response.agent_name, # type: ignore
265
+ run_id=from_run_response.run_id,
266
+ post_hook_name=post_hook_name,
267
+ )
268
+
269
+
270
+ def create_team_post_hook_started_event(
271
+ from_run_response: TeamRunOutput, post_hook_name: Optional[str] = None
272
+ ) -> TeamPostHookStartedEvent:
273
+ return TeamPostHookStartedEvent(
274
+ session_id=from_run_response.session_id,
275
+ team_id=from_run_response.team_id, # type: ignore
276
+ team_name=from_run_response.team_name, # type: ignore
277
+ run_id=from_run_response.run_id,
278
+ post_hook_name=post_hook_name,
279
+ )
280
+
281
+
282
+ def create_post_hook_completed_event(
283
+ from_run_response: RunOutput, post_hook_name: Optional[str] = None
284
+ ) -> PostHookCompletedEvent:
285
+ return PostHookCompletedEvent(
286
+ session_id=from_run_response.session_id,
287
+ agent_id=from_run_response.agent_id, # type: ignore
288
+ agent_name=from_run_response.agent_name, # type: ignore
289
+ run_id=from_run_response.run_id,
290
+ post_hook_name=post_hook_name,
291
+ )
292
+
293
+
294
+ def create_team_post_hook_completed_event(
295
+ from_run_response: TeamRunOutput, post_hook_name: Optional[str] = None
296
+ ) -> TeamPostHookCompletedEvent:
297
+ return TeamPostHookCompletedEvent(
298
+ session_id=from_run_response.session_id,
299
+ team_id=from_run_response.team_id, # type: ignore
300
+ team_name=from_run_response.team_name, # type: ignore
301
+ run_id=from_run_response.run_id,
302
+ post_hook_name=post_hook_name,
303
+ )
304
+
305
+
245
306
  def create_memory_update_started_event(from_run_response: RunOutput) -> MemoryUpdateStartedEvent:
246
307
  return MemoryUpdateStartedEvent(
247
308
  session_id=from_run_response.session_id,
@@ -278,6 +339,50 @@ def create_team_memory_update_completed_event(from_run_response: TeamRunOutput)
278
339
  )
279
340
 
280
341
 
342
+ def create_team_session_summary_started_event(
343
+ from_run_response: TeamRunOutput,
344
+ ) -> TeamSessionSummaryStartedEvent:
345
+ return TeamSessionSummaryStartedEvent(
346
+ session_id=from_run_response.session_id,
347
+ team_id=from_run_response.team_id, # type: ignore
348
+ team_name=from_run_response.team_name, # type: ignore
349
+ run_id=from_run_response.run_id,
350
+ )
351
+
352
+
353
+ def create_team_session_summary_completed_event(
354
+ from_run_response: TeamRunOutput, session_summary: Optional[SessionSummary] = None
355
+ ) -> TeamSessionSummaryCompletedEvent:
356
+ return TeamSessionSummaryCompletedEvent(
357
+ session_id=from_run_response.session_id,
358
+ team_id=from_run_response.team_id, # type: ignore
359
+ team_name=from_run_response.team_name, # type: ignore
360
+ run_id=from_run_response.run_id,
361
+ session_summary=session_summary,
362
+ )
363
+
364
+
365
+ def create_session_summary_started_event(from_run_response: RunOutput) -> SessionSummaryStartedEvent:
366
+ return SessionSummaryStartedEvent(
367
+ session_id=from_run_response.session_id,
368
+ agent_id=from_run_response.agent_id, # type: ignore
369
+ agent_name=from_run_response.agent_name, # type: ignore
370
+ run_id=from_run_response.run_id,
371
+ )
372
+
373
+
374
+ def create_session_summary_completed_event(
375
+ from_run_response: RunOutput, session_summary: Optional[SessionSummary] = None
376
+ ) -> SessionSummaryCompletedEvent:
377
+ return SessionSummaryCompletedEvent(
378
+ session_id=from_run_response.session_id,
379
+ agent_id=from_run_response.agent_id, # type: ignore
380
+ agent_name=from_run_response.agent_name, # type: ignore
381
+ run_id=from_run_response.run_id,
382
+ session_summary=session_summary,
383
+ )
384
+
385
+
281
386
  def create_reasoning_started_event(from_run_response: RunOutput) -> ReasoningStartedEvent:
282
387
  return ReasoningStartedEvent(
283
388
  session_id=from_run_response.session_id,
@@ -468,6 +573,28 @@ def create_team_run_output_content_event(
468
573
  )
469
574
 
470
575
 
576
+ def create_run_content_completed_event(
577
+ from_run_response: RunOutput,
578
+ ) -> RunContentCompletedEvent:
579
+ return RunContentCompletedEvent(
580
+ session_id=from_run_response.session_id,
581
+ agent_id=from_run_response.agent_id, # type: ignore
582
+ agent_name=from_run_response.agent_name, # type: ignore
583
+ run_id=from_run_response.run_id,
584
+ )
585
+
586
+
587
+ def create_team_run_content_completed_event(
588
+ from_run_response: TeamRunOutput,
589
+ ) -> TeamRunContentCompletedEvent:
590
+ return TeamRunContentCompletedEvent(
591
+ session_id=from_run_response.session_id,
592
+ team_id=from_run_response.team_id, # type: ignore
593
+ team_name=from_run_response.team_name, # type: ignore
594
+ run_id=from_run_response.run_id,
595
+ )
596
+
597
+
471
598
  def create_parser_model_response_started_event(
472
599
  from_run_response: RunOutput,
473
600
  ) -> ParserModelResponseStartedEvent:
@@ -550,3 +677,18 @@ def create_team_output_model_response_completed_event(
550
677
  team_name=from_run_response.team_name, # type: ignore
551
678
  run_id=from_run_response.run_id,
552
679
  )
680
+
681
+
682
+ def handle_event(
683
+ event: Union[RunOutputEvent, TeamRunOutputEvent],
684
+ run_response: Union[RunOutput, TeamRunOutput],
685
+ events_to_skip: Optional[List[Union[RunEvent, TeamRunEvent]]] = None,
686
+ store_events: bool = False,
687
+ ) -> Union[RunOutputEvent, TeamRunOutputEvent]:
688
+ # We only store events that are not run_response_content events
689
+ events_to_skip = [event.value for event in events_to_skip] if events_to_skip else []
690
+ if store_events and event.event not in events_to_skip:
691
+ if run_response.events is None:
692
+ run_response.events = []
693
+ run_response.events.append(event) # type: ignore
694
+ return event
agno/utils/message.py CHANGED
@@ -1,8 +1,68 @@
1
+ from copy import deepcopy
1
2
  from typing import Dict, List, Union
2
3
 
3
4
  from pydantic import BaseModel
4
5
 
5
6
  from agno.models.message import Message
7
+ from agno.utils.log import log_debug
8
+
9
+
10
+ def filter_tool_calls(messages: List[Message], max_tool_calls: int) -> None:
11
+ """
12
+ Filter messages (in-place) to keep only the most recent N tool calls.
13
+
14
+ Args:
15
+ messages: List of messages to filter (modified in-place)
16
+ max_tool_calls: Number of recent tool calls to keep
17
+ """
18
+ # Count total tool calls
19
+ tool_call_count = sum(1 for m in messages if m.role == "tool")
20
+
21
+ # No filtering needed
22
+ if tool_call_count <= max_tool_calls:
23
+ return
24
+
25
+ # Collect tool_call_ids to keep (most recent N)
26
+ tool_call_ids_list: List[str] = []
27
+ for msg in reversed(messages):
28
+ if msg.role == "tool" and len(tool_call_ids_list) < max_tool_calls:
29
+ if msg.tool_call_id:
30
+ tool_call_ids_list.append(msg.tool_call_id)
31
+
32
+ tool_call_ids_to_keep: set[str] = set(tool_call_ids_list)
33
+
34
+ # Filter messages in-place
35
+ filtered_messages = []
36
+ for msg in messages:
37
+ if msg.role == "tool":
38
+ # Keep only tool results in our window
39
+ if msg.tool_call_id in tool_call_ids_to_keep:
40
+ filtered_messages.append(msg)
41
+ elif msg.role == "assistant" and msg.tool_calls:
42
+ # Filter tool_calls within the assistant message
43
+ # Use deepcopy to ensure complete isolation of the filtered message
44
+ filtered_msg = deepcopy(msg)
45
+ # Filter tool_calls
46
+ if filtered_msg.tool_calls is not None:
47
+ filtered_msg.tool_calls = [
48
+ tc for tc in filtered_msg.tool_calls if tc.get("id") in tool_call_ids_to_keep
49
+ ]
50
+
51
+ if filtered_msg.tool_calls:
52
+ # Has tool_calls remaining, keep it
53
+ filtered_messages.append(filtered_msg)
54
+ # skip empty messages
55
+ elif filtered_msg.content:
56
+ filtered_msg.tool_calls = None
57
+ filtered_messages.append(filtered_msg)
58
+ else:
59
+ filtered_messages.append(msg)
60
+
61
+ messages[:] = filtered_messages
62
+
63
+ # Log filtering information
64
+ num_filtered = tool_call_count - len(tool_call_ids_to_keep)
65
+ log_debug(f"Filtered {num_filtered} tool calls, kept {len(tool_call_ids_to_keep)}")
6
66
 
7
67
 
8
68
  def get_text_from_message(message: Union[List, Dict, str, Message, BaseModel]) -> str: