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.
- agno/agent/agent.py +1 -1
- agno/app/fastapi/app.py +3 -1
- agno/app/fastapi/async_router.py +1 -1
- agno/app/playground/app.py +1 -0
- agno/document/chunking/semantic.py +1 -3
- agno/document/reader/markdown_reader.py +2 -7
- agno/document/reader/pdf_reader.py +69 -13
- agno/document/reader/text_reader.py +2 -2
- agno/knowledge/agent.py +70 -75
- agno/knowledge/markdown.py +15 -2
- agno/knowledge/pdf.py +32 -8
- agno/knowledge/pdf_url.py +13 -5
- agno/knowledge/website.py +4 -1
- agno/media.py +2 -0
- agno/models/aws/bedrock.py +51 -21
- agno/models/dashscope/__init__.py +5 -0
- agno/models/dashscope/dashscope.py +81 -0
- agno/models/openai/chat.py +3 -0
- agno/models/openai/responses.py +53 -7
- agno/models/qwen/__init__.py +5 -0
- agno/run/response.py +4 -0
- agno/run/team.py +4 -0
- agno/storage/in_memory.py +234 -0
- agno/team/team.py +25 -9
- agno/tools/brandfetch.py +210 -0
- agno/tools/github.py +46 -18
- agno/tools/trafilatura.py +372 -0
- agno/vectordb/clickhouse/clickhousedb.py +1 -1
- agno/vectordb/milvus/milvus.py +89 -1
- agno/vectordb/weaviate/weaviate.py +84 -18
- agno/workflow/workflow.py +3 -0
- {agno-1.7.9.dist-info → agno-1.7.11.dist-info}/METADATA +5 -1
- {agno-1.7.9.dist-info → agno-1.7.11.dist-info}/RECORD +37 -31
- {agno-1.7.9.dist-info → agno-1.7.11.dist-info}/WHEEL +0 -0
- {agno-1.7.9.dist-info → agno-1.7.11.dist-info}/entry_points.txt +0 -0
- {agno-1.7.9.dist-info → agno-1.7.11.dist-info}/licenses/LICENSE +0 -0
- {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[
|
|
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
|
-
|
|
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,
|
|
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
|
agno/models/aws/bedrock.py
CHANGED
|
@@ -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
|
|
266
|
-
raise ValueError("Image content
|
|
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
|
|
269
|
-
raise ValueError(
|
|
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
|
|
287
|
-
raise ValueError("Video content
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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,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
|
agno/models/openai/chat.py
CHANGED
|
@@ -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:
|
agno/models/openai/responses.py
CHANGED
|
@@ -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
|
|
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":
|
|
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
|
|
392
|
-
"call_id": tool_call
|
|
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
|
-
|
|
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
|
|
784
|
-
"call_id": item
|
|
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,
|
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,
|