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.
Files changed (68) hide show
  1. agno/agent/agent.py +67 -17
  2. agno/db/dynamo/dynamo.py +7 -5
  3. agno/db/firestore/firestore.py +4 -2
  4. agno/db/gcs_json/gcs_json_db.py +4 -2
  5. agno/db/json/json_db.py +8 -4
  6. agno/db/mongo/mongo.py +6 -4
  7. agno/db/mysql/mysql.py +2 -1
  8. agno/db/postgres/postgres.py +2 -1
  9. agno/db/redis/redis.py +1 -1
  10. agno/db/singlestore/singlestore.py +2 -2
  11. agno/db/sqlite/sqlite.py +1 -1
  12. agno/knowledge/chunking/semantic.py +33 -6
  13. agno/knowledge/embedder/openai.py +19 -11
  14. agno/knowledge/knowledge.py +4 -3
  15. agno/knowledge/reader/website_reader.py +33 -16
  16. agno/media.py +72 -0
  17. agno/models/aimlapi/aimlapi.py +2 -2
  18. agno/models/base.py +68 -12
  19. agno/models/cerebras/cerebras_openai.py +2 -2
  20. agno/models/deepinfra/deepinfra.py +2 -2
  21. agno/models/deepseek/deepseek.py +2 -2
  22. agno/models/fireworks/fireworks.py +2 -2
  23. agno/models/internlm/internlm.py +2 -2
  24. agno/models/langdb/langdb.py +4 -4
  25. agno/models/litellm/litellm_openai.py +2 -2
  26. agno/models/llama_cpp/__init__.py +5 -0
  27. agno/models/llama_cpp/llama_cpp.py +22 -0
  28. agno/models/message.py +26 -0
  29. agno/models/meta/llama_openai.py +2 -2
  30. agno/models/nebius/nebius.py +2 -2
  31. agno/models/nexus/__init__.py +3 -0
  32. agno/models/nexus/nexus.py +22 -0
  33. agno/models/nvidia/nvidia.py +2 -2
  34. agno/models/openrouter/openrouter.py +2 -2
  35. agno/models/perplexity/perplexity.py +2 -2
  36. agno/models/portkey/portkey.py +3 -3
  37. agno/models/response.py +2 -1
  38. agno/models/sambanova/sambanova.py +2 -2
  39. agno/models/together/together.py +2 -2
  40. agno/models/vercel/v0.py +2 -2
  41. agno/models/xai/xai.py +2 -2
  42. agno/os/app.py +4 -10
  43. agno/os/router.py +3 -2
  44. agno/os/routers/evals/evals.py +1 -1
  45. agno/os/routers/memory/memory.py +1 -1
  46. agno/os/schema.py +3 -4
  47. agno/os/utils.py +47 -12
  48. agno/run/agent.py +20 -0
  49. agno/run/team.py +18 -1
  50. agno/run/workflow.py +10 -0
  51. agno/team/team.py +58 -18
  52. agno/tools/decorator.py +4 -2
  53. agno/tools/e2b.py +14 -7
  54. agno/tools/file_generation.py +350 -0
  55. agno/tools/function.py +2 -0
  56. agno/tools/mcp.py +1 -1
  57. agno/tools/memori.py +1 -53
  58. agno/utils/events.py +7 -1
  59. agno/utils/gemini.py +24 -4
  60. agno/vectordb/chroma/chromadb.py +66 -25
  61. agno/vectordb/lancedb/lance_db.py +15 -4
  62. agno/vectordb/milvus/milvus.py +6 -0
  63. agno/workflow/workflow.py +32 -0
  64. {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/METADATA +4 -1
  65. {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/RECORD +68 -63
  66. {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/WHEEL +0 -0
  67. {agno-2.0.5.dist-info → agno-2.0.7.dist-info}/licenses/LICENSE +0 -0
  68. {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.name in ["article", "main"]:
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
- if any(cls in ["content", "main-content", "post-content"] for cls in tag.get("class", [])): # type: ignore
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
- # Use a single call to 'find' with a custom function to match tags or classes
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
- # If we only have a div without specific content classes, return empty string
121
- if soup.find("div") and not any(
122
- soup.find(class_=class_name) for class_name in ["content", "main-content", "post-content"]
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
- logger.warning(f"HTTP status error while crawling {current_url}: {e}")
217
- # For the initial URL, we should raise the error
218
- if current_url == url and not crawler_result:
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}
@@ -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(messages=messages, function_call_results=function_call_results)
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(messages=messages, function_call_results=function_call_results)
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(messages=messages, function_call_results=function_call_results)
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(messages=messages, function_call_results=function_call_results)
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
- stream_data.extra.update(model_response_delta.extra)
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(self, messages: List[Message], function_call_results: List[Message]) -> None:
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
- # If we have media artifacts, add a follow-up "user" message instead of a "tool"
1717
- # message with the media artifacts which throws error for some models
1718
- if all_images or all_videos or all_audio:
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
@@ -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"
@@ -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"
@@ -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,5 @@
1
+ from agno.models.llama_cpp.llama_cpp import LlamaCpp
2
+
3
+ __all__ = [
4
+ "LlamaCpp",
5
+ ]
@@ -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
 
@@ -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
@@ -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]:
@@ -0,0 +1,3 @@
1
+ from agno.models.nexus.nexus import Nexus
2
+
3
+ __all__ = ["Nexus"]