agno 2.2.1__py3-none-any.whl → 2.2.3__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 (69) hide show
  1. agno/agent/agent.py +735 -574
  2. agno/culture/manager.py +22 -24
  3. agno/db/async_postgres/__init__.py +1 -1
  4. agno/db/dynamo/dynamo.py +0 -2
  5. agno/db/firestore/firestore.py +0 -2
  6. agno/db/gcs_json/gcs_json_db.py +0 -4
  7. agno/db/gcs_json/utils.py +0 -24
  8. agno/db/in_memory/in_memory_db.py +0 -3
  9. agno/db/json/json_db.py +4 -10
  10. agno/db/json/utils.py +0 -24
  11. agno/db/mongo/__init__.py +15 -1
  12. agno/db/mongo/async_mongo.py +1999 -0
  13. agno/db/mongo/mongo.py +0 -2
  14. agno/db/mysql/mysql.py +0 -3
  15. agno/db/postgres/__init__.py +1 -1
  16. agno/db/{async_postgres → postgres}/async_postgres.py +19 -22
  17. agno/db/postgres/postgres.py +7 -10
  18. agno/db/postgres/utils.py +106 -2
  19. agno/db/redis/redis.py +0 -2
  20. agno/db/singlestore/singlestore.py +0 -3
  21. agno/db/sqlite/__init__.py +2 -1
  22. agno/db/sqlite/async_sqlite.py +2269 -0
  23. agno/db/sqlite/sqlite.py +0 -2
  24. agno/db/sqlite/utils.py +96 -0
  25. agno/db/surrealdb/surrealdb.py +0 -6
  26. agno/knowledge/knowledge.py +3 -3
  27. agno/knowledge/reader/reader_factory.py +16 -0
  28. agno/knowledge/reader/tavily_reader.py +194 -0
  29. agno/memory/manager.py +28 -25
  30. agno/models/anthropic/claude.py +63 -6
  31. agno/models/base.py +251 -32
  32. agno/models/response.py +69 -0
  33. agno/os/router.py +7 -5
  34. agno/os/routers/memory/memory.py +2 -1
  35. agno/os/routers/memory/schemas.py +5 -2
  36. agno/os/schema.py +25 -20
  37. agno/os/utils.py +9 -2
  38. agno/run/agent.py +23 -30
  39. agno/run/base.py +17 -1
  40. agno/run/team.py +23 -29
  41. agno/run/workflow.py +17 -12
  42. agno/session/agent.py +3 -0
  43. agno/session/summary.py +4 -1
  44. agno/session/team.py +1 -1
  45. agno/team/team.py +599 -367
  46. agno/tools/dalle.py +2 -4
  47. agno/tools/eleven_labs.py +23 -25
  48. agno/tools/function.py +40 -0
  49. agno/tools/mcp/__init__.py +10 -0
  50. agno/tools/mcp/mcp.py +324 -0
  51. agno/tools/mcp/multi_mcp.py +347 -0
  52. agno/tools/mcp/params.py +24 -0
  53. agno/tools/slack.py +18 -3
  54. agno/tools/tavily.py +146 -0
  55. agno/utils/agent.py +366 -1
  56. agno/utils/mcp.py +92 -2
  57. agno/utils/media.py +166 -1
  58. agno/utils/print_response/workflow.py +17 -1
  59. agno/utils/team.py +89 -1
  60. agno/workflow/step.py +0 -1
  61. agno/workflow/types.py +10 -15
  62. {agno-2.2.1.dist-info → agno-2.2.3.dist-info}/METADATA +28 -25
  63. {agno-2.2.1.dist-info → agno-2.2.3.dist-info}/RECORD +66 -62
  64. agno/db/async_postgres/schemas.py +0 -139
  65. agno/db/async_postgres/utils.py +0 -347
  66. agno/tools/mcp.py +0 -679
  67. {agno-2.2.1.dist-info → agno-2.2.3.dist-info}/WHEEL +0 -0
  68. {agno-2.2.1.dist-info → agno-2.2.3.dist-info}/licenses/LICENSE +0 -0
  69. {agno-2.2.1.dist-info → agno-2.2.3.dist-info}/top_level.txt +0 -0
agno/models/base.py CHANGED
@@ -1,7 +1,11 @@
1
1
  import asyncio
2
2
  import collections.abc
3
+ import json
3
4
  from abc import ABC, abstractmethod
4
5
  from dataclasses import dataclass, field
6
+ from hashlib import md5
7
+ from pathlib import Path
8
+ from time import time
5
9
  from types import AsyncGeneratorType, GeneratorType
6
10
  from typing import (
7
11
  Any,
@@ -29,7 +33,7 @@ from agno.run.agent import CustomEvent, RunContentEvent, RunOutput, RunOutputEve
29
33
  from agno.run.team import RunContentEvent as TeamRunContentEvent
30
34
  from agno.run.team import TeamRunOutputEvent
31
35
  from agno.tools.function import Function, FunctionCall, FunctionExecutionResult, UserInputField
32
- from agno.utils.log import log_debug, log_error, log_warning
36
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
33
37
  from agno.utils.timer import Timer
34
38
  from agno.utils.tools import get_function_call_for_tool_call, get_function_call_for_tool_execution
35
39
 
@@ -133,6 +137,11 @@ class Model(ABC):
133
137
  # The role of the assistant message.
134
138
  assistant_message_role: str = "assistant"
135
139
 
140
+ # Cache model responses to avoid redundant API calls during development
141
+ cache_response: bool = False
142
+ cache_ttl: Optional[int] = None
143
+ cache_dir: Optional[str] = None
144
+
136
145
  def __post_init__(self):
137
146
  if self.provider is None and self.name is not None:
138
147
  self.provider = f"{self.name} ({self.id})"
@@ -145,6 +154,100 @@ class Model(ABC):
145
154
  def get_provider(self) -> str:
146
155
  return self.provider or self.name or self.__class__.__name__
147
156
 
157
+ def _get_model_cache_key(self, messages: List[Message], stream: bool, **kwargs: Any) -> str:
158
+ """Generate a cache key based on model messages and core parameters."""
159
+ message_data = []
160
+ for msg in messages:
161
+ msg_dict = {
162
+ "role": msg.role,
163
+ "content": msg.content,
164
+ }
165
+ message_data.append(msg_dict)
166
+
167
+ # Include tools parameter in cache key
168
+ has_tools = bool(kwargs.get("tools"))
169
+
170
+ cache_data = {
171
+ "model_id": self.id,
172
+ "messages": message_data,
173
+ "has_tools": has_tools,
174
+ "response_format": kwargs.get("response_format"),
175
+ "stream": stream,
176
+ }
177
+
178
+ cache_str = json.dumps(cache_data, sort_keys=True)
179
+ return md5(cache_str.encode()).hexdigest()
180
+
181
+ def _get_model_cache_file_path(self, cache_key: str) -> Path:
182
+ """Get the file path for a cache key."""
183
+ if self.cache_dir:
184
+ cache_dir = Path(self.cache_dir)
185
+ else:
186
+ cache_dir = Path.home() / ".agno" / "cache" / "model_responses"
187
+
188
+ cache_dir.mkdir(parents=True, exist_ok=True)
189
+ return cache_dir / f"{cache_key}.json"
190
+
191
+ def _get_cached_model_response(self, cache_key: str) -> Optional[Dict[str, Any]]:
192
+ """Retrieve a cached response if it exists and is not expired."""
193
+ cache_file = self._get_model_cache_file_path(cache_key)
194
+
195
+ if not cache_file.exists():
196
+ return None
197
+
198
+ try:
199
+ with open(cache_file, "r") as f:
200
+ cached_data = json.load(f)
201
+
202
+ # Check TTL if set (None means no expiration)
203
+ if self.cache_ttl is not None:
204
+ if time() - cached_data["timestamp"] > self.cache_ttl:
205
+ return None
206
+
207
+ return cached_data
208
+ except Exception:
209
+ return None
210
+
211
+ def _save_model_response_to_cache(self, cache_key: str, result: ModelResponse, is_streaming: bool = False) -> None:
212
+ """Save a model response to cache."""
213
+ try:
214
+ cache_file = self._get_model_cache_file_path(cache_key)
215
+
216
+ cache_data = {
217
+ "timestamp": int(time()),
218
+ "is_streaming": is_streaming,
219
+ "result": result.to_dict(),
220
+ }
221
+ with open(cache_file, "w") as f:
222
+ json.dump(cache_data, f)
223
+ except Exception:
224
+ pass
225
+
226
+ def _save_streaming_responses_to_cache(self, cache_key: str, responses: List[ModelResponse]) -> None:
227
+ """Save streaming responses to cache."""
228
+ cache_file = self._get_model_cache_file_path(cache_key)
229
+
230
+ cache_data = {
231
+ "timestamp": int(time()),
232
+ "is_streaming": True,
233
+ "streaming_responses": [r.to_dict() for r in responses],
234
+ }
235
+
236
+ try:
237
+ with open(cache_file, "w") as f:
238
+ json.dump(cache_data, f)
239
+ except Exception:
240
+ pass
241
+
242
+ def _model_response_from_cache(self, cached_data: Dict[str, Any]) -> ModelResponse:
243
+ """Reconstruct a ModelResponse from cached data."""
244
+ return ModelResponse.from_dict(cached_data["result"])
245
+
246
+ def _streaming_responses_from_cache(self, cached_data: list) -> Iterator[ModelResponse]:
247
+ """Reconstruct streaming responses from cached data."""
248
+ for cached_response in cached_data:
249
+ yield ModelResponse.from_dict(cached_response)
250
+
148
251
  @abstractmethod
149
252
  def invoke(self, *args, **kwargs) -> ModelResponse:
150
253
  pass
@@ -187,12 +290,21 @@ class Model(ABC):
187
290
  """
188
291
  pass
189
292
 
293
+ def _format_tools(self, tools: Optional[List[Union[Function, dict]]]) -> List[Dict[str, Any]]:
294
+ _tool_dicts = []
295
+ for tool in tools or []:
296
+ if isinstance(tool, Function):
297
+ _tool_dicts.append({"type": "function", "function": tool.to_dict()})
298
+ else:
299
+ # If a dict is passed, it is a builtin tool
300
+ _tool_dicts.append(tool)
301
+ return _tool_dicts
302
+
190
303
  def response(
191
304
  self,
192
305
  messages: List[Message],
193
306
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
194
- tools: Optional[List[Dict[str, Any]]] = None,
195
- functions: Optional[Dict[str, Function]] = None,
307
+ tools: Optional[List[Union[Function, dict]]] = None,
196
308
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
197
309
  tool_call_limit: Optional[int] = None,
198
310
  run_response: Optional[RunOutput] = None,
@@ -200,8 +312,26 @@ class Model(ABC):
200
312
  ) -> ModelResponse:
201
313
  """
202
314
  Generate a response from the model.
315
+
316
+ Args:
317
+ messages: List of messages to send to the model
318
+ response_format: Response format to use
319
+ tools: List of tools to use. This includes the original Function objects and dicts for built-in tools.
320
+ tool_choice: Tool choice to use
321
+ tool_call_limit: Tool call limit
322
+ run_response: Run response to use
323
+ send_media_to_model: Whether to send media to the model
203
324
  """
204
325
 
326
+ # Check cache if enabled
327
+ if self.cache_response:
328
+ cache_key = self._get_model_cache_key(messages, stream=False, response_format=response_format, tools=tools)
329
+ cached_data = self._get_cached_model_response(cache_key)
330
+
331
+ if cached_data:
332
+ log_info("Cache hit for model response")
333
+ return self._model_response_from_cache(cached_data)
334
+
205
335
  log_debug(f"{self.get_provider()} Response Start", center=True, symbol="-")
206
336
  log_debug(f"Model: {self.id}", center=True, symbol="-")
207
337
 
@@ -210,6 +340,9 @@ class Model(ABC):
210
340
 
211
341
  function_call_count = 0
212
342
 
343
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
344
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
345
+
213
346
  while True:
214
347
  # Get response from model
215
348
  assistant_message = Message(role=self.assistant_message_role)
@@ -218,7 +351,7 @@ class Model(ABC):
218
351
  assistant_message=assistant_message,
219
352
  model_response=model_response,
220
353
  response_format=response_format,
221
- tools=tools,
354
+ tools=_tool_dicts,
222
355
  tool_choice=tool_choice or self._tool_choice,
223
356
  run_response=run_response,
224
357
  )
@@ -236,7 +369,7 @@ class Model(ABC):
236
369
  assistant_message=assistant_message,
237
370
  messages=messages,
238
371
  model_response=model_response,
239
- functions=functions,
372
+ functions=_functions,
240
373
  )
241
374
  function_call_results: List[Message] = []
242
375
 
@@ -334,14 +467,18 @@ class Model(ABC):
334
467
  break
335
468
 
336
469
  log_debug(f"{self.get_provider()} Response End", center=True, symbol="-")
470
+
471
+ # Save to cache if enabled
472
+ if self.cache_response:
473
+ self._save_model_response_to_cache(cache_key, model_response, is_streaming=False)
474
+
337
475
  return model_response
338
476
 
339
477
  async def aresponse(
340
478
  self,
341
479
  messages: List[Message],
342
480
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
343
- tools: Optional[List[Dict[str, Any]]] = None,
344
- functions: Optional[Dict[str, Function]] = None,
481
+ tools: Optional[List[Union[Function, dict]]] = None,
345
482
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
346
483
  tool_call_limit: Optional[int] = None,
347
484
  send_media_to_model: bool = True,
@@ -350,11 +487,23 @@ class Model(ABC):
350
487
  Generate an asynchronous response from the model.
351
488
  """
352
489
 
490
+ # Check cache if enabled
491
+ if self.cache_response:
492
+ cache_key = self._get_model_cache_key(messages, stream=False, response_format=response_format, tools=tools)
493
+ cached_data = self._get_cached_model_response(cache_key)
494
+
495
+ if cached_data:
496
+ log_info("Cache hit for model response")
497
+ return self._model_response_from_cache(cached_data)
498
+
353
499
  log_debug(f"{self.get_provider()} Async Response Start", center=True, symbol="-")
354
500
  log_debug(f"Model: {self.id}", center=True, symbol="-")
355
501
  _log_messages(messages)
356
502
  model_response = ModelResponse()
357
503
 
504
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
505
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
506
+
358
507
  function_call_count = 0
359
508
 
360
509
  while True:
@@ -365,7 +514,7 @@ class Model(ABC):
365
514
  assistant_message=assistant_message,
366
515
  model_response=model_response,
367
516
  response_format=response_format,
368
- tools=tools,
517
+ tools=_tool_dicts,
369
518
  tool_choice=tool_choice or self._tool_choice,
370
519
  )
371
520
 
@@ -382,7 +531,7 @@ class Model(ABC):
382
531
  assistant_message=assistant_message,
383
532
  messages=messages,
384
533
  model_response=model_response,
385
- functions=functions,
534
+ functions=_functions,
386
535
  )
387
536
  function_call_results: List[Message] = []
388
537
 
@@ -479,6 +628,11 @@ class Model(ABC):
479
628
  break
480
629
 
481
630
  log_debug(f"{self.get_provider()} Async Response End", center=True, symbol="-")
631
+
632
+ # Save to cache if enabled
633
+ if self.cache_response:
634
+ self._save_model_response_to_cache(cache_key, model_response, is_streaming=False)
635
+
482
636
  return model_response
483
637
 
484
638
  def _process_model_response(
@@ -693,8 +847,7 @@ class Model(ABC):
693
847
  self,
694
848
  messages: List[Message],
695
849
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
696
- tools: Optional[List[Dict[str, Any]]] = None,
697
- functions: Optional[Dict[str, Function]] = None,
850
+ tools: Optional[List[Union[Function, dict]]] = None,
698
851
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
699
852
  tool_call_limit: Optional[int] = None,
700
853
  stream_model_response: bool = True,
@@ -705,10 +858,31 @@ class Model(ABC):
705
858
  Generate a streaming response from the model.
706
859
  """
707
860
 
861
+ # Check cache if enabled - capture key BEFORE streaming to avoid mismatch
862
+ cache_key = None
863
+ if self.cache_response:
864
+ cache_key = self._get_model_cache_key(messages, stream=True, response_format=response_format, tools=tools)
865
+ cached_data = self._get_cached_model_response(cache_key)
866
+
867
+ if cached_data:
868
+ log_info("Cache hit for streaming model response")
869
+ # Yield cached responses
870
+ for response in self._streaming_responses_from_cache(cached_data["streaming_responses"]):
871
+ yield response
872
+ return
873
+
874
+ log_info("Cache miss for streaming model response")
875
+
876
+ # Track streaming responses for caching
877
+ streaming_responses: List[ModelResponse] = []
878
+
708
879
  log_debug(f"{self.get_provider()} Response Stream Start", center=True, symbol="-")
709
880
  log_debug(f"Model: {self.id}", center=True, symbol="-")
710
881
  _log_messages(messages)
711
882
 
883
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
884
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
885
+
712
886
  function_call_count = 0
713
887
 
714
888
  while True:
@@ -718,15 +892,18 @@ class Model(ABC):
718
892
  model_response = ModelResponse()
719
893
  if stream_model_response:
720
894
  # Generate response
721
- yield from self.process_response_stream(
895
+ for response in self.process_response_stream(
722
896
  messages=messages,
723
897
  assistant_message=assistant_message,
724
898
  stream_data=stream_data,
725
899
  response_format=response_format,
726
- tools=tools,
900
+ tools=_tool_dicts,
727
901
  tool_choice=tool_choice or self._tool_choice,
728
902
  run_response=run_response,
729
- )
903
+ ):
904
+ if self.cache_response and isinstance(response, ModelResponse):
905
+ streaming_responses.append(response)
906
+ yield response
730
907
 
731
908
  # Populate assistant message from stream data
732
909
  if stream_data.response_content:
@@ -750,9 +927,11 @@ class Model(ABC):
750
927
  assistant_message=assistant_message,
751
928
  model_response=model_response,
752
929
  response_format=response_format,
753
- tools=tools,
930
+ tools=_tool_dicts,
754
931
  tool_choice=tool_choice or self._tool_choice,
755
932
  )
933
+ if self.cache_response:
934
+ streaming_responses.append(model_response)
756
935
  yield model_response
757
936
 
758
937
  # Add assistant message to messages
@@ -763,7 +942,7 @@ class Model(ABC):
763
942
  if assistant_message.tool_calls is not None:
764
943
  # Prepare function calls
765
944
  function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
766
- assistant_message, messages, functions
945
+ assistant_message=assistant_message, messages=messages, functions=_functions
767
946
  )
768
947
  function_call_results: List[Message] = []
769
948
 
@@ -774,6 +953,8 @@ class Model(ABC):
774
953
  current_function_call_count=function_call_count,
775
954
  function_call_limit=tool_call_limit,
776
955
  ):
956
+ if self.cache_response and isinstance(function_call_response, ModelResponse):
957
+ streaming_responses.append(function_call_response)
777
958
  yield function_call_response
778
959
 
779
960
  # Add a function call for each successful execution
@@ -792,7 +973,7 @@ class Model(ABC):
792
973
  self.format_function_call_results(messages=messages, function_call_results=function_call_results)
793
974
 
794
975
  # Handle function call media
795
- if any(msg.images or msg.videos or msg.audio for msg in function_call_results):
976
+ if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
796
977
  self._handle_function_call_media(
797
978
  messages=messages,
798
979
  function_call_results=function_call_results,
@@ -826,6 +1007,10 @@ class Model(ABC):
826
1007
 
827
1008
  log_debug(f"{self.get_provider()} Response Stream End", center=True, symbol="-")
828
1009
 
1010
+ # Save streaming responses to cache if enabled
1011
+ if self.cache_response and cache_key and streaming_responses:
1012
+ self._save_streaming_responses_to_cache(cache_key, streaming_responses)
1013
+
829
1014
  async def aprocess_response_stream(
830
1015
  self,
831
1016
  messages: List[Message],
@@ -861,8 +1046,7 @@ class Model(ABC):
861
1046
  self,
862
1047
  messages: List[Message],
863
1048
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
864
- tools: Optional[List[Dict[str, Any]]] = None,
865
- functions: Optional[Dict[str, Function]] = None,
1049
+ tools: Optional[List[Union[Function, dict]]] = None,
866
1050
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
867
1051
  tool_call_limit: Optional[int] = None,
868
1052
  stream_model_response: bool = True,
@@ -873,10 +1057,31 @@ class Model(ABC):
873
1057
  Generate an asynchronous streaming response from the model.
874
1058
  """
875
1059
 
1060
+ # Check cache if enabled - capture key BEFORE streaming to avoid mismatch
1061
+ cache_key = None
1062
+ if self.cache_response:
1063
+ cache_key = self._get_model_cache_key(messages, stream=True, response_format=response_format, tools=tools)
1064
+ cached_data = self._get_cached_model_response(cache_key)
1065
+
1066
+ if cached_data:
1067
+ log_info("Cache hit for async streaming model response")
1068
+ # Yield cached responses
1069
+ for response in self._streaming_responses_from_cache(cached_data["streaming_responses"]):
1070
+ yield response
1071
+ return
1072
+
1073
+ log_info("Cache miss for async streaming model response")
1074
+
1075
+ # Track streaming responses for caching
1076
+ streaming_responses: List[ModelResponse] = []
1077
+
876
1078
  log_debug(f"{self.get_provider()} Async Response Stream Start", center=True, symbol="-")
877
1079
  log_debug(f"Model: {self.id}", center=True, symbol="-")
878
1080
  _log_messages(messages)
879
1081
 
1082
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
1083
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
1084
+
880
1085
  function_call_count = 0
881
1086
 
882
1087
  while True:
@@ -891,10 +1096,12 @@ class Model(ABC):
891
1096
  assistant_message=assistant_message,
892
1097
  stream_data=stream_data,
893
1098
  response_format=response_format,
894
- tools=tools,
1099
+ tools=_tool_dicts,
895
1100
  tool_choice=tool_choice or self._tool_choice,
896
1101
  run_response=run_response,
897
1102
  ):
1103
+ if self.cache_response and isinstance(model_response, ModelResponse):
1104
+ streaming_responses.append(model_response)
898
1105
  yield model_response
899
1106
 
900
1107
  # Populate assistant message from stream data
@@ -917,10 +1124,12 @@ class Model(ABC):
917
1124
  assistant_message=assistant_message,
918
1125
  model_response=model_response,
919
1126
  response_format=response_format,
920
- tools=tools,
1127
+ tools=_tool_dicts,
921
1128
  tool_choice=tool_choice or self._tool_choice,
922
1129
  run_response=run_response,
923
1130
  )
1131
+ if self.cache_response:
1132
+ streaming_responses.append(model_response)
924
1133
  yield model_response
925
1134
 
926
1135
  # Add assistant message to messages
@@ -931,7 +1140,7 @@ class Model(ABC):
931
1140
  if assistant_message.tool_calls is not None:
932
1141
  # Prepare function calls
933
1142
  function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
934
- assistant_message, messages, functions
1143
+ assistant_message=assistant_message, messages=messages, functions=_functions
935
1144
  )
936
1145
  function_call_results: List[Message] = []
937
1146
 
@@ -942,6 +1151,8 @@ class Model(ABC):
942
1151
  current_function_call_count=function_call_count,
943
1152
  function_call_limit=tool_call_limit,
944
1153
  ):
1154
+ if self.cache_response and isinstance(function_call_response, ModelResponse):
1155
+ streaming_responses.append(function_call_response)
945
1156
  yield function_call_response
946
1157
 
947
1158
  # Add a function call for each successful execution
@@ -960,7 +1171,7 @@ class Model(ABC):
960
1171
  self.format_function_call_results(messages=messages, function_call_results=function_call_results)
961
1172
 
962
1173
  # Handle function call media
963
- if any(msg.images or msg.videos or msg.audio for msg in function_call_results):
1174
+ if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
964
1175
  self._handle_function_call_media(
965
1176
  messages=messages,
966
1177
  function_call_results=function_call_results,
@@ -994,6 +1205,10 @@ class Model(ABC):
994
1205
 
995
1206
  log_debug(f"{self.get_provider()} Async Response Stream End", center=True, symbol="-")
996
1207
 
1208
+ # Save streaming responses to cache if enabled
1209
+ if self.cache_response and cache_key and streaming_responses:
1210
+ self._save_streaming_responses_to_cache(cache_key, streaming_responses)
1211
+
997
1212
  def _populate_stream_data_and_assistant_message(
998
1213
  self, stream_data: MessageData, assistant_message: Message, model_response_delta: ModelResponse
999
1214
  ) -> Iterator[ModelResponse]:
@@ -1147,12 +1362,14 @@ class Model(ABC):
1147
1362
  images = None
1148
1363
  videos = None
1149
1364
  audios = None
1365
+ files = None
1150
1366
 
1151
1367
  if success and function_execution_result:
1152
1368
  # With unified classes, no conversion needed - use directly
1153
1369
  images = function_execution_result.images
1154
1370
  videos = function_execution_result.videos
1155
1371
  audios = function_execution_result.audios
1372
+ files = function_execution_result.files
1156
1373
 
1157
1374
  return Message(
1158
1375
  role=self.tool_message_role,
@@ -1165,6 +1382,7 @@ class Model(ABC):
1165
1382
  images=images,
1166
1383
  videos=videos,
1167
1384
  audio=audios,
1385
+ files=files,
1168
1386
  **kwargs, # type: ignore
1169
1387
  )
1170
1388
 
@@ -1234,7 +1452,7 @@ class Model(ABC):
1234
1452
  # Capture output
1235
1453
  function_call_output += item.content or ""
1236
1454
 
1237
- if function_call.function.show_result:
1455
+ if function_call.function.show_result and item.content is not None:
1238
1456
  yield ModelResponse(content=item.content)
1239
1457
 
1240
1458
  if isinstance(item, CustomEvent):
@@ -1245,7 +1463,7 @@ class Model(ABC):
1245
1463
 
1246
1464
  else:
1247
1465
  function_call_output += str(item)
1248
- if function_call.function.show_result:
1466
+ if function_call.function.show_result and item is not None:
1249
1467
  yield ModelResponse(content=str(item))
1250
1468
  else:
1251
1469
  from agno.tools.function import ToolResult
@@ -1267,7 +1485,7 @@ class Model(ABC):
1267
1485
  else:
1268
1486
  function_call_output = str(function_execution_result.result) if function_execution_result.result else ""
1269
1487
 
1270
- if function_call.function.show_result:
1488
+ if function_call.function.show_result and function_call_output is not None:
1271
1489
  yield ModelResponse(content=function_call_output)
1272
1490
 
1273
1491
  # Create and yield function call result
@@ -1416,6 +1634,7 @@ class Model(ABC):
1416
1634
  function_call_timer = Timer()
1417
1635
  function_call_timer.start()
1418
1636
  success: Union[bool, AgentRunException] = False
1637
+ result: FunctionExecutionResult = FunctionExecutionResult(status="failure")
1419
1638
 
1420
1639
  try:
1421
1640
  if (
@@ -1622,7 +1841,7 @@ class Model(ABC):
1622
1841
  # Capture output
1623
1842
  function_call_output += item.content or ""
1624
1843
 
1625
- if function_call.function.show_result:
1844
+ if function_call.function.show_result and item.content is not None:
1626
1845
  await event_queue.put(ModelResponse(content=item.content))
1627
1846
  continue
1628
1847
 
@@ -1635,7 +1854,7 @@ class Model(ABC):
1635
1854
  # Yield custom events emitted by the tool
1636
1855
  else:
1637
1856
  function_call_output += str(item)
1638
- if function_call.function.show_result:
1857
+ if function_call.function.show_result and item is not None:
1639
1858
  await event_queue.put(ModelResponse(content=str(item)))
1640
1859
 
1641
1860
  # Store the final output for this generator
@@ -1731,7 +1950,7 @@ class Model(ABC):
1731
1950
  # Capture output
1732
1951
  function_call_output += item.content or ""
1733
1952
 
1734
- if function_call.function.show_result:
1953
+ if function_call.function.show_result and item.content is not None:
1735
1954
  yield ModelResponse(content=item.content)
1736
1955
  continue
1737
1956
 
@@ -1739,7 +1958,7 @@ class Model(ABC):
1739
1958
  yield item
1740
1959
  else:
1741
1960
  function_call_output += str(item)
1742
- if function_call.function.show_result:
1961
+ if function_call.function.show_result and item is not None:
1743
1962
  yield ModelResponse(content=str(item))
1744
1963
  else:
1745
1964
  from agno.tools.function import ToolResult
@@ -1759,7 +1978,7 @@ class Model(ABC):
1759
1978
  else:
1760
1979
  function_call_output = str(function_call.result)
1761
1980
 
1762
- if function_call.function.show_result:
1981
+ if function_call.function.show_result and function_call_output is not None:
1763
1982
  yield ModelResponse(content=function_call_output)
1764
1983
 
1765
1984
  # Create and yield function call result
@@ -1814,7 +2033,7 @@ class Model(ABC):
1814
2033
  model_response.tool_calls = []
1815
2034
 
1816
2035
  function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
1817
- assistant_message, messages, functions
2036
+ assistant_message=assistant_message, messages=messages, functions=functions
1818
2037
  )
1819
2038
  return function_calls_to_run
1820
2039
 
agno/models/response.py CHANGED
@@ -123,6 +123,75 @@ class ModelResponse:
123
123
 
124
124
  updated_session_state: Optional[Dict[str, Any]] = None
125
125
 
126
+ def to_dict(self) -> Dict[str, Any]:
127
+ """Serialize ModelResponse to dictionary for caching."""
128
+ _dict = asdict(self)
129
+
130
+ # Handle special serialization for audio
131
+ if self.audio is not None:
132
+ _dict["audio"] = self.audio.to_dict()
133
+
134
+ # Handle lists of media objects
135
+ if self.images is not None:
136
+ _dict["images"] = [img.to_dict() for img in self.images]
137
+ if self.videos is not None:
138
+ _dict["videos"] = [vid.to_dict() for vid in self.videos]
139
+ if self.audios is not None:
140
+ _dict["audios"] = [aud.to_dict() for aud in self.audios]
141
+ if self.files is not None:
142
+ _dict["files"] = [f.to_dict() for f in self.files]
143
+
144
+ # Handle tool executions
145
+ if self.tool_executions is not None:
146
+ _dict["tool_executions"] = [tool_execution.to_dict() for tool_execution in self.tool_executions]
147
+
148
+ # Handle response usage which might be a Pydantic BaseModel
149
+ response_usage = _dict.pop("response_usage", None)
150
+ if response_usage is not None:
151
+ try:
152
+ from pydantic import BaseModel
153
+
154
+ if isinstance(response_usage, BaseModel):
155
+ _dict["response_usage"] = response_usage.model_dump()
156
+ else:
157
+ _dict["response_usage"] = response_usage
158
+ except ImportError:
159
+ _dict["response_usage"] = response_usage
160
+
161
+ return _dict
162
+
163
+ @classmethod
164
+ def from_dict(cls, data: Dict[str, Any]) -> "ModelResponse":
165
+ """Reconstruct ModelResponse from cached dictionary."""
166
+ # Reconstruct media objects
167
+ if data.get("audio"):
168
+ data["audio"] = Audio(**data["audio"])
169
+
170
+ if data.get("images"):
171
+ data["images"] = [Image(**img) for img in data["images"]]
172
+ if data.get("videos"):
173
+ data["videos"] = [Video(**vid) for vid in data["videos"]]
174
+ if data.get("audios"):
175
+ data["audios"] = [Audio(**aud) for aud in data["audios"]]
176
+ if data.get("files"):
177
+ data["files"] = [File(**f) for f in data["files"]]
178
+
179
+ # Reconstruct tool executions
180
+ if data.get("tool_executions"):
181
+ data["tool_executions"] = [ToolExecution.from_dict(te) for te in data["tool_executions"]]
182
+
183
+ # Reconstruct citations
184
+ if data.get("citations") and isinstance(data["citations"], dict):
185
+ data["citations"] = Citations(**data["citations"])
186
+
187
+ # Reconstruct response usage (Metrics)
188
+ if data.get("response_usage") and isinstance(data["response_usage"], dict):
189
+ from agno.models.metrics import Metrics
190
+
191
+ data["response_usage"] = Metrics(**data["response_usage"])
192
+
193
+ return cls(**data)
194
+
126
195
 
127
196
  class FileType(str, Enum):
128
197
  MP4 = "mp4"