agno 2.0.6__py3-none-any.whl → 2.0.8__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 (52) hide show
  1. agno/agent/agent.py +94 -48
  2. agno/db/migrations/v1_to_v2.py +140 -11
  3. agno/knowledge/chunking/semantic.py +33 -6
  4. agno/knowledge/embedder/sentence_transformer.py +3 -3
  5. agno/knowledge/knowledge.py +152 -31
  6. agno/knowledge/types.py +8 -0
  7. agno/media.py +2 -0
  8. agno/models/base.py +38 -9
  9. agno/models/cometapi/__init__.py +5 -0
  10. agno/models/cometapi/cometapi.py +57 -0
  11. agno/models/google/gemini.py +4 -8
  12. agno/models/llama_cpp/__init__.py +5 -0
  13. agno/models/llama_cpp/llama_cpp.py +22 -0
  14. agno/models/nexus/__init__.py +1 -1
  15. agno/models/nexus/nexus.py +2 -5
  16. agno/models/ollama/chat.py +24 -1
  17. agno/models/openai/chat.py +2 -7
  18. agno/models/openai/responses.py +21 -17
  19. agno/os/app.py +4 -10
  20. agno/os/interfaces/agui/agui.py +2 -2
  21. agno/os/interfaces/agui/utils.py +81 -18
  22. agno/os/interfaces/slack/slack.py +2 -2
  23. agno/os/interfaces/whatsapp/whatsapp.py +2 -2
  24. agno/os/router.py +3 -4
  25. agno/os/routers/evals/evals.py +1 -1
  26. agno/os/routers/memory/memory.py +1 -1
  27. agno/os/schema.py +3 -4
  28. agno/os/utils.py +55 -12
  29. agno/reasoning/default.py +3 -1
  30. agno/run/agent.py +4 -0
  31. agno/run/team.py +3 -1
  32. agno/session/agent.py +8 -5
  33. agno/session/team.py +14 -10
  34. agno/team/team.py +239 -115
  35. agno/tools/decorator.py +4 -2
  36. agno/tools/function.py +43 -4
  37. agno/tools/mcp.py +61 -38
  38. agno/tools/memori.py +1 -53
  39. agno/utils/events.py +7 -1
  40. agno/utils/gemini.py +147 -19
  41. agno/utils/models/claude.py +9 -0
  42. agno/utils/print_response/agent.py +16 -0
  43. agno/utils/print_response/team.py +16 -0
  44. agno/vectordb/base.py +2 -2
  45. agno/vectordb/langchaindb/langchaindb.py +5 -7
  46. agno/vectordb/llamaindex/llamaindexdb.py +25 -6
  47. agno/workflow/workflow.py +59 -15
  48. {agno-2.0.6.dist-info → agno-2.0.8.dist-info}/METADATA +1 -1
  49. {agno-2.0.6.dist-info → agno-2.0.8.dist-info}/RECORD +52 -48
  50. {agno-2.0.6.dist-info → agno-2.0.8.dist-info}/WHEEL +0 -0
  51. {agno-2.0.6.dist-info → agno-2.0.8.dist-info}/licenses/LICENSE +0 -0
  52. {agno-2.0.6.dist-info → agno-2.0.8.dist-info}/top_level.txt +0 -0
@@ -21,7 +21,6 @@ from agno.knowledge.remote_content.remote_content import GCSContent, RemoteConte
21
21
  from agno.utils.http import async_fetch_with_retry
22
22
  from agno.utils.log import log_debug, log_error, log_info, log_warning
23
23
  from agno.utils.string import generate_id
24
- from agno.vectordb import VectorDb
25
24
 
26
25
  ContentDict = Dict[str, Union[str, Dict[str, str]]]
27
26
 
@@ -39,12 +38,15 @@ class Knowledge:
39
38
 
40
39
  name: Optional[str] = None
41
40
  description: Optional[str] = None
42
- vector_db: Optional[VectorDb] = None
41
+ vector_db: Optional[Any] = None
43
42
  contents_db: Optional[BaseDb] = None
44
43
  max_results: int = 10
45
44
  readers: Optional[Dict[str, Reader]] = None
46
45
 
47
46
  def __post_init__(self):
47
+ from agno.vectordb import VectorDb
48
+
49
+ self.vector_db = cast(VectorDb, self.vector_db)
48
50
  if self.vector_db and not self.vector_db.exists():
49
51
  self.vector_db.create()
50
52
 
@@ -64,9 +66,12 @@ class Knowledge:
64
66
  paths: Optional[List[str]] = None,
65
67
  urls: Optional[List[str]] = None,
66
68
  metadata: Optional[Dict[str, str]] = None,
69
+ topics: Optional[List[str]] = None,
70
+ text_contents: Optional[List[str]] = None,
71
+ reader: Optional[Reader] = None,
67
72
  include: Optional[List[str]] = None,
68
73
  exclude: Optional[List[str]] = None,
69
- upsert: bool = False,
74
+ upsert: bool = True,
70
75
  skip_if_exists: bool = False,
71
76
  remote_content: Optional[RemoteContent] = None,
72
77
  ) -> None: ...
@@ -74,7 +79,7 @@ class Knowledge:
74
79
  async def add_contents_async(self, *args, **kwargs) -> None:
75
80
  if args and isinstance(args[0], list):
76
81
  arguments = args[0]
77
- upsert = kwargs.get("upsert", False)
82
+ upsert = kwargs.get("upsert", True)
78
83
  skip_if_exists = kwargs.get("skip_if_exists", False)
79
84
  for argument in arguments:
80
85
  await self.add_content_async(
@@ -84,6 +89,7 @@ class Knowledge:
84
89
  url=argument.get("url"),
85
90
  metadata=argument.get("metadata"),
86
91
  topics=argument.get("topics"),
92
+ text_contents=argument.get("text_contents"),
87
93
  reader=argument.get("reader"),
88
94
  include=argument.get("include"),
89
95
  exclude=argument.get("exclude"),
@@ -97,11 +103,13 @@ class Knowledge:
97
103
  metadata = kwargs.get("metadata", {})
98
104
  description = kwargs.get("description", [])
99
105
  topics = kwargs.get("topics", [])
106
+ reader = kwargs.get("reader", None)
100
107
  paths = kwargs.get("paths", [])
101
108
  urls = kwargs.get("urls", [])
109
+ text_contents = kwargs.get("text_contents", [])
102
110
  include = kwargs.get("include")
103
111
  exclude = kwargs.get("exclude")
104
- upsert = kwargs.get("upsert", False)
112
+ upsert = kwargs.get("upsert", True)
105
113
  skip_if_exists = kwargs.get("skip_if_exists", False)
106
114
  remote_content = kwargs.get("remote_content", None)
107
115
  for path in paths:
@@ -126,6 +134,19 @@ class Knowledge:
126
134
  upsert=upsert,
127
135
  skip_if_exists=skip_if_exists,
128
136
  )
137
+ for i, text_content in enumerate(text_contents):
138
+ content_name = f"{name}_{i}" if name else f"text_content_{i}"
139
+ log_debug(f"Adding text content: {content_name}")
140
+ await self.add_content_async(
141
+ name=content_name,
142
+ description=description,
143
+ text_content=text_content,
144
+ metadata=metadata,
145
+ include=include,
146
+ exclude=exclude,
147
+ upsert=upsert,
148
+ skip_if_exists=skip_if_exists,
149
+ )
129
150
  if topics:
130
151
  await self.add_content_async(
131
152
  name=name,
@@ -136,6 +157,7 @@ class Knowledge:
136
157
  exclude=exclude,
137
158
  upsert=upsert,
138
159
  skip_if_exists=skip_if_exists,
160
+ reader=reader,
139
161
  )
140
162
 
141
163
  if remote_content:
@@ -163,7 +185,7 @@ class Knowledge:
163
185
  metadata: Optional[Dict[str, str]] = None,
164
186
  include: Optional[List[str]] = None,
165
187
  exclude: Optional[List[str]] = None,
166
- upsert: bool = False,
188
+ upsert: bool = True,
167
189
  skip_if_exists: bool = False,
168
190
  ) -> None: ...
169
191
 
@@ -201,7 +223,7 @@ class Knowledge:
201
223
  metadata: Optional[Dict[str, str]] = None,
202
224
  include: Optional[List[str]] = None,
203
225
  exclude: Optional[List[str]] = None,
204
- upsert: bool = False,
226
+ upsert: bool = True,
205
227
  skip_if_exists: bool = False,
206
228
  reader: Optional[Reader] = None,
207
229
  auth: Optional[ContentAuth] = None,
@@ -268,7 +290,7 @@ class Knowledge:
268
290
  metadata: Optional[Dict[str, str]] = None,
269
291
  include: Optional[List[str]] = None,
270
292
  exclude: Optional[List[str]] = None,
271
- upsert: bool = False,
293
+ upsert: bool = True,
272
294
  skip_if_exists: bool = False,
273
295
  reader: Optional[Reader] = None,
274
296
  auth: Optional[ContentAuth] = None,
@@ -291,7 +313,7 @@ class Knowledge:
291
313
  include: Optional[List[str]] = None,
292
314
  exclude: Optional[List[str]] = None,
293
315
  upsert: bool = True,
294
- skip_if_exists: bool = True,
316
+ skip_if_exists: bool = False,
295
317
  auth: Optional[ContentAuth] = None,
296
318
  ) -> None:
297
319
  """
@@ -342,7 +364,11 @@ class Knowledge:
342
364
  Returns:
343
365
  bool: True if should skip processing, False if should continue
344
366
  """
367
+ from agno.vectordb import VectorDb
368
+
369
+ self.vector_db = cast(VectorDb, self.vector_db)
345
370
  if self.vector_db and self.vector_db.content_hash_exists(content_hash) and skip_if_exists:
371
+ log_debug(f"Content already exists: {content_hash}, skipping...")
346
372
  return True
347
373
 
348
374
  return False
@@ -355,6 +381,10 @@ class Knowledge:
355
381
  include: Optional[List[str]] = None,
356
382
  exclude: Optional[List[str]] = None,
357
383
  ):
384
+ from agno.vectordb import VectorDb
385
+
386
+ self.vector_db = cast(VectorDb, self.vector_db)
387
+
358
388
  log_info(f"Adding content from path, {content.id}, {content.name}, {content.path}, {content.description}")
359
389
  path = Path(content.path) # type: ignore
360
390
 
@@ -451,6 +481,11 @@ class Knowledge:
451
481
  3. Read the content
452
482
  4. Prepare and insert the content in the vector database
453
483
  """
484
+
485
+ from agno.vectordb import VectorDb
486
+
487
+ self.vector_db = cast(VectorDb, self.vector_db)
488
+
454
489
  log_info(f"Adding content from URL {content.url}")
455
490
  content.file_type = "url"
456
491
 
@@ -559,8 +594,12 @@ class Knowledge:
559
594
  self,
560
595
  content: Content,
561
596
  upsert: bool = True,
562
- skip_if_exists: bool = True,
597
+ skip_if_exists: bool = False,
563
598
  ):
599
+ from agno.vectordb import VectorDb
600
+
601
+ self.vector_db = cast(VectorDb, self.vector_db)
602
+
564
603
  if content.name:
565
604
  name = content.name
566
605
  elif content.file_data and content.file_data.content:
@@ -595,10 +634,7 @@ class Knowledge:
595
634
  read_documents = []
596
635
 
597
636
  if isinstance(content.file_data, str):
598
- try:
599
- content_bytes = content.file_data.encode("utf-8")
600
- except UnicodeEncodeError:
601
- content_bytes = content.file_data.encode("latin-1")
637
+ content_bytes = content.file_data.encode("utf-8", errors="replace")
602
638
  content_io = io.BytesIO(content_bytes)
603
639
 
604
640
  if content.reader:
@@ -619,14 +655,7 @@ class Knowledge:
619
655
  if isinstance(content.file_data.content, bytes):
620
656
  content_io = io.BytesIO(content.file_data.content)
621
657
  elif isinstance(content.file_data.content, str):
622
- if self._is_text_mime_type(content.file_data.type):
623
- try:
624
- content_bytes = content.file_data.content.encode("utf-8")
625
- except UnicodeEncodeError:
626
- log_debug(f"UTF-8 encoding failed for {content.file_data.type}, using latin-1")
627
- content_bytes = content.file_data.content.encode("latin-1")
628
- else:
629
- content_bytes = content.file_data.content.encode("latin-1")
658
+ content_bytes = content.file_data.content.encode("utf-8", errors="replace")
630
659
  content_io = io.BytesIO(content_bytes)
631
660
  else:
632
661
  content_io = content.file_data.content # type: ignore
@@ -663,6 +692,9 @@ class Knowledge:
663
692
  upsert: bool,
664
693
  skip_if_exists: bool,
665
694
  ):
695
+ from agno.vectordb import VectorDb
696
+
697
+ self.vector_db = cast(VectorDb, self.vector_db)
666
698
  log_info(f"Adding content from topics: {content.topics}")
667
699
 
668
700
  if content.topics is None:
@@ -896,6 +928,10 @@ class Knowledge:
896
928
  await self._handle_vector_db_insert(content_entry, read_documents, upsert)
897
929
 
898
930
  async def _handle_vector_db_insert(self, content: Content, read_documents, upsert):
931
+ from agno.vectordb import VectorDb
932
+
933
+ self.vector_db = cast(VectorDb, self.vector_db)
934
+
899
935
  if not self.vector_db:
900
936
  log_error("No vector database configured")
901
937
  content.status = ContentStatus.FAILED
@@ -985,6 +1021,48 @@ class Knowledge:
985
1021
  )
986
1022
  return hashlib.sha256(fallback.encode()).hexdigest()
987
1023
 
1024
+ def _ensure_string_field(self, value: Any, field_name: str, default: str = "") -> str:
1025
+ """
1026
+ Safely ensure a field is a string, handling various edge cases.
1027
+
1028
+ Args:
1029
+ value: The value to convert to string
1030
+ field_name: Name of the field for logging purposes
1031
+ default: Default string value if conversion fails
1032
+
1033
+ Returns:
1034
+ str: A safe string value
1035
+ """
1036
+ # Handle None/falsy values
1037
+ if value is None or value == "":
1038
+ return default
1039
+
1040
+ # Handle unexpected list types (the root cause of our Pydantic warning)
1041
+ if isinstance(value, list):
1042
+ if len(value) == 0:
1043
+ log_debug(f"Empty list found for {field_name}, using default: '{default}'")
1044
+ return default
1045
+ elif len(value) == 1:
1046
+ # Single item list, extract the item
1047
+ log_debug(f"Single-item list found for {field_name}, extracting: '{value[0]}'")
1048
+ return str(value[0]) if value[0] is not None else default
1049
+ else:
1050
+ # Multiple items, join them
1051
+ log_debug(f"Multi-item list found for {field_name}, joining: {value}")
1052
+ return " | ".join(str(item) for item in value if item is not None)
1053
+
1054
+ # Handle other unexpected types
1055
+ if not isinstance(value, str):
1056
+ log_debug(f"Non-string type {type(value)} found for {field_name}, converting: '{value}'")
1057
+ try:
1058
+ return str(value)
1059
+ except Exception as e:
1060
+ log_warning(f"Failed to convert {field_name} to string: {e}, using default")
1061
+ return default
1062
+
1063
+ # Already a string, return as-is
1064
+ return value
1065
+
988
1066
  def _add_to_contents_db(self, content: Content):
989
1067
  if self.contents_db:
990
1068
  created_at = content.created_at if content.created_at else int(time.time())
@@ -997,10 +1075,18 @@ class Knowledge:
997
1075
  if content.file_data and content.file_data.type
998
1076
  else None
999
1077
  )
1078
+ # Safely handle string fields with proper type checking
1079
+ safe_name = self._ensure_string_field(content.name, "content.name", default="")
1080
+ safe_description = self._ensure_string_field(content.description, "content.description", default="")
1081
+ safe_linked_to = self._ensure_string_field(self.name, "knowledge.name", default="")
1082
+ safe_status_message = self._ensure_string_field(
1083
+ content.status_message, "content.status_message", default=""
1084
+ )
1085
+
1000
1086
  content_row = KnowledgeRow(
1001
1087
  id=content.id,
1002
- name=content.name if content.name else "",
1003
- description=content.description if content.description else "",
1088
+ name=safe_name,
1089
+ description=safe_description,
1004
1090
  metadata=content.metadata,
1005
1091
  type=file_type,
1006
1092
  size=content.size
@@ -1008,16 +1094,19 @@ class Knowledge:
1008
1094
  else len(content.file_data.content)
1009
1095
  if content.file_data and content.file_data.content
1010
1096
  else None,
1011
- linked_to=self.name,
1097
+ linked_to=safe_linked_to,
1012
1098
  access_count=0,
1013
1099
  status=content.status if content.status else ContentStatus.PROCESSING,
1014
- status_message="",
1100
+ status_message=safe_status_message,
1015
1101
  created_at=created_at,
1016
1102
  updated_at=updated_at,
1017
1103
  )
1018
1104
  self.contents_db.upsert_knowledge_content(knowledge_row=content_row)
1019
1105
 
1020
1106
  def _update_content(self, content: Content) -> Optional[Dict[str, Any]]:
1107
+ from agno.vectordb import VectorDb
1108
+
1109
+ self.vector_db = cast(VectorDb, self.vector_db)
1021
1110
  if self.contents_db:
1022
1111
  if not content.id:
1023
1112
  log_warning("Content id is required to update Knowledge content")
@@ -1029,18 +1118,25 @@ class Knowledge:
1029
1118
  log_warning(f"Content row not found for id: {content.id}, cannot update status")
1030
1119
  return None
1031
1120
 
1121
+ # Apply safe string handling for updates as well
1032
1122
  if content.name is not None:
1033
- content_row.name = content.name
1123
+ content_row.name = self._ensure_string_field(content.name, "content.name", default="")
1034
1124
  if content.description is not None:
1035
- content_row.description = content.description
1125
+ content_row.description = self._ensure_string_field(
1126
+ content.description, "content.description", default=""
1127
+ )
1036
1128
  if content.metadata is not None:
1037
1129
  content_row.metadata = content.metadata
1038
1130
  if content.status is not None:
1039
1131
  content_row.status = content.status
1040
1132
  if content.status_message is not None:
1041
- content_row.status_message = content.status_message if content.status_message else ""
1133
+ content_row.status_message = self._ensure_string_field(
1134
+ content.status_message, "content.status_message", default=""
1135
+ )
1042
1136
  if content.external_id is not None:
1043
- content_row.external_id = content.external_id
1137
+ content_row.external_id = self._ensure_string_field(
1138
+ content.external_id, "content.external_id", default=""
1139
+ )
1044
1140
  content_row.updated_at = int(time.time())
1045
1141
  self.contents_db.upsert_knowledge_content(knowledge_row=content_row)
1046
1142
 
@@ -1053,10 +1149,17 @@ class Knowledge:
1053
1149
  return content_row.to_dict()
1054
1150
 
1055
1151
  else:
1056
- log_warning(f"Contents DB not found for knowledge base: {self.name}")
1152
+ if self.name:
1153
+ log_warning(f"Contents DB not found for knowledge base: {self.name}")
1154
+ else:
1155
+ log_warning("Contents DB not found for knowledge base")
1057
1156
  return None
1058
1157
 
1059
1158
  async def _process_lightrag_content(self, content: Content, content_type: KnowledgeContentOrigin) -> None:
1159
+ from agno.vectordb import VectorDb
1160
+
1161
+ self.vector_db = cast(VectorDb, self.vector_db)
1162
+
1060
1163
  self._add_to_contents_db(content)
1061
1164
  if content_type == KnowledgeContentOrigin.PATH:
1062
1165
  if content.file_data is None:
@@ -1214,6 +1317,9 @@ class Knowledge:
1214
1317
  ) -> List[Document]:
1215
1318
  """Returns relevant documents matching a query"""
1216
1319
 
1320
+ from agno.vectordb import VectorDb
1321
+
1322
+ self.vector_db = cast(VectorDb, self.vector_db)
1217
1323
  try:
1218
1324
  if self.vector_db is None:
1219
1325
  log_warning("No vector db provided")
@@ -1231,6 +1337,9 @@ class Knowledge:
1231
1337
  ) -> List[Document]:
1232
1338
  """Returns relevant documents matching a query"""
1233
1339
 
1340
+ from agno.vectordb import VectorDb
1341
+
1342
+ self.vector_db = cast(VectorDb, self.vector_db)
1234
1343
  try:
1235
1344
  if self.vector_db is None:
1236
1345
  log_warning("No vector db provided")
@@ -1295,18 +1404,27 @@ class Knowledge:
1295
1404
  return valid_filters
1296
1405
 
1297
1406
  def remove_vector_by_id(self, id: str) -> bool:
1407
+ from agno.vectordb import VectorDb
1408
+
1409
+ self.vector_db = cast(VectorDb, self.vector_db)
1298
1410
  if self.vector_db is None:
1299
1411
  log_warning("No vector DB provided")
1300
1412
  return False
1301
1413
  return self.vector_db.delete_by_id(id)
1302
1414
 
1303
1415
  def remove_vectors_by_name(self, name: str) -> bool:
1416
+ from agno.vectordb import VectorDb
1417
+
1418
+ self.vector_db = cast(VectorDb, self.vector_db)
1304
1419
  if self.vector_db is None:
1305
1420
  log_warning("No vector DB provided")
1306
1421
  return False
1307
1422
  return self.vector_db.delete_by_name(name)
1308
1423
 
1309
1424
  def remove_vectors_by_metadata(self, metadata: Dict[str, Any]) -> bool:
1425
+ from agno.vectordb import VectorDb
1426
+
1427
+ self.vector_db = cast(VectorDb, self.vector_db)
1310
1428
  if self.vector_db is None:
1311
1429
  log_warning("No vector DB provided")
1312
1430
  return False
@@ -1393,6 +1511,9 @@ class Knowledge:
1393
1511
  return status, content_row.status_message
1394
1512
 
1395
1513
  def remove_content_by_id(self, content_id: str):
1514
+ from agno.vectordb import VectorDb
1515
+
1516
+ self.vector_db = cast(VectorDb, self.vector_db)
1396
1517
  if self.vector_db is not None:
1397
1518
  if self.vector_db.__class__.__name__ == "LightRag":
1398
1519
  # For LightRAG, get the content first to find the external_id
agno/knowledge/types.py CHANGED
@@ -1,4 +1,7 @@
1
1
  from enum import Enum
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel
2
5
 
3
6
 
4
7
  class ContentType(str, Enum):
@@ -28,3 +31,8 @@ class ContentType(str, Enum):
28
31
  def get_content_type_enum(content_type_str: str) -> ContentType:
29
32
  """Convert a content type string to ContentType enum."""
30
33
  return ContentType(content_type_str)
34
+
35
+
36
+ class KnowledgeFilter(BaseModel):
37
+ key: str
38
+ value: Any
agno/media.py CHANGED
@@ -371,6 +371,8 @@ class File(BaseModel):
371
371
  "application/pdf",
372
372
  "application/json",
373
373
  "application/x-javascript",
374
+ "application/json",
375
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
374
376
  "text/javascript",
375
377
  "application/x-python",
376
378
  "text/x-python",
agno/models/base.py CHANGED
@@ -196,6 +196,7 @@ class Model(ABC):
196
196
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
197
197
  tool_call_limit: Optional[int] = None,
198
198
  run_response: Optional[RunOutput] = None,
199
+ send_media_to_model: bool = True,
199
200
  ) -> ModelResponse:
200
201
  """
201
202
  Generate a response from the model.
@@ -301,7 +302,11 @@ class Model(ABC):
301
302
 
302
303
  if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
303
304
  # Handle function call media
304
- 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
+ )
305
310
 
306
311
  for function_call_result in function_call_results:
307
312
  function_call_result.log(metrics=True)
@@ -339,6 +344,7 @@ class Model(ABC):
339
344
  functions: Optional[Dict[str, Function]] = None,
340
345
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
341
346
  tool_call_limit: Optional[int] = None,
347
+ send_media_to_model: bool = True,
342
348
  ) -> ModelResponse:
343
349
  """
344
350
  Generate an asynchronous response from the model.
@@ -441,7 +447,11 @@ class Model(ABC):
441
447
 
442
448
  if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
443
449
  # Handle function call media
444
- 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
+ )
445
455
 
446
456
  for function_call_result in function_call_results:
447
457
  function_call_result.log(metrics=True)
@@ -689,6 +699,7 @@ class Model(ABC):
689
699
  tool_call_limit: Optional[int] = None,
690
700
  stream_model_response: bool = True,
691
701
  run_response: Optional[RunOutput] = None,
702
+ send_media_to_model: bool = True,
692
703
  ) -> Iterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
693
704
  """
694
705
  Generate a streaming response from the model.
@@ -778,7 +789,11 @@ class Model(ABC):
778
789
 
779
790
  # Handle function call media
780
791
  if any(msg.images or msg.videos or msg.audio for msg in function_call_results):
781
- 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
+ )
782
797
 
783
798
  for function_call_result in function_call_results:
784
799
  function_call_result.log(metrics=True)
@@ -848,6 +863,7 @@ class Model(ABC):
848
863
  tool_call_limit: Optional[int] = None,
849
864
  stream_model_response: bool = True,
850
865
  run_response: Optional[RunOutput] = None,
866
+ send_media_to_model: bool = True,
851
867
  ) -> AsyncIterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
852
868
  """
853
869
  Generate an asynchronous streaming response from the model.
@@ -937,7 +953,11 @@ class Model(ABC):
937
953
 
938
954
  # Handle function call media
939
955
  if any(msg.images or msg.videos or msg.audio for msg in function_call_results):
940
- 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
+ )
941
961
 
942
962
  for function_call_result in function_call_results:
943
963
  function_call_result.log(metrics=True)
@@ -1041,7 +1061,13 @@ class Model(ABC):
1041
1061
  if model_response_delta.extra is not None:
1042
1062
  if stream_data.extra is None:
1043
1063
  stream_data.extra = {}
1044
- 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]
1045
1071
 
1046
1072
  if should_yield:
1047
1073
  yield model_response_delta
@@ -1708,7 +1734,9 @@ class Model(ABC):
1708
1734
  if len(function_call_results) > 0:
1709
1735
  messages.extend(function_call_results)
1710
1736
 
1711
- 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:
1712
1740
  """
1713
1741
  Handle media artifacts from function calls by adding follow-up user messages for generated media if needed.
1714
1742
  """
@@ -1739,9 +1767,10 @@ class Model(ABC):
1739
1767
  all_files.extend(result_message.files)
1740
1768
  result_message.files = None
1741
1769
 
1742
- # If we have media artifacts, add a follow-up "user" message instead of a "tool"
1743
- # message with the media artifacts which throws error for some models
1744
- if all_images or all_videos or all_audio or all_files:
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
1745
1774
  media_message = Message(
1746
1775
  role="user",
1747
1776
  content="Take note of the following content",
@@ -0,0 +1,5 @@
1
+ from agno.models.cometapi.cometapi import CometAPI
2
+
3
+ __all__ = [
4
+ "CometAPI",
5
+ ]
@@ -0,0 +1,57 @@
1
+ from dataclasses import dataclass, field
2
+ from os import getenv
3
+ from typing import List, Optional
4
+
5
+ import httpx
6
+
7
+ from agno.models.openai.like import OpenAILike
8
+ from agno.utils.log import log_debug
9
+
10
+
11
+ @dataclass
12
+ class CometAPI(OpenAILike):
13
+ """
14
+ The CometAPI class provides access to multiple AI model providers
15
+ (GPT, Claude, Gemini, DeepSeek, etc.) through OpenAI-compatible endpoints.
16
+
17
+ Args:
18
+ id (str): The id of the CometAPI model to use. Default is "gpt-5-mini".
19
+ name (str): The name for this model. Defaults to "CometAPI".
20
+ api_key (str): The API key for CometAPI. Defaults to COMETAPI_KEY environment variable.
21
+ base_url (str): The base URL for CometAPI. Defaults to "https://api.cometapi.com/v1".
22
+ """
23
+
24
+ name: str = "CometAPI"
25
+ id: str = "gpt-5-mini"
26
+ api_key: Optional[str] = field(default_factory=lambda: getenv("COMETAPI_KEY"))
27
+ base_url: str = "https://api.cometapi.com/v1"
28
+
29
+ def get_available_models(self) -> List[str]:
30
+ """
31
+ Fetch available chat models from CometAPI, filtering out non-chat models.
32
+
33
+ Returns:
34
+ List of available chat model IDs
35
+ """
36
+ if not self.api_key:
37
+ log_debug("No API key provided, returning empty model list")
38
+ return []
39
+
40
+ try:
41
+ with httpx.Client() as client:
42
+ response = client.get(
43
+ f"{self.base_url}/models",
44
+ headers={"Authorization": f"Bearer {self.api_key}", "Accept": "application/json"},
45
+ timeout=30.0,
46
+ )
47
+ response.raise_for_status()
48
+
49
+ data = response.json()
50
+ all_models = data.get("data", [])
51
+
52
+ log_debug(f"Found {len(all_models)} total models")
53
+ return sorted(all_models)
54
+
55
+ except Exception as e:
56
+ log_debug(f"Error fetching models from CometAPI: {e}")
57
+ return []
@@ -16,9 +16,8 @@ from agno.models.message import Citations, Message, UrlCitation
16
16
  from agno.models.metrics import Metrics
17
17
  from agno.models.response import ModelResponse
18
18
  from agno.run.agent import RunOutput
19
- from agno.utils.gemini import convert_schema, format_function_definitions, format_image_for_message
19
+ from agno.utils.gemini import format_function_definitions, format_image_for_message, prepare_response_schema
20
20
  from agno.utils.log import log_debug, log_error, log_info, log_warning
21
- from agno.utils.models.schema_utils import get_response_schema_for_provider
22
21
 
23
22
  try:
24
23
  from google import genai
@@ -191,12 +190,9 @@ class Gemini(Model):
191
190
 
192
191
  if response_format is not None and isinstance(response_format, type) and issubclass(response_format, BaseModel):
193
192
  config["response_mime_type"] = "application/json" # type: ignore
194
- # Convert Pydantic model to JSON schema, then normalize for Gemini, then convert to Gemini schema format
195
-
196
- # Get the normalized schema for Gemini
197
- normalized_schema = get_response_schema_for_provider(response_format, "gemini")
198
- gemini_schema = convert_schema(normalized_schema)
199
- config["response_schema"] = gemini_schema
193
+ # Convert Pydantic model using our hybrid approach
194
+ # This will handle complex schemas with nested models, dicts, and circular refs
195
+ config["response_schema"] = prepare_response_schema(response_format)
200
196
 
201
197
  # Add thinking configuration
202
198
  thinking_config_params = {}
@@ -0,0 +1,5 @@
1
+ from agno.models.llama_cpp.llama_cpp import LlamaCpp
2
+
3
+ __all__ = [
4
+ "LlamaCpp",
5
+ ]