agno 2.0.5__py3-none-any.whl → 2.0.7__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 +67 -17
- agno/db/dynamo/dynamo.py +7 -5
- agno/db/firestore/firestore.py +4 -2
- agno/db/gcs_json/gcs_json_db.py +4 -2
- agno/db/json/json_db.py +8 -4
- agno/db/mongo/mongo.py +6 -4
- agno/db/mysql/mysql.py +2 -1
- agno/db/postgres/postgres.py +2 -1
- agno/db/redis/redis.py +1 -1
- agno/db/singlestore/singlestore.py +2 -2
- agno/db/sqlite/sqlite.py +1 -1
- agno/knowledge/chunking/semantic.py +33 -6
- agno/knowledge/embedder/openai.py +19 -11
- agno/knowledge/knowledge.py +4 -3
- agno/knowledge/reader/website_reader.py +33 -16
- agno/media.py +72 -0
- agno/models/aimlapi/aimlapi.py +2 -2
- agno/models/base.py +68 -12
- agno/models/cerebras/cerebras_openai.py +2 -2
- agno/models/deepinfra/deepinfra.py +2 -2
- agno/models/deepseek/deepseek.py +2 -2
- agno/models/fireworks/fireworks.py +2 -2
- agno/models/internlm/internlm.py +2 -2
- agno/models/langdb/langdb.py +4 -4
- agno/models/litellm/litellm_openai.py +2 -2
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/message.py +26 -0
- agno/models/meta/llama_openai.py +2 -2
- agno/models/nebius/nebius.py +2 -2
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/nvidia.py +2 -2
- agno/models/openrouter/openrouter.py +2 -2
- agno/models/perplexity/perplexity.py +2 -2
- agno/models/portkey/portkey.py +3 -3
- agno/models/response.py +2 -1
- agno/models/sambanova/sambanova.py +2 -2
- agno/models/together/together.py +2 -2
- agno/models/vercel/v0.py +2 -2
- agno/models/xai/xai.py +2 -2
- agno/os/app.py +4 -10
- agno/os/router.py +3 -2
- agno/os/routers/evals/evals.py +1 -1
- agno/os/routers/memory/memory.py +1 -1
- agno/os/schema.py +3 -4
- agno/os/utils.py +47 -12
- agno/run/agent.py +20 -0
- agno/run/team.py +18 -1
- agno/run/workflow.py +10 -0
- agno/team/team.py +58 -18
- agno/tools/decorator.py +4 -2
- agno/tools/e2b.py +14 -7
- agno/tools/file_generation.py +350 -0
- agno/tools/function.py +2 -0
- agno/tools/mcp.py +1 -1
- agno/tools/memori.py +1 -53
- agno/utils/events.py +7 -1
- agno/utils/gemini.py +24 -4
- agno/vectordb/chroma/chromadb.py +66 -25
- agno/vectordb/lancedb/lance_db.py +15 -4
- agno/vectordb/milvus/milvus.py +6 -0
- agno/workflow/workflow.py +32 -0
- {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/METADATA +4 -1
- {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/RECORD +68 -63
- {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/WHEEL +0 -0
- {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/top_level.txt +0 -0
|
@@ -106,23 +106,35 @@ class WebsiteReader(Reader):
|
|
|
106
106
|
"""
|
|
107
107
|
Check if the tag matches any of the relevant tags or class names
|
|
108
108
|
"""
|
|
109
|
-
if tag
|
|
109
|
+
if not isinstance(tag, Tag):
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
if tag.name in ["article", "main", "section"]:
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
classes = tag.get("class", [])
|
|
116
|
+
content_classes = ["content", "main-content", "post-content", "entry-content", "article-body"]
|
|
117
|
+
if any(cls in content_classes for cls in classes):
|
|
110
118
|
return True
|
|
111
|
-
|
|
119
|
+
|
|
120
|
+
# Check for common content IDs
|
|
121
|
+
tag_id = tag.get("id", "")
|
|
122
|
+
if tag_id in ["content", "main", "article"]:
|
|
112
123
|
return True
|
|
124
|
+
|
|
113
125
|
return False
|
|
114
126
|
|
|
115
|
-
#
|
|
127
|
+
# Try to find main content element
|
|
116
128
|
element = soup.find(match)
|
|
117
129
|
if element:
|
|
130
|
+
# Remove common unwanted elements from the found content
|
|
131
|
+
for unwanted in element.find_all(["script", "style", "nav", "header", "footer"]):
|
|
132
|
+
unwanted.decompose()
|
|
118
133
|
return element.get_text(strip=True, separator=" ")
|
|
119
134
|
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
):
|
|
124
|
-
return ""
|
|
125
|
-
|
|
135
|
+
# Fallback: get full page content
|
|
136
|
+
for unwanted in soup.find_all(["script", "style", "nav", "header", "footer"]):
|
|
137
|
+
unwanted.decompose()
|
|
126
138
|
return soup.get_text(strip=True, separator=" ")
|
|
127
139
|
|
|
128
140
|
def crawl(self, url: str, starting_depth: int = 1) -> Dict[str, str]:
|
|
@@ -164,7 +176,7 @@ class WebsiteReader(Reader):
|
|
|
164
176
|
if (
|
|
165
177
|
current_url in self._visited
|
|
166
178
|
or not urlparse(current_url).netloc.endswith(primary_domain)
|
|
167
|
-
or current_depth > self.max_depth
|
|
179
|
+
or (current_depth > self.max_depth and current_url != url)
|
|
168
180
|
or num_links >= self.max_links
|
|
169
181
|
):
|
|
170
182
|
continue
|
|
@@ -174,13 +186,14 @@ class WebsiteReader(Reader):
|
|
|
174
186
|
|
|
175
187
|
try:
|
|
176
188
|
log_debug(f"Crawling: {current_url}")
|
|
189
|
+
|
|
177
190
|
response = (
|
|
178
|
-
httpx.get(current_url, timeout=self.timeout, proxy=self.proxy)
|
|
191
|
+
httpx.get(current_url, timeout=self.timeout, proxy=self.proxy, follow_redirects=True)
|
|
179
192
|
if self.proxy
|
|
180
|
-
else httpx.get(current_url, timeout=self.timeout)
|
|
193
|
+
else httpx.get(current_url, timeout=self.timeout, follow_redirects=True)
|
|
181
194
|
)
|
|
182
|
-
|
|
183
195
|
response.raise_for_status()
|
|
196
|
+
|
|
184
197
|
soup = BeautifulSoup(response.content, "html.parser")
|
|
185
198
|
|
|
186
199
|
# Extract main content
|
|
@@ -213,9 +226,13 @@ class WebsiteReader(Reader):
|
|
|
213
226
|
|
|
214
227
|
except httpx.HTTPStatusError as e:
|
|
215
228
|
# Log HTTP status errors but continue crawling other pages
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
229
|
+
# Skip redirect errors (3xx) as they should be handled by follow_redirects
|
|
230
|
+
if e.response.status_code >= 300 and e.response.status_code < 400:
|
|
231
|
+
logger.debug(f"Redirect encountered for {current_url}, skipping: {e}")
|
|
232
|
+
else:
|
|
233
|
+
logger.warning(f"HTTP status error while crawling {current_url}: {e}")
|
|
234
|
+
# For the initial URL, we should raise the error only if it's not a redirect
|
|
235
|
+
if current_url == url and not crawler_result and not (300 <= e.response.status_code < 400):
|
|
219
236
|
raise
|
|
220
237
|
except httpx.RequestError as e:
|
|
221
238
|
# Log request errors but continue crawling other pages
|
agno/media.py
CHANGED
|
@@ -334,11 +334,16 @@ class Video(BaseModel):
|
|
|
334
334
|
|
|
335
335
|
|
|
336
336
|
class File(BaseModel):
|
|
337
|
+
id: Optional[str] = None
|
|
337
338
|
url: Optional[str] = None
|
|
338
339
|
filepath: Optional[Union[Path, str]] = None
|
|
339
340
|
# Raw bytes content of a file
|
|
340
341
|
content: Optional[Any] = None
|
|
341
342
|
mime_type: Optional[str] = None
|
|
343
|
+
|
|
344
|
+
file_type: Optional[str] = None
|
|
345
|
+
filename: Optional[str] = None
|
|
346
|
+
size: Optional[int] = None
|
|
342
347
|
# External file object (e.g. GeminiFile, must be a valid object as expected by the model you are using)
|
|
343
348
|
external: Optional[Any] = None
|
|
344
349
|
format: Optional[str] = None # E.g. `pdf`, `txt`, `csv`, `xml`, etc.
|
|
@@ -364,7 +369,10 @@ class File(BaseModel):
|
|
|
364
369
|
def valid_mime_types(cls) -> List[str]:
|
|
365
370
|
return [
|
|
366
371
|
"application/pdf",
|
|
372
|
+
"application/json",
|
|
367
373
|
"application/x-javascript",
|
|
374
|
+
"application/json",
|
|
375
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
368
376
|
"text/javascript",
|
|
369
377
|
"application/x-python",
|
|
370
378
|
"text/x-python",
|
|
@@ -377,6 +385,29 @@ class File(BaseModel):
|
|
|
377
385
|
"text/rtf",
|
|
378
386
|
]
|
|
379
387
|
|
|
388
|
+
@classmethod
|
|
389
|
+
def from_base64(
|
|
390
|
+
cls,
|
|
391
|
+
base64_content: str,
|
|
392
|
+
id: Optional[str] = None,
|
|
393
|
+
mime_type: Optional[str] = None,
|
|
394
|
+
filename: Optional[str] = None,
|
|
395
|
+
name: Optional[str] = None,
|
|
396
|
+
format: Optional[str] = None,
|
|
397
|
+
) -> "File":
|
|
398
|
+
"""Create File from base64 encoded content"""
|
|
399
|
+
import base64
|
|
400
|
+
|
|
401
|
+
content_bytes = base64.b64decode(base64_content)
|
|
402
|
+
return cls(
|
|
403
|
+
content=content_bytes,
|
|
404
|
+
id=id,
|
|
405
|
+
mime_type=mime_type,
|
|
406
|
+
filename=filename,
|
|
407
|
+
name=name,
|
|
408
|
+
format=format,
|
|
409
|
+
)
|
|
410
|
+
|
|
380
411
|
@property
|
|
381
412
|
def file_url_content(self) -> Optional[Tuple[bytes, str]]:
|
|
382
413
|
import httpx
|
|
@@ -388,3 +419,44 @@ class File(BaseModel):
|
|
|
388
419
|
return content, mime_type
|
|
389
420
|
else:
|
|
390
421
|
return None
|
|
422
|
+
|
|
423
|
+
def _normalise_content(self) -> Optional[Union[str, bytes]]:
|
|
424
|
+
if self.content is None:
|
|
425
|
+
return None
|
|
426
|
+
content_normalised: Union[str, bytes] = self.content
|
|
427
|
+
if content_normalised and isinstance(content_normalised, bytes):
|
|
428
|
+
from base64 import b64encode
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
if self.mime_type and self.mime_type.startswith("text/"):
|
|
432
|
+
content_normalised = content_normalised.decode("utf-8")
|
|
433
|
+
else:
|
|
434
|
+
content_normalised = b64encode(content_normalised).decode("utf-8")
|
|
435
|
+
except UnicodeDecodeError:
|
|
436
|
+
if isinstance(self.content, bytes):
|
|
437
|
+
content_normalised = b64encode(self.content).decode("utf-8")
|
|
438
|
+
except Exception:
|
|
439
|
+
try:
|
|
440
|
+
if isinstance(self.content, bytes):
|
|
441
|
+
content_normalised = b64encode(self.content).decode("utf-8")
|
|
442
|
+
except Exception:
|
|
443
|
+
pass
|
|
444
|
+
return content_normalised
|
|
445
|
+
|
|
446
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
447
|
+
content_normalised = self._normalise_content()
|
|
448
|
+
|
|
449
|
+
response_dict = {
|
|
450
|
+
"id": self.id,
|
|
451
|
+
"url": self.url,
|
|
452
|
+
"filepath": str(self.filepath) if self.filepath else None,
|
|
453
|
+
"content": content_normalised,
|
|
454
|
+
"mime_type": self.mime_type,
|
|
455
|
+
"file_type": self.file_type,
|
|
456
|
+
"filename": self.filename,
|
|
457
|
+
"size": self.size,
|
|
458
|
+
"external": self.external,
|
|
459
|
+
"format": self.format,
|
|
460
|
+
"name": self.name,
|
|
461
|
+
}
|
|
462
|
+
return {k: v for k, v in response_dict.items() if v is not None}
|
agno/models/aimlapi/aimlapi.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Any, Dict, Optional
|
|
4
4
|
|
|
@@ -24,7 +24,7 @@ class AIMLAPI(OpenAILike):
|
|
|
24
24
|
name: str = "AIMLAPI"
|
|
25
25
|
provider: str = "AIMLAPI"
|
|
26
26
|
|
|
27
|
-
api_key: Optional[str] = getenv("AIMLAPI_API_KEY")
|
|
27
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("AIMLAPI_API_KEY"))
|
|
28
28
|
base_url: str = "https://api.aimlapi.com/v1"
|
|
29
29
|
max_tokens: int = 4096
|
|
30
30
|
|
agno/models/base.py
CHANGED
|
@@ -21,7 +21,7 @@ from uuid import uuid4
|
|
|
21
21
|
from pydantic import BaseModel
|
|
22
22
|
|
|
23
23
|
from agno.exceptions import AgentRunException
|
|
24
|
-
from agno.media import Audio, Image, Video
|
|
24
|
+
from agno.media import Audio, File, Image, Video
|
|
25
25
|
from agno.models.message import Citations, Message
|
|
26
26
|
from agno.models.metrics import Metrics
|
|
27
27
|
from agno.models.response import ModelResponse, ModelResponseEvent, ToolExecution
|
|
@@ -46,6 +46,7 @@ class MessageData:
|
|
|
46
46
|
response_audio: Optional[Audio] = None
|
|
47
47
|
response_image: Optional[Image] = None
|
|
48
48
|
response_video: Optional[Video] = None
|
|
49
|
+
response_file: Optional[File] = None
|
|
49
50
|
|
|
50
51
|
# Data from the provider that we might need on subsequent messages
|
|
51
52
|
response_provider_data: Optional[Dict[str, Any]] = None
|
|
@@ -195,6 +196,7 @@ class Model(ABC):
|
|
|
195
196
|
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
196
197
|
tool_call_limit: Optional[int] = None,
|
|
197
198
|
run_response: Optional[RunOutput] = None,
|
|
199
|
+
send_media_to_model: bool = True,
|
|
198
200
|
) -> ModelResponse:
|
|
199
201
|
"""
|
|
200
202
|
Generate a response from the model.
|
|
@@ -266,6 +268,11 @@ class Model(ABC):
|
|
|
266
268
|
model_response.videos = []
|
|
267
269
|
model_response.videos.extend(function_call_response.videos)
|
|
268
270
|
|
|
271
|
+
if function_call_response.files is not None:
|
|
272
|
+
if model_response.files is None:
|
|
273
|
+
model_response.files = []
|
|
274
|
+
model_response.files.extend(function_call_response.files)
|
|
275
|
+
|
|
269
276
|
if (
|
|
270
277
|
function_call_response.event
|
|
271
278
|
in [
|
|
@@ -293,9 +300,13 @@ class Model(ABC):
|
|
|
293
300
|
messages=messages, function_call_results=function_call_results, **model_response.extra or {}
|
|
294
301
|
)
|
|
295
302
|
|
|
296
|
-
if any(msg.images or msg.videos or msg.audio for msg in function_call_results):
|
|
303
|
+
if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
|
|
297
304
|
# Handle function call media
|
|
298
|
-
self._handle_function_call_media(
|
|
305
|
+
self._handle_function_call_media(
|
|
306
|
+
messages=messages,
|
|
307
|
+
function_call_results=function_call_results,
|
|
308
|
+
send_media_to_model=send_media_to_model,
|
|
309
|
+
)
|
|
299
310
|
|
|
300
311
|
for function_call_result in function_call_results:
|
|
301
312
|
function_call_result.log(metrics=True)
|
|
@@ -333,6 +344,7 @@ class Model(ABC):
|
|
|
333
344
|
functions: Optional[Dict[str, Function]] = None,
|
|
334
345
|
tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
|
|
335
346
|
tool_call_limit: Optional[int] = None,
|
|
347
|
+
send_media_to_model: bool = True,
|
|
336
348
|
) -> ModelResponse:
|
|
337
349
|
"""
|
|
338
350
|
Generate an asynchronous response from the model.
|
|
@@ -402,6 +414,11 @@ class Model(ABC):
|
|
|
402
414
|
model_response.videos = []
|
|
403
415
|
model_response.videos.extend(function_call_response.videos)
|
|
404
416
|
|
|
417
|
+
if function_call_response.files is not None:
|
|
418
|
+
if model_response.files is None:
|
|
419
|
+
model_response.files = []
|
|
420
|
+
model_response.files.extend(function_call_response.files)
|
|
421
|
+
|
|
405
422
|
if (
|
|
406
423
|
function_call_response.event
|
|
407
424
|
in [
|
|
@@ -428,9 +445,13 @@ class Model(ABC):
|
|
|
428
445
|
messages=messages, function_call_results=function_call_results, **model_response.extra or {}
|
|
429
446
|
)
|
|
430
447
|
|
|
431
|
-
if any(msg.images or msg.videos or msg.audio for msg in function_call_results):
|
|
448
|
+
if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
|
|
432
449
|
# Handle function call media
|
|
433
|
-
self._handle_function_call_media(
|
|
450
|
+
self._handle_function_call_media(
|
|
451
|
+
messages=messages,
|
|
452
|
+
function_call_results=function_call_results,
|
|
453
|
+
send_media_to_model=send_media_to_model,
|
|
454
|
+
)
|
|
434
455
|
|
|
435
456
|
for function_call_result in function_call_results:
|
|
436
457
|
function_call_result.log(metrics=True)
|
|
@@ -607,6 +628,10 @@ class Model(ABC):
|
|
|
607
628
|
if provider_response.videos:
|
|
608
629
|
assistant_message.video_output = provider_response.videos[-1] # Taking last (most recent) video
|
|
609
630
|
|
|
631
|
+
if provider_response.files is not None:
|
|
632
|
+
if provider_response.files:
|
|
633
|
+
assistant_message.file_output = provider_response.files[-1] # Taking last (most recent) file
|
|
634
|
+
|
|
610
635
|
if provider_response.audios is not None:
|
|
611
636
|
if provider_response.audios:
|
|
612
637
|
assistant_message.audio_output = provider_response.audios[-1] # Taking last (most recent) audio
|
|
@@ -674,6 +699,7 @@ class Model(ABC):
|
|
|
674
699
|
tool_call_limit: Optional[int] = None,
|
|
675
700
|
stream_model_response: bool = True,
|
|
676
701
|
run_response: Optional[RunOutput] = None,
|
|
702
|
+
send_media_to_model: bool = True,
|
|
677
703
|
) -> Iterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
|
|
678
704
|
"""
|
|
679
705
|
Generate a streaming response from the model.
|
|
@@ -763,7 +789,11 @@ class Model(ABC):
|
|
|
763
789
|
|
|
764
790
|
# Handle function call media
|
|
765
791
|
if any(msg.images or msg.videos or msg.audio for msg in function_call_results):
|
|
766
|
-
self._handle_function_call_media(
|
|
792
|
+
self._handle_function_call_media(
|
|
793
|
+
messages=messages,
|
|
794
|
+
function_call_results=function_call_results,
|
|
795
|
+
send_media_to_model=send_media_to_model,
|
|
796
|
+
)
|
|
767
797
|
|
|
768
798
|
for function_call_result in function_call_results:
|
|
769
799
|
function_call_result.log(metrics=True)
|
|
@@ -833,6 +863,7 @@ class Model(ABC):
|
|
|
833
863
|
tool_call_limit: Optional[int] = None,
|
|
834
864
|
stream_model_response: bool = True,
|
|
835
865
|
run_response: Optional[RunOutput] = None,
|
|
866
|
+
send_media_to_model: bool = True,
|
|
836
867
|
) -> AsyncIterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
|
|
837
868
|
"""
|
|
838
869
|
Generate an asynchronous streaming response from the model.
|
|
@@ -922,7 +953,11 @@ class Model(ABC):
|
|
|
922
953
|
|
|
923
954
|
# Handle function call media
|
|
924
955
|
if any(msg.images or msg.videos or msg.audio for msg in function_call_results):
|
|
925
|
-
self._handle_function_call_media(
|
|
956
|
+
self._handle_function_call_media(
|
|
957
|
+
messages=messages,
|
|
958
|
+
function_call_results=function_call_results,
|
|
959
|
+
send_media_to_model=send_media_to_model,
|
|
960
|
+
)
|
|
926
961
|
|
|
927
962
|
for function_call_result in function_call_results:
|
|
928
963
|
function_call_result.log(metrics=True)
|
|
@@ -1026,7 +1061,13 @@ class Model(ABC):
|
|
|
1026
1061
|
if model_response_delta.extra is not None:
|
|
1027
1062
|
if stream_data.extra is None:
|
|
1028
1063
|
stream_data.extra = {}
|
|
1029
|
-
|
|
1064
|
+
for key in model_response_delta.extra:
|
|
1065
|
+
if isinstance(model_response_delta.extra[key], list):
|
|
1066
|
+
if not stream_data.extra.get(key):
|
|
1067
|
+
stream_data.extra[key] = []
|
|
1068
|
+
stream_data.extra[key].extend(model_response_delta.extra[key])
|
|
1069
|
+
else:
|
|
1070
|
+
stream_data.extra[key] = model_response_delta.extra[key]
|
|
1030
1071
|
|
|
1031
1072
|
if should_yield:
|
|
1032
1073
|
yield model_response_delta
|
|
@@ -1213,6 +1254,8 @@ class Model(ABC):
|
|
|
1213
1254
|
function_execution_result.videos = tool_result.videos
|
|
1214
1255
|
if tool_result.audios:
|
|
1215
1256
|
function_execution_result.audios = tool_result.audios
|
|
1257
|
+
if tool_result.files:
|
|
1258
|
+
function_execution_result.files = tool_result.files
|
|
1216
1259
|
else:
|
|
1217
1260
|
function_call_output = str(function_execution_result.result) if function_execution_result.result else ""
|
|
1218
1261
|
|
|
@@ -1246,6 +1289,7 @@ class Model(ABC):
|
|
|
1246
1289
|
images=function_execution_result.images,
|
|
1247
1290
|
videos=function_execution_result.videos,
|
|
1248
1291
|
audios=function_execution_result.audios,
|
|
1292
|
+
files=function_execution_result.files,
|
|
1249
1293
|
)
|
|
1250
1294
|
|
|
1251
1295
|
# Add function call to function call results
|
|
@@ -1617,6 +1661,8 @@ class Model(ABC):
|
|
|
1617
1661
|
function_execution_result.videos = tool_result.videos
|
|
1618
1662
|
if tool_result.audios:
|
|
1619
1663
|
function_execution_result.audios = tool_result.audios
|
|
1664
|
+
if tool_result.files:
|
|
1665
|
+
function_execution_result.files = tool_result.files
|
|
1620
1666
|
else:
|
|
1621
1667
|
function_call_output = str(function_call.result)
|
|
1622
1668
|
|
|
@@ -1649,6 +1695,7 @@ class Model(ABC):
|
|
|
1649
1695
|
images=function_execution_result.images,
|
|
1650
1696
|
videos=function_execution_result.videos,
|
|
1651
1697
|
audios=function_execution_result.audios,
|
|
1698
|
+
files=function_execution_result.files,
|
|
1652
1699
|
)
|
|
1653
1700
|
|
|
1654
1701
|
# Add function call result to function call results
|
|
@@ -1687,7 +1734,9 @@ class Model(ABC):
|
|
|
1687
1734
|
if len(function_call_results) > 0:
|
|
1688
1735
|
messages.extend(function_call_results)
|
|
1689
1736
|
|
|
1690
|
-
def _handle_function_call_media(
|
|
1737
|
+
def _handle_function_call_media(
|
|
1738
|
+
self, messages: List[Message], function_call_results: List[Message], send_media_to_model: bool = True
|
|
1739
|
+
) -> None:
|
|
1691
1740
|
"""
|
|
1692
1741
|
Handle media artifacts from function calls by adding follow-up user messages for generated media if needed.
|
|
1693
1742
|
"""
|
|
@@ -1698,6 +1747,7 @@ class Model(ABC):
|
|
|
1698
1747
|
all_images: List[Image] = []
|
|
1699
1748
|
all_videos: List[Video] = []
|
|
1700
1749
|
all_audio: List[Audio] = []
|
|
1750
|
+
all_files: List[File] = []
|
|
1701
1751
|
|
|
1702
1752
|
for result_message in function_call_results:
|
|
1703
1753
|
if result_message.images:
|
|
@@ -1713,15 +1763,21 @@ class Model(ABC):
|
|
|
1713
1763
|
all_audio.extend(result_message.audio)
|
|
1714
1764
|
result_message.audio = None
|
|
1715
1765
|
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1766
|
+
if result_message.files:
|
|
1767
|
+
all_files.extend(result_message.files)
|
|
1768
|
+
result_message.files = None
|
|
1769
|
+
|
|
1770
|
+
# Only add media message if we should send media to model
|
|
1771
|
+
if send_media_to_model and (all_images or all_videos or all_audio or all_files):
|
|
1772
|
+
# If we have media artifacts, add a follow-up "user" message instead of a "tool"
|
|
1773
|
+
# message with the media artifacts which throws error for some models
|
|
1719
1774
|
media_message = Message(
|
|
1720
1775
|
role="user",
|
|
1721
1776
|
content="Take note of the following content",
|
|
1722
1777
|
images=all_images if all_images else None,
|
|
1723
1778
|
videos=all_videos if all_videos else None,
|
|
1724
1779
|
audio=all_audio if all_audio else None,
|
|
1780
|
+
files=all_files if all_files else None,
|
|
1725
1781
|
)
|
|
1726
1782
|
messages.append(media_message)
|
|
1727
1783
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from dataclasses import dataclass
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
3
|
from os import getenv
|
|
4
4
|
from typing import Any, Dict, List, Optional, Type, Union
|
|
5
5
|
|
|
@@ -18,7 +18,7 @@ class CerebrasOpenAI(OpenAILike):
|
|
|
18
18
|
|
|
19
19
|
parallel_tool_calls: Optional[bool] = None
|
|
20
20
|
base_url: str = "https://api.cerebras.ai/v1"
|
|
21
|
-
api_key: Optional[str] = getenv("CEREBRAS_API_KEY", None)
|
|
21
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("CEREBRAS_API_KEY", None))
|
|
22
22
|
|
|
23
23
|
def get_request_params(
|
|
24
24
|
self,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
@@ -22,7 +22,7 @@ class DeepInfra(OpenAILike):
|
|
|
22
22
|
name: str = "DeepInfra"
|
|
23
23
|
provider: str = "DeepInfra"
|
|
24
24
|
|
|
25
|
-
api_key: Optional[str] = getenv("DEEPINFRA_API_KEY")
|
|
25
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("DEEPINFRA_API_KEY"))
|
|
26
26
|
base_url: str = "https://api.deepinfra.com/v1/openai"
|
|
27
27
|
|
|
28
28
|
supports_native_structured_outputs: bool = False
|
agno/models/deepseek/deepseek.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Any, Dict, Optional
|
|
4
4
|
|
|
@@ -23,7 +23,7 @@ class DeepSeek(OpenAILike):
|
|
|
23
23
|
name: str = "DeepSeek"
|
|
24
24
|
provider: str = "DeepSeek"
|
|
25
25
|
|
|
26
|
-
api_key: Optional[str] = getenv("DEEPSEEK_API_KEY")
|
|
26
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("DEEPSEEK_API_KEY"))
|
|
27
27
|
base_url: str = "https://api.deepseek.com"
|
|
28
28
|
|
|
29
29
|
# Their support for structured outputs is currently broken
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
@@ -22,5 +22,5 @@ class Fireworks(OpenAILike):
|
|
|
22
22
|
name: str = "Fireworks"
|
|
23
23
|
provider: str = "Fireworks"
|
|
24
24
|
|
|
25
|
-
api_key: Optional[str] = getenv("FIREWORKS_API_KEY")
|
|
25
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("FIREWORKS_API_KEY"))
|
|
26
26
|
base_url: str = "https://api.fireworks.ai/inference/v1"
|
agno/models/internlm/internlm.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
@@ -22,5 +22,5 @@ class InternLM(OpenAILike):
|
|
|
22
22
|
name: str = "InternLM"
|
|
23
23
|
provider: str = "InternLM"
|
|
24
24
|
|
|
25
|
-
api_key: Optional[str] = getenv("INTERNLM_API_KEY")
|
|
25
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("INTERNLM_API_KEY"))
|
|
26
26
|
base_url: Optional[str] = "https://internlm-chat.intern-ai.org.cn/puyu/api/v1/chat/completions"
|
agno/models/langdb/langdb.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Any, Dict, Optional
|
|
4
4
|
|
|
@@ -22,10 +22,10 @@ class LangDB(OpenAILike):
|
|
|
22
22
|
name: str = "LangDB"
|
|
23
23
|
provider: str = "LangDB"
|
|
24
24
|
|
|
25
|
-
api_key: Optional[str] = getenv("LANGDB_API_KEY")
|
|
26
|
-
project_id: Optional[str] = getenv("LANGDB_PROJECT_ID")
|
|
25
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("LANGDB_API_KEY"))
|
|
26
|
+
project_id: Optional[str] = field(default_factory=lambda: getenv("LANGDB_PROJECT_ID"))
|
|
27
27
|
|
|
28
|
-
base_host_url: str = getenv("LANGDB_API_BASE_URL", "https://api.us-east-1.langdb.ai")
|
|
28
|
+
base_host_url: str = field(default_factory=lambda: getenv("LANGDB_API_BASE_URL", "https://api.us-east-1.langdb.ai"))
|
|
29
29
|
|
|
30
30
|
base_url: Optional[str] = None
|
|
31
31
|
label: Optional[str] = None
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
@@ -21,5 +21,5 @@ class LiteLLMOpenAI(OpenAILike):
|
|
|
21
21
|
name: str = "LiteLLM"
|
|
22
22
|
provider: str = "LiteLLM"
|
|
23
23
|
|
|
24
|
-
api_key: Optional[str] = getenv("LITELLM_API_KEY")
|
|
24
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("LITELLM_API_KEY"))
|
|
25
25
|
base_url: str = "http://0.0.0.0:4000"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from agno.models.openai.like import OpenAILike
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class LlamaCpp(OpenAILike):
|
|
8
|
+
"""
|
|
9
|
+
A class for interacting with LLMs using Llama CPP.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
id (str): The id of the Llama CPP model. Default is "ggml-org/gpt-oss-20b-GGUF".
|
|
13
|
+
name (str): The name of this chat model instance. Default is "LlamaCpp".
|
|
14
|
+
provider (str): The provider of the model. Default is "LlamaCpp".
|
|
15
|
+
base_url (str): The base url to which the requests are sent.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
id: str = "ggml-org/gpt-oss-20b-GGUF"
|
|
19
|
+
name: str = "LlamaCpp"
|
|
20
|
+
provider: str = "LlamaCpp"
|
|
21
|
+
|
|
22
|
+
base_url: str = "http://127.0.0.1:8080/v1"
|
agno/models/message.py
CHANGED
|
@@ -74,6 +74,7 @@ class Message(BaseModel):
|
|
|
74
74
|
audio_output: Optional[Audio] = None
|
|
75
75
|
image_output: Optional[Image] = None
|
|
76
76
|
video_output: Optional[Video] = None
|
|
77
|
+
file_output: Optional[File] = None
|
|
77
78
|
|
|
78
79
|
# The thinking content from the model
|
|
79
80
|
redacted_reasoning_content: Optional[str] = None
|
|
@@ -188,6 +189,29 @@ class Message(BaseModel):
|
|
|
188
189
|
reconstructed_videos.append(vid_data)
|
|
189
190
|
data["videos"] = reconstructed_videos
|
|
190
191
|
|
|
192
|
+
# Handle file reconstruction properly
|
|
193
|
+
if "files" in data and data["files"]:
|
|
194
|
+
reconstructed_files = []
|
|
195
|
+
for i, file_data in enumerate(data["files"]):
|
|
196
|
+
if isinstance(file_data, dict):
|
|
197
|
+
# If content is base64, decode it back to bytes
|
|
198
|
+
if "content" in file_data and isinstance(file_data["content"], str):
|
|
199
|
+
reconstructed_files.append(
|
|
200
|
+
File.from_base64(
|
|
201
|
+
file_data["content"],
|
|
202
|
+
id=file_data.get("id"),
|
|
203
|
+
mime_type=file_data.get("mime_type"),
|
|
204
|
+
filename=file_data.get("filename"),
|
|
205
|
+
name=file_data.get("name"),
|
|
206
|
+
format=file_data.get("format"),
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
reconstructed_files.append(File(**file_data))
|
|
211
|
+
else:
|
|
212
|
+
reconstructed_files.append(file_data)
|
|
213
|
+
data["files"] = reconstructed_files
|
|
214
|
+
|
|
191
215
|
if "audio_output" in data and data["audio_output"]:
|
|
192
216
|
aud_data = data["audio_output"]
|
|
193
217
|
if isinstance(aud_data, dict):
|
|
@@ -261,6 +285,8 @@ class Message(BaseModel):
|
|
|
261
285
|
message_dict["audio"] = [aud.to_dict() for aud in self.audio]
|
|
262
286
|
if self.videos:
|
|
263
287
|
message_dict["videos"] = [vid.to_dict() for vid in self.videos]
|
|
288
|
+
if self.files:
|
|
289
|
+
message_dict["files"] = [file.to_dict() for file in self.files]
|
|
264
290
|
if self.audio_output:
|
|
265
291
|
message_dict["audio_output"] = self.audio_output.to_dict()
|
|
266
292
|
|
agno/models/meta/llama_openai.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Any, Dict, Optional
|
|
4
4
|
|
|
@@ -31,7 +31,7 @@ class LlamaOpenAI(OpenAILike):
|
|
|
31
31
|
name: str = "LlamaOpenAI"
|
|
32
32
|
provider: str = "LlamaOpenAI"
|
|
33
33
|
|
|
34
|
-
api_key: Optional[str] = getenv("LLAMA_API_KEY")
|
|
34
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("LLAMA_API_KEY"))
|
|
35
35
|
base_url: Optional[str] = "https://api.llama.com/compat/v1/"
|
|
36
36
|
|
|
37
37
|
# Request parameters
|
agno/models/nebius/nebius.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Any, Dict, Optional
|
|
4
4
|
|
|
@@ -23,7 +23,7 @@ class Nebius(OpenAILike):
|
|
|
23
23
|
name: str = "Nebius"
|
|
24
24
|
provider: str = "Nebius"
|
|
25
25
|
|
|
26
|
-
api_key: Optional[str] = getenv("NEBIUS_API_KEY")
|
|
26
|
+
api_key: Optional[str] = field(default_factory=lambda: getenv("NEBIUS_API_KEY"))
|
|
27
27
|
base_url: str = "https://api.studio.nebius.com/v1/"
|
|
28
28
|
|
|
29
29
|
def _get_client_params(self) -> Dict[str, Any]:
|