agno 1.7.9__py3-none-any.whl → 1.7.11__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 (37) hide show
  1. agno/agent/agent.py +1 -1
  2. agno/app/fastapi/app.py +3 -1
  3. agno/app/fastapi/async_router.py +1 -1
  4. agno/app/playground/app.py +1 -0
  5. agno/document/chunking/semantic.py +1 -3
  6. agno/document/reader/markdown_reader.py +2 -7
  7. agno/document/reader/pdf_reader.py +69 -13
  8. agno/document/reader/text_reader.py +2 -2
  9. agno/knowledge/agent.py +70 -75
  10. agno/knowledge/markdown.py +15 -2
  11. agno/knowledge/pdf.py +32 -8
  12. agno/knowledge/pdf_url.py +13 -5
  13. agno/knowledge/website.py +4 -1
  14. agno/media.py +2 -0
  15. agno/models/aws/bedrock.py +51 -21
  16. agno/models/dashscope/__init__.py +5 -0
  17. agno/models/dashscope/dashscope.py +81 -0
  18. agno/models/openai/chat.py +3 -0
  19. agno/models/openai/responses.py +53 -7
  20. agno/models/qwen/__init__.py +5 -0
  21. agno/run/response.py +4 -0
  22. agno/run/team.py +4 -0
  23. agno/storage/in_memory.py +234 -0
  24. agno/team/team.py +25 -9
  25. agno/tools/brandfetch.py +210 -0
  26. agno/tools/github.py +46 -18
  27. agno/tools/trafilatura.py +372 -0
  28. agno/vectordb/clickhouse/clickhousedb.py +1 -1
  29. agno/vectordb/milvus/milvus.py +89 -1
  30. agno/vectordb/weaviate/weaviate.py +84 -18
  31. agno/workflow/workflow.py +3 -0
  32. {agno-1.7.9.dist-info → agno-1.7.11.dist-info}/METADATA +5 -1
  33. {agno-1.7.9.dist-info → agno-1.7.11.dist-info}/RECORD +37 -31
  34. {agno-1.7.9.dist-info → agno-1.7.11.dist-info}/WHEEL +0 -0
  35. {agno-1.7.9.dist-info → agno-1.7.11.dist-info}/entry_points.txt +0 -0
  36. {agno-1.7.9.dist-info → agno-1.7.11.dist-info}/licenses/LICENSE +0 -0
  37. {agno-1.7.9.dist-info → agno-1.7.11.dist-info}/top_level.txt +0 -0
agno/knowledge/pdf.py CHANGED
@@ -2,15 +2,22 @@ from pathlib import Path
2
2
  from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Union
3
3
 
4
4
  from pydantic import Field
5
+ from typing_extensions import TypedDict
5
6
 
6
7
  from agno.document import Document
7
8
  from agno.document.reader.pdf_reader import PDFImageReader, PDFReader
8
9
  from agno.knowledge.agent import AgentKnowledge
9
- from agno.utils.log import log_info, logger
10
+ from agno.utils.log import log_error, log_info, logger
11
+
12
+
13
+ class PDFConfig(TypedDict, total=False):
14
+ path: str
15
+ password: Optional[str]
16
+ metadata: Optional[Dict[str, Any]]
10
17
 
11
18
 
12
19
  class PDFKnowledgeBase(AgentKnowledge):
13
- path: Optional[Union[str, Path, List[Dict[str, Union[str, Dict[str, Any]]]]]] = None
20
+ path: Optional[Union[str, Path, List[PDFConfig]]] = None
14
21
  formats: List[str] = [".pdf"]
15
22
  exclude_files: List[str] = Field(default_factory=list)
16
23
  reader: Union[PDFReader, PDFImageReader] = PDFReader()
@@ -24,19 +31,21 @@ class PDFKnowledgeBase(AgentKnowledge):
24
31
  if isinstance(self.path, list):
25
32
  for item in self.path:
26
33
  if isinstance(item, dict) and "path" in item:
27
- # Handle path with metadata
28
34
  file_path = item["path"]
29
35
  config = item.get("metadata", {})
36
+ file_password = item.get("password")
37
+ if file_password is not None and not isinstance(file_password, str):
38
+ file_password = None
39
+
30
40
  _pdf_path = Path(file_path) # type: ignore
31
41
  if self._is_valid_pdf(_pdf_path):
32
- documents = self.reader.read(pdf=_pdf_path)
42
+ documents = self.reader.read(pdf=_pdf_path, password=file_password)
33
43
  if config:
34
44
  for doc in documents:
35
45
  log_info(f"Adding metadata {config} to document: {doc.name}")
36
46
  doc.meta_data.update(config) # type: ignore
37
47
  yield documents
38
48
  else:
39
- # Handle single path
40
49
  _pdf_path = Path(self.path)
41
50
  if _pdf_path.is_dir():
42
51
  for _pdf in _pdf_path.glob("**/*.pdf"):
@@ -47,7 +56,19 @@ class PDFKnowledgeBase(AgentKnowledge):
47
56
 
48
57
  def _is_valid_pdf(self, path: Path) -> bool:
49
58
  """Helper to check if path is a valid PDF file."""
50
- return path.exists() and path.is_file() and path.suffix == ".pdf" and path.name not in self.exclude_files
59
+ if not path.exists():
60
+ log_error(f"PDF file not found: {path}")
61
+ return False
62
+ if not path.is_file():
63
+ log_error(f"Path is not a file: {path}")
64
+ return False
65
+ if path.suffix != ".pdf":
66
+ log_error(f"File is not a PDF: {path}")
67
+ return False
68
+ if path.name in self.exclude_files:
69
+ log_error(f"PDF file excluded: {path}")
70
+ return False
71
+ return True
51
72
 
52
73
  @property
53
74
  async def async_document_lists(self) -> AsyncIterator[List[Document]]:
@@ -58,12 +79,15 @@ class PDFKnowledgeBase(AgentKnowledge):
58
79
  if isinstance(self.path, list):
59
80
  for item in self.path:
60
81
  if isinstance(item, dict) and "path" in item:
61
- # Handle path with metadata
62
82
  file_path = item["path"]
63
83
  config = item.get("metadata", {})
84
+ file_password = item.get("password")
85
+ if file_password is not None and not isinstance(file_password, str):
86
+ file_password = None
87
+
64
88
  _pdf_path = Path(file_path) # type: ignore
65
89
  if self._is_valid_pdf(_pdf_path):
66
- documents = await self.reader.async_read(pdf=_pdf_path)
90
+ documents = await self.reader.async_read(pdf=_pdf_path, password=file_password)
67
91
  if config:
68
92
  for doc in documents:
69
93
  log_info(f"Adding metadata {config} to document: {doc.name}")
agno/knowledge/pdf_url.py CHANGED
@@ -19,18 +19,22 @@ class PDFUrlKnowledgeBase(AgentKnowledge):
19
19
 
20
20
  for item in self.urls:
21
21
  if isinstance(item, dict) and "url" in item:
22
- # Handle URL with metadata
22
+ # Handle URL with metadata/password
23
23
  url = item["url"]
24
24
  config = item.get("metadata", {})
25
+ pdf_password = item.get("password")
26
+ if pdf_password is not None and not isinstance(pdf_password, str):
27
+ pdf_password = None
28
+
25
29
  if self._is_valid_url(url): # type: ignore
26
- documents = self.reader.read(url=url) # type: ignore
30
+ documents = self.reader.read(url=url, password=pdf_password) # type: ignore
27
31
  if config:
28
32
  for doc in documents:
29
33
  log_info(f"Adding metadata {config} to document from URL: {url}")
30
34
  doc.meta_data.update(config) # type: ignore
31
35
  yield documents
32
36
  else:
33
- # Handle simple URL
37
+ # Handle simple URL - no password
34
38
  if self._is_valid_url(item): # type: ignore
35
39
  yield self.reader.read(url=item) # type: ignore
36
40
 
@@ -49,11 +53,15 @@ class PDFUrlKnowledgeBase(AgentKnowledge):
49
53
 
50
54
  for item in self.urls:
51
55
  if isinstance(item, dict) and "url" in item:
52
- # Handle URL with metadata
56
+ # Handle URL with metadata/password
53
57
  url = item["url"]
54
58
  config = item.get("metadata", {})
59
+ pdf_password = item.get("password")
60
+ if pdf_password is not None and not isinstance(pdf_password, str):
61
+ pdf_password = None
62
+
55
63
  if self._is_valid_url(url): # type: ignore
56
- documents = await self.reader.async_read(url=url) # type: ignore
64
+ documents = await self.reader.async_read(url=url, password=pdf_password) # type: ignore
57
65
  if config:
58
66
  for doc in documents:
59
67
  log_info(f"Adding metadata {config} to document from URL: {url}")
agno/knowledge/website.py CHANGED
@@ -4,6 +4,7 @@ from typing import Any, AsyncIterator, Dict, Iterator, List, Optional
4
4
  from pydantic import model_validator
5
5
 
6
6
  from agno.document import Document
7
+ from agno.document.chunking.fixed import FixedSizeChunking
7
8
  from agno.document.reader.website_reader import WebsiteReader
8
9
  from agno.knowledge.agent import AgentKnowledge
9
10
  from agno.utils.log import log_debug, log_info, logger
@@ -21,7 +22,9 @@ class WebsiteKnowledgeBase(AgentKnowledge):
21
22
  def set_reader(self) -> "WebsiteKnowledgeBase":
22
23
  if self.reader is None:
23
24
  self.reader = WebsiteReader(
24
- max_depth=self.max_depth, max_links=self.max_links, chunking_strategy=self.chunking_strategy
25
+ max_depth=self.max_depth,
26
+ max_links=self.max_links,
27
+ chunking_strategy=self.chunking_strategy or FixedSizeChunking(),
25
28
  )
26
29
  return self
27
30
 
agno/media.py CHANGED
@@ -319,6 +319,8 @@ class File(BaseModel):
319
319
  mime_type: Optional[str] = None
320
320
  # External file object (e.g. GeminiFile, must be a valid object as expected by the model you are using)
321
321
  external: Optional[Any] = None
322
+ format: Optional[str] = None # E.g. `pdf`, `txt`, `csv`, `xml`, etc.
323
+ name: Optional[str] = None # Name of the file, mandatory for AWS Bedrock document input
322
324
 
323
325
  @model_validator(mode="before")
324
326
  @classmethod
@@ -27,6 +27,11 @@ except ImportError:
27
27
  AIOBOTO3_AVAILABLE = False
28
28
 
29
29
 
30
+ BEDROCK_SUPPORTED_IMAGE_FORMATS = ["png", "jpeg", "webp", "gif"]
31
+ BEDROCK_SUPPORTED_VIDEO_FORMATS = ["mp4", "mov", "mkv", "webm", "flv", "mpeg", "mpg", "wmv", "three_gp"]
32
+ BEDROCK_SUPPORTED_FILE_FORMATS = ["pdf", "csv", "doc", "docx", "xls", "xlsx", "html", "txt", "md"]
33
+
34
+
30
35
  @dataclass
31
36
  class AwsBedrock(Model):
32
37
  """
@@ -262,11 +267,16 @@ class AwsBedrock(Model):
262
267
 
263
268
  if message.images:
264
269
  for image in message.images:
265
- if not image.content or not image.format:
266
- raise ValueError("Image content and format are required.")
270
+ if not image.content:
271
+ raise ValueError("Image content is required for AWS Bedrock.")
272
+ if not image.format:
273
+ raise ValueError("Image format is required for AWS Bedrock.")
267
274
 
268
- if image.format not in ["png", "jpeg", "webp", "gif"]:
269
- raise ValueError(f"Unsupported image format: {image.format}")
275
+ if image.format not in BEDROCK_SUPPORTED_IMAGE_FORMATS:
276
+ raise ValueError(
277
+ f"Unsupported image format: {image.format}. "
278
+ f"Supported formats: {BEDROCK_SUPPORTED_IMAGE_FORMATS}"
279
+ )
270
280
 
271
281
  formatted_message["content"].append(
272
282
  {
@@ -283,21 +293,16 @@ class AwsBedrock(Model):
283
293
 
284
294
  if message.videos:
285
295
  for video in message.videos:
286
- if not video.content or not video.format:
287
- raise ValueError("Video content and format are required.")
288
-
289
- if video.format not in [
290
- "mp4",
291
- "mov",
292
- "mkv",
293
- "webm",
294
- "flv",
295
- "mpeg",
296
- "mpg",
297
- "wmv",
298
- "three_gp",
299
- ]:
300
- raise ValueError(f"Unsupported video format: {video.format}")
296
+ if not video.content:
297
+ raise ValueError("Video content is required for AWS Bedrock.")
298
+ if not video.format:
299
+ raise ValueError("Video format is required for AWS Bedrock.")
300
+
301
+ if video.format not in BEDROCK_SUPPORTED_VIDEO_FORMATS:
302
+ raise ValueError(
303
+ f"Unsupported video format: {video.format}. "
304
+ f"Supported formats: {BEDROCK_SUPPORTED_VIDEO_FORMATS}"
305
+ )
301
306
 
302
307
  formatted_message["content"].append(
303
308
  {
@@ -309,8 +314,33 @@ class AwsBedrock(Model):
309
314
  }
310
315
  }
311
316
  )
312
- if message.files is not None and len(message.files) > 0:
313
- log_warning("File input is currently unsupported.")
317
+
318
+ if message.files:
319
+ for file in message.files:
320
+ if not file.content:
321
+ raise ValueError("File content is required for AWS Bedrock document input.")
322
+ if not file.format:
323
+ raise ValueError("File format is required for AWS Bedrock document input.")
324
+ if not file.name:
325
+ raise ValueError("File name is required for AWS Bedrock document input.")
326
+
327
+ if file.format not in BEDROCK_SUPPORTED_FILE_FORMATS:
328
+ raise ValueError(
329
+ f"Unsupported file format: {file.format}. "
330
+ f"Supported formats: {BEDROCK_SUPPORTED_FILE_FORMATS}"
331
+ )
332
+
333
+ formatted_message["content"].append(
334
+ {
335
+ "document": {
336
+ "format": file.format,
337
+ "name": file.name,
338
+ "source": {
339
+ "bytes": file.content,
340
+ },
341
+ }
342
+ }
343
+ )
314
344
 
315
345
  formatted_messages.append(formatted_message)
316
346
  # TODO: Add caching: https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html
@@ -0,0 +1,5 @@
1
+ from agno.models.dashscope.dashscope import DashScope
2
+
3
+ __all__ = [
4
+ "DashScope",
5
+ ]
@@ -0,0 +1,81 @@
1
+ from dataclasses import dataclass
2
+ from os import getenv
3
+ from typing import Any, Dict, List, Optional, Type, Union
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from agno.exceptions import ModelProviderError
8
+ from agno.models.openai.like import OpenAILike
9
+
10
+
11
+ @dataclass
12
+ class DashScope(OpenAILike):
13
+ """
14
+ A class for interacting with Qwen models via DashScope API.
15
+
16
+ Attributes:
17
+ id (str): The model id. Defaults to "qwen-plus".
18
+ name (str): The model name. Defaults to "Qwen".
19
+ provider (str): The provider name. Defaults to "Qwen".
20
+ api_key (Optional[str]): The DashScope API key.
21
+ base_url (str): The base URL. Defaults to "https://dashscope-intl.aliyuncs.com/compatible-mode/v1".
22
+ enable_thinking (Optional[bool]): Enable thinking process (DashScope native parameter). Defaults to None.
23
+ include_thoughts (Optional[bool]): Include thinking process in response (alternative parameter). Defaults to None.
24
+ """
25
+
26
+ id: str = "qwen-plus"
27
+ name: str = "Qwen"
28
+ provider: str = "Dashscope"
29
+
30
+ api_key: Optional[str] = getenv("DASHSCOPE_API_KEY") or getenv("QWEN_API_KEY")
31
+ base_url: str = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
32
+
33
+ # Thinking parameters
34
+ enable_thinking: Optional[bool] = None
35
+ include_thoughts: Optional[bool] = None
36
+
37
+ # DashScope supports structured outputs
38
+ supports_native_structured_outputs: bool = True
39
+ supports_json_schema_outputs: bool = True
40
+
41
+ def _get_client_params(self) -> Dict[str, Any]:
42
+ if not self.api_key:
43
+ self.api_key = getenv("DASHSCOPE_API_KEY")
44
+ if not self.api_key:
45
+ raise ModelProviderError(
46
+ message="DASHSCOPE_API_KEY not set. Please set the DASHSCOPE_API_KEY environment variable.",
47
+ model_name=self.name,
48
+ model_id=self.id,
49
+ )
50
+
51
+ # Define base client params
52
+ base_params = {
53
+ "api_key": self.api_key,
54
+ "organization": self.organization,
55
+ "base_url": self.base_url,
56
+ "timeout": self.timeout,
57
+ "max_retries": self.max_retries,
58
+ "default_headers": self.default_headers,
59
+ "default_query": self.default_query,
60
+ }
61
+
62
+ # Create client_params dict with non-None values
63
+ client_params = {k: v for k, v in base_params.items() if v is not None}
64
+
65
+ # Add additional client params if provided
66
+ if self.client_params:
67
+ client_params.update(self.client_params)
68
+ return client_params
69
+
70
+ def get_request_params(
71
+ self,
72
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
73
+ tools: Optional[List[Dict[str, Any]]] = None,
74
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
75
+ ) -> Dict[str, Any]:
76
+ params = super().get_request_params(response_format=response_format, tools=tools, tool_choice=tool_choice)
77
+
78
+ should_include_thoughts = self.enable_thinking or self.include_thoughts
79
+ if should_include_thoughts:
80
+ params["extra_body"] = {"enable_thinking": True}
81
+ return params
@@ -694,6 +694,9 @@ class OpenAIChat(Model):
694
694
  if choice_delta.tool_calls is not None:
695
695
  model_response.tool_calls = choice_delta.tool_calls # type: ignore
696
696
 
697
+ if hasattr(choice_delta, "reasoning_content") and choice_delta.reasoning_content is not None:
698
+ model_response.reasoning_content = choice_delta.reasoning_content
699
+
697
700
  # Add audio if present
698
701
  if hasattr(choice_delta, "audio") and choice_delta.audio is not None:
699
702
  try:
@@ -78,6 +78,10 @@ class OpenAIResponses(Model):
78
78
  }
79
79
  )
80
80
 
81
+ def _using_reasoning_model(self) -> bool:
82
+ """Return True if the contextual used model is a known reasoning model."""
83
+ return self.id.startswith("o3") or self.id.startswith("o4-mini") or self.id.startswith("gpt-5")
84
+
81
85
  def _get_client_params(self) -> Dict[str, Any]:
82
86
  """
83
87
  Get client parameters for API requests.
@@ -221,7 +225,7 @@ class OpenAIResponses(Model):
221
225
  request_params["tool_choice"] = tool_choice
222
226
 
223
227
  # Handle reasoning tools for o3 and o4-mini models
224
- if (self.id.startswith("o3") or self.id.startswith("o4-mini")) and messages is not None:
228
+ if self._using_reasoning_model() and messages is not None:
225
229
  request_params["store"] = True
226
230
 
227
231
  # Check if the last assistant message has a previous_response_id to continue from
@@ -352,6 +356,33 @@ class OpenAIResponses(Model):
352
356
  Dict[str, Any]: The formatted message.
353
357
  """
354
358
  formatted_messages: List[Dict[str, Any]] = []
359
+
360
+ if self._using_reasoning_model():
361
+ # Detect whether we're chaining via previous_response_id. If so, we should NOT
362
+ # re-send prior function_call items; the Responses API already has the state and
363
+ # expects only the corresponding function_call_output items.
364
+ previous_response_id: Optional[str] = None
365
+ for msg in reversed(messages):
366
+ if (
367
+ msg.role == "assistant"
368
+ and hasattr(msg, "provider_data")
369
+ and msg.provider_data
370
+ and "response_id" in msg.provider_data
371
+ ):
372
+ previous_response_id = msg.provider_data["response_id"]
373
+ break
374
+
375
+ # Build a mapping from function_call id (fc_*) → call_id (call_*) from prior assistant tool_calls
376
+ fc_id_to_call_id: Dict[str, str] = {}
377
+ for msg in messages:
378
+ tool_calls = getattr(msg, "tool_calls", None)
379
+ if tool_calls:
380
+ for tc in tool_calls:
381
+ fc_id = tc.get("id")
382
+ call_id = tc.get("call_id") or fc_id
383
+ if isinstance(fc_id, str) and isinstance(call_id, str):
384
+ fc_id_to_call_id[fc_id] = call_id
385
+
355
386
  for message in messages:
356
387
  if message.role in ["user", "system"]:
357
388
  message_dict: Dict[str, Any] = {
@@ -378,18 +409,32 @@ class OpenAIResponses(Model):
378
409
 
379
410
  formatted_messages.append(message_dict)
380
411
 
412
+ # Tool call result
381
413
  elif message.role == "tool":
382
414
  if message.tool_call_id and message.content is not None:
415
+ function_call_id = message.tool_call_id
416
+ # Normalize: if a fc_* id was provided, translate to its corresponding call_* id
417
+ if isinstance(function_call_id, str) and function_call_id in fc_id_to_call_id:
418
+ call_id_value = fc_id_to_call_id[function_call_id]
419
+ else:
420
+ call_id_value = function_call_id
383
421
  formatted_messages.append(
384
- {"type": "function_call_output", "call_id": message.tool_call_id, "output": message.content}
422
+ {"type": "function_call_output", "call_id": call_id_value, "output": message.content}
385
423
  )
424
+ # Tool Calls
386
425
  elif message.tool_calls is not None and len(message.tool_calls) > 0:
426
+ # Only skip re-sending prior function_call items when we have a previous_response_id
427
+ # (reasoning models). For non-reasoning models, we must include the prior function_call
428
+ # so the API can associate the subsequent function_call_output by call_id.
429
+ if self._using_reasoning_model() and previous_response_id is not None:
430
+ continue
431
+
387
432
  for tool_call in message.tool_calls:
388
433
  formatted_messages.append(
389
434
  {
390
435
  "type": "function_call",
391
- "id": tool_call["id"],
392
- "call_id": tool_call["call_id"],
436
+ "id": tool_call.get("id"),
437
+ "call_id": tool_call.get("call_id", tool_call.get("id")),
393
438
  "name": tool_call["function"]["name"],
394
439
  "arguments": tool_call["function"]["arguments"],
395
440
  "status": "completed",
@@ -690,7 +735,8 @@ class OpenAIResponses(Model):
690
735
  model_response.tool_calls.append(
691
736
  {
692
737
  "id": output.id,
693
- "call_id": output.call_id,
738
+ # Store additional call_id from OpenAI responses
739
+ "call_id": output.call_id or output.id,
694
740
  "type": "function",
695
741
  "function": {
696
742
  "name": output.name,
@@ -780,8 +826,8 @@ class OpenAIResponses(Model):
780
826
  item = stream_event.item
781
827
  if item.type == "function_call":
782
828
  tool_use = {
783
- "id": item.id,
784
- "call_id": item.call_id,
829
+ "id": getattr(item, "id", None),
830
+ "call_id": getattr(item, "call_id", None) or getattr(item, "id", None),
785
831
  "type": "function",
786
832
  "function": {
787
833
  "name": item.name,
@@ -0,0 +1,5 @@
1
+ from agno.models.dashscope.dashscope import DashScope as Qwen
2
+
3
+ __all__ = [
4
+ "Qwen",
5
+ ]
agno/run/response.py CHANGED
@@ -420,6 +420,9 @@ class RunResponse:
420
420
  messages = data.pop("messages", None)
421
421
  messages = [Message.model_validate(message) for message in messages] if messages else None
422
422
 
423
+ citations = data.pop("citations", None)
424
+ citations = Citations.model_validate(citations) if citations else None
425
+
423
426
  tools = data.pop("tools", [])
424
427
  tools = [ToolExecution.from_dict(tool) for tool in tools] if tools else None
425
428
 
@@ -441,6 +444,7 @@ class RunResponse:
441
444
 
442
445
  return cls(
443
446
  messages=messages,
447
+ citations=citations,
444
448
  tools=tools,
445
449
  images=images,
446
450
  audio=audio,
agno/run/team.py CHANGED
@@ -416,6 +416,9 @@ class TeamRunResponse:
416
416
  response_audio = data.pop("response_audio", None)
417
417
  response_audio = AudioResponse.model_validate(response_audio) if response_audio else None
418
418
 
419
+ citations = data.pop("citations", None)
420
+ citations = Citations.model_validate(citations) if citations else None
421
+
419
422
  # To make it backwards compatible
420
423
  if "event" in data:
421
424
  data.pop("event")
@@ -428,6 +431,7 @@ class TeamRunResponse:
428
431
  videos=videos,
429
432
  audio=audio,
430
433
  response_audio=response_audio,
434
+ citations=citations,
431
435
  tools=tools,
432
436
  events=events,
433
437
  **data,