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.
- agno/agent/agent.py +735 -574
- agno/culture/manager.py +22 -24
- agno/db/async_postgres/__init__.py +1 -1
- agno/db/dynamo/dynamo.py +0 -2
- agno/db/firestore/firestore.py +0 -2
- agno/db/gcs_json/gcs_json_db.py +0 -4
- agno/db/gcs_json/utils.py +0 -24
- agno/db/in_memory/in_memory_db.py +0 -3
- agno/db/json/json_db.py +4 -10
- agno/db/json/utils.py +0 -24
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +1999 -0
- agno/db/mongo/mongo.py +0 -2
- agno/db/mysql/mysql.py +0 -3
- agno/db/postgres/__init__.py +1 -1
- agno/db/{async_postgres → postgres}/async_postgres.py +19 -22
- agno/db/postgres/postgres.py +7 -10
- agno/db/postgres/utils.py +106 -2
- agno/db/redis/redis.py +0 -2
- agno/db/singlestore/singlestore.py +0 -3
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2269 -0
- agno/db/sqlite/sqlite.py +0 -2
- agno/db/sqlite/utils.py +96 -0
- agno/db/surrealdb/surrealdb.py +0 -6
- agno/knowledge/knowledge.py +3 -3
- agno/knowledge/reader/reader_factory.py +16 -0
- agno/knowledge/reader/tavily_reader.py +194 -0
- agno/memory/manager.py +28 -25
- agno/models/anthropic/claude.py +63 -6
- agno/models/base.py +251 -32
- agno/models/response.py +69 -0
- agno/os/router.py +7 -5
- agno/os/routers/memory/memory.py +2 -1
- agno/os/routers/memory/schemas.py +5 -2
- agno/os/schema.py +25 -20
- agno/os/utils.py +9 -2
- agno/run/agent.py +23 -30
- agno/run/base.py +17 -1
- agno/run/team.py +23 -29
- agno/run/workflow.py +17 -12
- agno/session/agent.py +3 -0
- agno/session/summary.py +4 -1
- agno/session/team.py +1 -1
- agno/team/team.py +599 -367
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/function.py +40 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +324 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/slack.py +18 -3
- agno/tools/tavily.py +146 -0
- agno/utils/agent.py +366 -1
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +166 -1
- agno/utils/print_response/workflow.py +17 -1
- agno/utils/team.py +89 -1
- agno/workflow/step.py +0 -1
- agno/workflow/types.py +10 -15
- {agno-2.2.1.dist-info → agno-2.2.3.dist-info}/METADATA +28 -25
- {agno-2.2.1.dist-info → agno-2.2.3.dist-info}/RECORD +66 -62
- agno/db/async_postgres/schemas.py +0 -139
- agno/db/async_postgres/utils.py +0 -347
- agno/tools/mcp.py +0 -679
- {agno-2.2.1.dist-info → agno-2.2.3.dist-info}/WHEEL +0 -0
- {agno-2.2.1.dist-info → agno-2.2.3.dist-info}/licenses/LICENSE +0 -0
- {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[
|
|
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=
|
|
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=
|
|
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[
|
|
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=
|
|
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=
|
|
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[
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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[
|
|
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=
|
|
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=
|
|
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"
|