agno 2.0.11__py3-none-any.whl → 2.1.1__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 (93) hide show
  1. agno/agent/agent.py +607 -176
  2. agno/db/in_memory/in_memory_db.py +42 -29
  3. agno/db/mongo/mongo.py +65 -66
  4. agno/db/postgres/postgres.py +6 -4
  5. agno/db/utils.py +50 -22
  6. agno/exceptions.py +62 -1
  7. agno/guardrails/__init__.py +6 -0
  8. agno/guardrails/base.py +19 -0
  9. agno/guardrails/openai.py +144 -0
  10. agno/guardrails/pii.py +94 -0
  11. agno/guardrails/prompt_injection.py +51 -0
  12. agno/knowledge/embedder/aws_bedrock.py +9 -4
  13. agno/knowledge/embedder/azure_openai.py +54 -0
  14. agno/knowledge/embedder/base.py +2 -0
  15. agno/knowledge/embedder/cohere.py +184 -5
  16. agno/knowledge/embedder/google.py +79 -1
  17. agno/knowledge/embedder/huggingface.py +9 -4
  18. agno/knowledge/embedder/jina.py +63 -0
  19. agno/knowledge/embedder/mistral.py +78 -11
  20. agno/knowledge/embedder/ollama.py +5 -0
  21. agno/knowledge/embedder/openai.py +18 -54
  22. agno/knowledge/embedder/voyageai.py +69 -16
  23. agno/knowledge/knowledge.py +11 -4
  24. agno/knowledge/reader/pdf_reader.py +4 -3
  25. agno/knowledge/reader/website_reader.py +3 -2
  26. agno/models/base.py +125 -32
  27. agno/models/cerebras/cerebras.py +1 -0
  28. agno/models/cerebras/cerebras_openai.py +1 -0
  29. agno/models/dashscope/dashscope.py +1 -0
  30. agno/models/google/gemini.py +27 -5
  31. agno/models/openai/chat.py +13 -4
  32. agno/models/openai/responses.py +1 -1
  33. agno/models/perplexity/perplexity.py +2 -3
  34. agno/models/requesty/__init__.py +5 -0
  35. agno/models/requesty/requesty.py +49 -0
  36. agno/models/vllm/vllm.py +1 -0
  37. agno/models/xai/xai.py +1 -0
  38. agno/os/app.py +98 -126
  39. agno/os/interfaces/__init__.py +1 -0
  40. agno/os/interfaces/agui/agui.py +21 -5
  41. agno/os/interfaces/base.py +4 -2
  42. agno/os/interfaces/slack/slack.py +13 -8
  43. agno/os/interfaces/whatsapp/router.py +2 -0
  44. agno/os/interfaces/whatsapp/whatsapp.py +12 -5
  45. agno/os/mcp.py +2 -2
  46. agno/os/middleware/__init__.py +7 -0
  47. agno/os/middleware/jwt.py +233 -0
  48. agno/os/router.py +182 -46
  49. agno/os/routers/home.py +2 -2
  50. agno/os/routers/memory/memory.py +23 -1
  51. agno/os/routers/memory/schemas.py +1 -1
  52. agno/os/routers/session/session.py +20 -3
  53. agno/os/utils.py +74 -8
  54. agno/run/agent.py +120 -77
  55. agno/run/base.py +2 -13
  56. agno/run/team.py +115 -72
  57. agno/run/workflow.py +5 -15
  58. agno/session/summary.py +9 -10
  59. agno/session/team.py +2 -1
  60. agno/team/team.py +721 -169
  61. agno/tools/firecrawl.py +4 -4
  62. agno/tools/function.py +42 -2
  63. agno/tools/knowledge.py +3 -3
  64. agno/tools/searxng.py +2 -2
  65. agno/tools/serper.py +2 -2
  66. agno/tools/spider.py +2 -2
  67. agno/tools/workflow.py +4 -5
  68. agno/utils/events.py +66 -1
  69. agno/utils/hooks.py +57 -0
  70. agno/utils/media.py +11 -9
  71. agno/utils/print_response/agent.py +43 -5
  72. agno/utils/print_response/team.py +48 -12
  73. agno/utils/serialize.py +32 -0
  74. agno/vectordb/cassandra/cassandra.py +44 -4
  75. agno/vectordb/chroma/chromadb.py +79 -8
  76. agno/vectordb/clickhouse/clickhousedb.py +43 -6
  77. agno/vectordb/couchbase/couchbase.py +76 -5
  78. agno/vectordb/lancedb/lance_db.py +38 -3
  79. agno/vectordb/milvus/milvus.py +76 -4
  80. agno/vectordb/mongodb/mongodb.py +76 -4
  81. agno/vectordb/pgvector/pgvector.py +50 -6
  82. agno/vectordb/pineconedb/pineconedb.py +39 -2
  83. agno/vectordb/qdrant/qdrant.py +76 -26
  84. agno/vectordb/singlestore/singlestore.py +77 -4
  85. agno/vectordb/upstashdb/upstashdb.py +42 -2
  86. agno/vectordb/weaviate/weaviate.py +39 -3
  87. agno/workflow/types.py +5 -6
  88. agno/workflow/workflow.py +58 -2
  89. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/METADATA +4 -3
  90. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/RECORD +93 -82
  91. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/WHEEL +0 -0
  92. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/licenses/LICENSE +0 -0
  93. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/top_level.txt +0 -0
@@ -140,66 +140,24 @@ class OpenAIEmbedder(Embedder):
140
140
  logger.warning(e)
141
141
  return [], None
142
142
 
143
- def get_embeddings_batch(self, texts: List[str], batch_size: int = 100) -> List[List[float]]:
143
+ async def async_get_embeddings_batch_and_usage(
144
+ self, texts: List[str]
145
+ ) -> Tuple[List[List[float]], List[Optional[Dict]]]:
144
146
  """
145
- Get embeddings for multiple texts in batches.
147
+ Get embeddings and usage for multiple texts in batches (async version).
146
148
 
147
149
  Args:
148
150
  texts: List of text strings to embed
149
- batch_size: Number of texts to process in each API call (max ~2048)
150
151
 
151
152
  Returns:
152
- List of embedding vectors
153
+ Tuple of (List of embedding vectors, List of usage dictionaries)
153
154
  """
154
155
  all_embeddings = []
156
+ all_usage = []
157
+ logger.info(f"Getting embeddings and usage for {len(texts)} texts in batches of {self.batch_size} (async)")
155
158
 
156
- for i in range(0, len(texts), batch_size):
157
- batch_texts = texts[i : i + batch_size]
158
-
159
- req: Dict[str, Any] = {
160
- "input": batch_texts,
161
- "model": self.id,
162
- "encoding_format": self.encoding_format,
163
- }
164
- if self.user is not None:
165
- req["user"] = self.user
166
- if self.id.startswith("text-embedding-3"):
167
- req["dimensions"] = self.dimensions
168
- if self.request_params:
169
- req.update(self.request_params)
170
-
171
- try:
172
- response: CreateEmbeddingResponse = self.client.embeddings.create(**req)
173
- batch_embeddings = [data.embedding for data in response.data]
174
- all_embeddings.extend(batch_embeddings)
175
- except Exception as e:
176
- logger.warning(f"Error in batch embedding: {e}")
177
- # Fallback to individual calls for this batch
178
- for text in batch_texts:
179
- try:
180
- embedding = self.get_embedding(text)
181
- all_embeddings.append(embedding)
182
- except Exception as e2:
183
- logger.warning(f"Error in individual embedding fallback: {e2}")
184
- all_embeddings.append([])
185
-
186
- return all_embeddings
187
-
188
- async def async_get_embeddings_batch(self, texts: List[str], batch_size: int = 100) -> List[List[float]]:
189
- """
190
- Get embeddings for multiple texts in batches (async version).
191
-
192
- Args:
193
- texts: List of text strings to embed
194
- batch_size: Number of texts to process in each API call (max ~2048)
195
-
196
- Returns:
197
- List of embedding vectors
198
- """
199
- all_embeddings = []
200
-
201
- for i in range(0, len(texts), batch_size):
202
- batch_texts = texts[i : i + batch_size]
159
+ for i in range(0, len(texts), self.batch_size):
160
+ batch_texts = texts[i : i + self.batch_size]
203
161
 
204
162
  req: Dict[str, Any] = {
205
163
  "input": batch_texts,
@@ -217,15 +175,21 @@ class OpenAIEmbedder(Embedder):
217
175
  response: CreateEmbeddingResponse = await self.aclient.embeddings.create(**req)
218
176
  batch_embeddings = [data.embedding for data in response.data]
219
177
  all_embeddings.extend(batch_embeddings)
178
+
179
+ # For each embedding in the batch, add the same usage information
180
+ usage_dict = response.usage.model_dump() if response.usage else None
181
+ all_usage.extend([usage_dict] * len(batch_embeddings))
220
182
  except Exception as e:
221
183
  logger.warning(f"Error in async batch embedding: {e}")
222
- # Fallback to individual async calls for this batch
184
+ # Fallback to individual calls for this batch
223
185
  for text in batch_texts:
224
186
  try:
225
- embedding = await self.async_get_embedding(text)
187
+ embedding, usage = await self.async_get_embedding_and_usage(text)
226
188
  all_embeddings.append(embedding)
189
+ all_usage.append(usage)
227
190
  except Exception as e2:
228
191
  logger.warning(f"Error in individual async embedding fallback: {e2}")
229
192
  all_embeddings.append([])
193
+ all_usage.append(None)
230
194
 
231
- return all_embeddings
195
+ return all_embeddings, all_usage
@@ -30,12 +30,13 @@ class VoyageAIEmbedder(Embedder):
30
30
  if self.voyage_client:
31
31
  return self.voyage_client
32
32
 
33
- _client_params = {
34
- "api_key": self.api_key,
35
- "max_retries": self.max_retries,
36
- "timeout": self.timeout,
37
- }
38
- _client_params = {k: v for k, v in _client_params.items() if v is not None}
33
+ _client_params: Dict[str, Any] = {}
34
+ if self.api_key is not None:
35
+ _client_params["api_key"] = self.api_key
36
+ if self.max_retries is not None:
37
+ _client_params["max_retries"] = self.max_retries
38
+ if self.timeout is not None:
39
+ _client_params["timeout"] = self.timeout
39
40
  if self.client_params:
40
41
  _client_params.update(self.client_params)
41
42
  self.voyage_client = VoyageClient(**_client_params)
@@ -46,12 +47,13 @@ class VoyageAIEmbedder(Embedder):
46
47
  if self.async_client:
47
48
  return self.async_client
48
49
 
49
- _client_params = {
50
- "api_key": self.api_key,
51
- "max_retries": self.max_retries,
52
- "timeout": self.timeout,
53
- }
54
- _client_params = {k: v for k, v in _client_params.items() if v is not None}
50
+ _client_params: Dict[str, Any] = {}
51
+ if self.api_key is not None:
52
+ _client_params["api_key"] = self.api_key
53
+ if self.max_retries is not None:
54
+ _client_params["max_retries"] = self.max_retries
55
+ if self.timeout is not None:
56
+ _client_params["timeout"] = self.timeout
55
57
  if self.client_params:
56
58
  _client_params.update(self.client_params)
57
59
  self.async_client = AsyncVoyageClient(**_client_params)
@@ -69,7 +71,8 @@ class VoyageAIEmbedder(Embedder):
69
71
  def get_embedding(self, text: str) -> List[float]:
70
72
  response: EmbeddingsObject = self._response(text=text)
71
73
  try:
72
- return response.embeddings[0]
74
+ embedding = response.embeddings[0]
75
+ return [float(x) for x in embedding] # Ensure all values are float
73
76
  except Exception as e:
74
77
  logger.warning(e)
75
78
  return []
@@ -79,7 +82,7 @@ class VoyageAIEmbedder(Embedder):
79
82
 
80
83
  embedding = response.embeddings[0]
81
84
  usage = {"total_tokens": response.total_tokens}
82
- return embedding, usage
85
+ return [float(x) for x in embedding], usage
83
86
 
84
87
  async def _async_response(self, text: str) -> EmbeddingsObject:
85
88
  """Async version of _response using AsyncVoyageClient."""
@@ -95,7 +98,8 @@ class VoyageAIEmbedder(Embedder):
95
98
  """Async version of get_embedding."""
96
99
  try:
97
100
  response: EmbeddingsObject = await self._async_response(text=text)
98
- return response.embeddings[0]
101
+ embedding = response.embeddings[0]
102
+ return [float(x) for x in embedding] # Ensure all values are float
99
103
  except Exception as e:
100
104
  logger.warning(f"Error getting embedding: {e}")
101
105
  return []
@@ -106,7 +110,56 @@ class VoyageAIEmbedder(Embedder):
106
110
  response: EmbeddingsObject = await self._async_response(text=text)
107
111
  embedding = response.embeddings[0]
108
112
  usage = {"total_tokens": response.total_tokens}
109
- return embedding, usage
113
+ return [float(x) for x in embedding], usage
110
114
  except Exception as e:
111
115
  logger.warning(f"Error getting embedding and usage: {e}")
112
116
  return [], None
117
+
118
+ async def async_get_embeddings_batch_and_usage(
119
+ self, texts: List[str]
120
+ ) -> Tuple[List[List[float]], List[Optional[Dict]]]:
121
+ """
122
+ Get embeddings and usage for multiple texts in batches.
123
+
124
+ Args:
125
+ texts: List of text strings to embed
126
+
127
+ Returns:
128
+ Tuple of (List of embedding vectors, List of usage dictionaries)
129
+ """
130
+ all_embeddings: List[List[float]] = []
131
+ all_usage: List[Optional[Dict]] = []
132
+ logger.info(f"Getting embeddings and usage for {len(texts)} texts in batches of {self.batch_size}")
133
+
134
+ for i in range(0, len(texts), self.batch_size):
135
+ batch_texts = texts[i : i + self.batch_size]
136
+
137
+ req: Dict[str, Any] = {
138
+ "texts": batch_texts,
139
+ "model": self.id,
140
+ }
141
+ if self.request_params:
142
+ req.update(self.request_params)
143
+
144
+ try:
145
+ response: EmbeddingsObject = await self.aclient.embed(**req)
146
+ batch_embeddings = [[float(x) for x in emb] for emb in response.embeddings]
147
+ all_embeddings.extend(batch_embeddings)
148
+
149
+ # For each embedding in the batch, add the same usage information
150
+ usage_dict = {"total_tokens": response.total_tokens}
151
+ all_usage.extend([usage_dict] * len(batch_embeddings))
152
+ except Exception as e:
153
+ logger.warning(f"Error in async batch embedding: {e}")
154
+ # Fallback to individual calls for this batch
155
+ for text in batch_texts:
156
+ try:
157
+ embedding, usage = await self.async_get_embedding_and_usage(text)
158
+ all_embeddings.append(embedding)
159
+ all_usage.append(usage)
160
+ except Exception as e2:
161
+ logger.warning(f"Error in individual async embedding fallback: {e2}")
162
+ all_embeddings.append([])
163
+ all_usage.append(None)
164
+
165
+ return all_embeddings, all_usage
@@ -89,7 +89,7 @@ class Knowledge:
89
89
  url=argument.get("url"),
90
90
  metadata=argument.get("metadata"),
91
91
  topics=argument.get("topics"),
92
- text_contents=argument.get("text_contents"),
92
+ text_content=argument.get("text_content"),
93
93
  reader=argument.get("reader"),
94
94
  include=argument.get("include"),
95
95
  exclude=argument.get("exclude"),
@@ -251,7 +251,9 @@ class Knowledge:
251
251
  ) -> None:
252
252
  # Validation: At least one of the parameters must be provided
253
253
  if all(argument is None for argument in [path, url, text_content, topics, remote_content]):
254
- log_info("At least one of 'path', 'url', 'text_content', 'topics', or 'remote_content' must be provided.")
254
+ log_warning(
255
+ "At least one of 'path', 'url', 'text_content', 'topics', or 'remote_content' must be provided."
256
+ )
255
257
  return
256
258
 
257
259
  if not skip_if_exists:
@@ -534,7 +536,6 @@ class Knowledge:
534
536
  reader = content.reader
535
537
  name = content.name if content.name else content.url
536
538
  # Else select based on file extension
537
-
538
539
  if reader is None:
539
540
  if file_extension == ".csv":
540
541
  name = basename(parsed_url.path) or "data.csv"
@@ -570,6 +571,7 @@ class Knowledge:
570
571
  read_documents = reader.read(bytes_content, name=name)
571
572
  else:
572
573
  read_documents = reader.read(content.url, name=name)
574
+
573
575
  except Exception as e:
574
576
  log_error(f"Error reading URL: {content.url} - {str(e)}")
575
577
  content.status = ContentStatus.FAILED
@@ -580,7 +582,6 @@ class Knowledge:
580
582
  # 6. Chunk documents if needed
581
583
  if reader and not reader.chunk:
582
584
  read_documents = await reader.chunk_documents_async(read_documents)
583
-
584
585
  # 7. Prepare and insert the content in the vector database
585
586
  file_size = 0
586
587
  if read_documents:
@@ -1356,6 +1357,12 @@ class Knowledge:
1356
1357
  log_error(f"Error searching for documents: {e}")
1357
1358
  return []
1358
1359
 
1360
+ def get_valid_filters(self) -> Set[str]:
1361
+ if self.valid_metadata_filters is None:
1362
+ self.valid_metadata_filters = set()
1363
+ self.valid_metadata_filters.update(self._get_filters_from_db)
1364
+ return self.valid_metadata_filters
1365
+
1359
1366
  def validate_filters(self, filters: Optional[Dict[str, Any]]) -> Tuple[Dict[str, Any], List[str]]:
1360
1367
  if self.valid_metadata_filters is None:
1361
1368
  self.valid_metadata_filters = set()
@@ -117,6 +117,10 @@ def _clean_page_numbers(
117
117
  page_numbers = [find_page_number(content) for content in page_content_list]
118
118
  if all(x is None or x > 5 for x in page_numbers):
119
119
  # This approach won't work reliably for higher page numbers.
120
+ page_content_list = [
121
+ f"\n{page_content_list[i]}\n{extra_content[i]}" if extra_content else page_content_list[i]
122
+ for i in range(len(page_content_list))
123
+ ]
120
124
  return page_content_list, None
121
125
 
122
126
  # Possible range shifts to detect page numbering
@@ -261,7 +265,6 @@ class BasePDFReader(Reader):
261
265
 
262
266
  if self.chunk:
263
267
  return self._build_chunked_documents(documents)
264
-
265
268
  return documents
266
269
 
267
270
  def _pdf_reader_to_documents(
@@ -339,8 +342,6 @@ class PDFReader(BasePDFReader):
339
342
  except Exception:
340
343
  doc_name = "pdf"
341
344
 
342
- log_info(f"Reading: {doc_name}")
343
-
344
345
  try:
345
346
  DocumentReader(pdf)
346
347
  except PdfStreamError as e:
@@ -112,7 +112,8 @@ class WebsiteReader(Reader):
112
112
  if tag.name in ["article", "main", "section"]:
113
113
  return True
114
114
 
115
- classes = tag.get("class", [])
115
+ classes_attr = tag.get("class")
116
+ classes: List[str] = classes_attr if isinstance(classes_attr, list) else []
116
117
  content_classes = ["content", "main-content", "post-content", "entry-content", "article-body"]
117
118
  if any(cls in content_classes for cls in classes):
118
119
  return True
@@ -126,7 +127,7 @@ class WebsiteReader(Reader):
126
127
 
127
128
  # Try to find main content element
128
129
  element = soup.find(match)
129
- if element:
130
+ if element and hasattr(element, "find_all"):
130
131
  # Remove common unwanted elements from the found content
131
132
  for unwanted in element.find_all(["script", "style", "nav", "header", "footer"]):
132
133
  unwanted.decompose()
agno/models/base.py CHANGED
@@ -715,6 +715,7 @@ class Model(ABC):
715
715
  assistant_message = Message(role=self.assistant_message_role)
716
716
  # Create assistant message and stream data
717
717
  stream_data = MessageData()
718
+ model_response = ModelResponse()
718
719
  if stream_model_response:
719
720
  # Generate response
720
721
  yield from self.process_response_stream(
@@ -744,7 +745,6 @@ class Model(ABC):
744
745
  assistant_message.tool_calls = self.parse_tool_calls(stream_data.response_tool_calls)
745
746
 
746
747
  else:
747
- model_response = ModelResponse()
748
748
  self._process_model_response(
749
749
  messages=messages,
750
750
  assistant_message=assistant_message,
@@ -784,6 +784,10 @@ class Model(ABC):
784
784
  self.format_function_call_results(
785
785
  messages=messages, function_call_results=function_call_results, **stream_data.extra
786
786
  )
787
+ elif model_response and model_response.extra is not None:
788
+ self.format_function_call_results(
789
+ messages=messages, function_call_results=function_call_results, **model_response.extra
790
+ )
787
791
  else:
788
792
  self.format_function_call_results(messages=messages, function_call_results=function_call_results)
789
793
 
@@ -879,9 +883,10 @@ class Model(ABC):
879
883
  # Create assistant message and stream data
880
884
  assistant_message = Message(role=self.assistant_message_role)
881
885
  stream_data = MessageData()
886
+ model_response = ModelResponse()
882
887
  if stream_model_response:
883
888
  # Generate response
884
- async for response in self.aprocess_response_stream(
889
+ async for model_response in self.aprocess_response_stream(
885
890
  messages=messages,
886
891
  assistant_message=assistant_message,
887
892
  stream_data=stream_data,
@@ -890,7 +895,7 @@ class Model(ABC):
890
895
  tool_choice=tool_choice or self._tool_choice,
891
896
  run_response=run_response,
892
897
  ):
893
- yield response
898
+ yield model_response
894
899
 
895
900
  # Populate assistant message from stream data
896
901
  if stream_data.response_content:
@@ -907,7 +912,6 @@ class Model(ABC):
907
912
  assistant_message.tool_calls = self.parse_tool_calls(stream_data.response_tool_calls)
908
913
 
909
914
  else:
910
- model_response = ModelResponse()
911
915
  await self._aprocess_model_response(
912
916
  messages=messages,
913
917
  assistant_message=assistant_message,
@@ -948,6 +952,10 @@ class Model(ABC):
948
952
  self.format_function_call_results(
949
953
  messages=messages, function_call_results=function_call_results, **stream_data.extra
950
954
  )
955
+ elif model_response and model_response.extra is not None:
956
+ self.format_function_call_results(
957
+ messages=messages, function_call_results=function_call_results, **model_response.extra or {}
958
+ )
951
959
  else:
952
960
  self.format_function_call_results(messages=messages, function_call_results=function_call_results)
953
961
 
@@ -1573,30 +1581,35 @@ class Model(ABC):
1573
1581
  *(self.arun_function_call(fc) for fc in function_calls_to_run), return_exceptions=True
1574
1582
  )
1575
1583
 
1576
- # Process results
1584
+ # Separate async generators from other results for concurrent processing
1585
+ async_generator_results: List[Any] = []
1586
+ non_async_generator_results: List[Any] = []
1587
+
1577
1588
  for result in results:
1578
- # If result is an exception, skip processing it
1579
1589
  if isinstance(result, BaseException):
1580
- log_error(f"Error during function call: {result}")
1581
- raise result
1590
+ non_async_generator_results.append(result)
1591
+ continue
1582
1592
 
1583
- # Unpack result
1584
1593
  function_call_success, function_call_timer, function_call, function_execution_result = result
1585
1594
 
1586
- updated_session_state = function_execution_result.updated_session_state
1595
+ # Check if this result contains an async generator
1596
+ if isinstance(function_call.result, (AsyncGeneratorType, AsyncIterator)):
1597
+ async_generator_results.append(result)
1598
+ else:
1599
+ non_async_generator_results.append(result)
1587
1600
 
1588
- # Handle AgentRunException
1589
- if isinstance(function_call_success, AgentRunException):
1590
- a_exc = function_call_success
1591
- # Update additional messages from function call
1592
- _handle_agent_exception(a_exc, additional_input)
1593
- # Set function call success to False if an exception occurred
1594
- function_call_success = False
1601
+ # Process async generators with real-time event streaming using asyncio.Queue
1602
+ async_generator_outputs: Dict[int, Tuple[Any, str, Optional[BaseException]]] = {}
1603
+ event_queue: asyncio.Queue = asyncio.Queue()
1604
+ active_generators_count: int = len(async_generator_results)
1595
1605
 
1596
- # Process function call output
1597
- function_call_output: str = ""
1598
- if isinstance(function_call.result, (GeneratorType, collections.abc.Iterator)):
1599
- for item in function_call.result:
1606
+ # Create background tasks for each async generator
1607
+ async def process_async_generator(result, generator_id):
1608
+ function_call_success, function_call_timer, function_call, function_execution_result = result
1609
+ function_call_output = ""
1610
+
1611
+ try:
1612
+ async for item in function_call.result:
1600
1613
  # This function yields agent/team run events
1601
1614
  if isinstance(item, tuple(get_args(RunOutputEvent))) or isinstance(
1602
1615
  item, tuple(get_args(TeamRunOutputEvent))
@@ -1610,17 +1623,102 @@ class Model(ABC):
1610
1623
  function_call_output += item.content or ""
1611
1624
 
1612
1625
  if function_call.function.show_result:
1613
- yield ModelResponse(content=item.content)
1626
+ await event_queue.put(ModelResponse(content=item.content))
1614
1627
  continue
1615
1628
 
1616
- # Yield the event itself to bubble it up
1617
- yield item
1629
+ if isinstance(item, CustomEvent):
1630
+ function_call_output += str(item)
1631
+
1632
+ # Put the event into the queue to be yielded
1633
+ await event_queue.put(item)
1634
+
1635
+ # Yield custom events emitted by the tool
1618
1636
  else:
1619
1637
  function_call_output += str(item)
1620
1638
  if function_call.function.show_result:
1621
- yield ModelResponse(content=str(item))
1622
- elif isinstance(function_call.result, (AsyncGeneratorType, collections.abc.AsyncIterator)):
1623
- async for item in function_call.result:
1639
+ await event_queue.put(ModelResponse(content=str(item)))
1640
+
1641
+ # Store the final output for this generator
1642
+ async_generator_outputs[generator_id] = (result, function_call_output, None)
1643
+
1644
+ except Exception as e:
1645
+ # Store the exception
1646
+ async_generator_outputs[generator_id] = (result, "", e)
1647
+
1648
+ # Signal that this generator is done
1649
+ await event_queue.put(("GENERATOR_DONE", generator_id))
1650
+
1651
+ # Start all async generator tasks
1652
+ generator_tasks = []
1653
+ for i, result in enumerate(async_generator_results):
1654
+ task = asyncio.create_task(process_async_generator(result, i))
1655
+ generator_tasks.append(task)
1656
+
1657
+ # Stream events from the queue as they arrive
1658
+ completed_generators_count = 0
1659
+ while completed_generators_count < active_generators_count:
1660
+ try:
1661
+ event = await event_queue.get()
1662
+
1663
+ # Check if this is a completion signal
1664
+ if isinstance(event, tuple) and event[0] == "GENERATOR_DONE":
1665
+ completed_generators_count += 1
1666
+ continue
1667
+
1668
+ # Yield the actual event
1669
+ yield event
1670
+
1671
+ except Exception as e:
1672
+ log_error(f"Error processing async generator event: {e}")
1673
+ break
1674
+
1675
+ # Now process all results (non-async generators and completed async generators)
1676
+ for i, original_result in enumerate(results):
1677
+ # If result is an exception, skip processing it
1678
+ if isinstance(original_result, BaseException):
1679
+ log_error(f"Error during function call: {original_result}")
1680
+ raise original_result
1681
+
1682
+ # Unpack result
1683
+ function_call_success, function_call_timer, function_call, function_execution_result = original_result
1684
+
1685
+ # Check if this was an async generator that was already processed
1686
+ async_function_call_output = None
1687
+ if isinstance(function_call.result, (AsyncGeneratorType, collections.abc.AsyncIterator)):
1688
+ # Find the corresponding processed result
1689
+ async_gen_index = 0
1690
+ for j, result in enumerate(results[: i + 1]):
1691
+ if not isinstance(result, BaseException):
1692
+ _, _, fc, _ = result
1693
+ if isinstance(fc.result, (AsyncGeneratorType, collections.abc.AsyncIterator)):
1694
+ if j == i: # This is our async generator
1695
+ if async_gen_index in async_generator_outputs:
1696
+ _, async_function_call_output, error = async_generator_outputs[async_gen_index]
1697
+ if error:
1698
+ log_error(f"Error in async generator: {error}")
1699
+ raise error
1700
+ break
1701
+ async_gen_index += 1
1702
+
1703
+ updated_session_state = function_execution_result.updated_session_state
1704
+
1705
+ # Handle AgentRunException
1706
+ if isinstance(function_call_success, AgentRunException):
1707
+ a_exc = function_call_success
1708
+ # Update additional messages from function call
1709
+ _handle_agent_exception(a_exc, additional_input)
1710
+ # Set function call success to False if an exception occurred
1711
+ function_call_success = False
1712
+
1713
+ # Process function call output
1714
+ function_call_output: str = ""
1715
+
1716
+ # Check if this was an async generator that was already processed
1717
+ if async_function_call_output is not None:
1718
+ function_call_output = async_function_call_output
1719
+ # Events from async generators were already yielded in real-time above
1720
+ elif isinstance(function_call.result, (GeneratorType, collections.abc.Iterator)):
1721
+ for item in function_call.result:
1624
1722
  # This function yields agent/team run events
1625
1723
  if isinstance(item, tuple(get_args(RunOutputEvent))) or isinstance(
1626
1724
  item, tuple(get_args(TeamRunOutputEvent))
@@ -1637,13 +1735,8 @@ class Model(ABC):
1637
1735
  yield ModelResponse(content=item.content)
1638
1736
  continue
1639
1737
 
1640
- if isinstance(item, CustomEvent):
1641
- function_call_output += str(item)
1642
-
1643
1738
  # Yield the event itself to bubble it up
1644
1739
  yield item
1645
-
1646
- # Yield custom events emitted by the tool
1647
1740
  else:
1648
1741
  function_call_output += str(item)
1649
1742
  if function_call.function.show_result:
@@ -136,6 +136,7 @@ class Cerebras(Model):
136
136
  self,
137
137
  tools: Optional[List[Dict[str, Any]]] = None,
138
138
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
139
+ **kwargs: Any,
139
140
  ) -> Dict[str, Any]:
140
141
  """
141
142
  Returns keyword arguments for API requests.
@@ -25,6 +25,7 @@ class CerebrasOpenAI(OpenAILike):
25
25
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
26
26
  tools: Optional[List[Dict[str, Any]]] = None,
27
27
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
28
+ **kwargs: Any,
28
29
  ) -> Dict[str, Any]:
29
30
  """
30
31
  Returns keyword arguments for API requests.
@@ -73,6 +73,7 @@ class DashScope(OpenAILike):
73
73
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
74
74
  tools: Optional[List[Dict[str, Any]]] = None,
75
75
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
76
+ **kwargs: Any,
76
77
  ) -> Dict[str, Any]:
77
78
  params = super().get_request_params(response_format=response_format, tools=tools, tool_choice=tool_choice)
78
79