Skip to content

Commit 733913f

Browse files
authored
claude-bug-fix-ag-2961 (#2720)
## Summary Add a default message to the content if the tool calls do not include a message to prevent the following error: ![image](https://github.com/user-attachments/assets/336b15bb-2307-4317-988f-54409d2b4228) ## Type of change - [x] Bug fix (#2712) - [ ] New feature - [ ] Breaking change - [ ] Improvement - [ ] Model update - [ ] Other: _____________________ --- ## Checklist - [ ] Code complies with style guidelines - [ ] Ran format/validation scripts (`./scripts/format.sh` and `./scripts/validate.sh`) - [ ] Self-review completed - [ ] Documentation updated (comments, docstrings) - [ ] Examples and guides: Relevant cookbook examples have been included or updated (if applicable) - [ ] Tested in clean environment - [ ] Tests added/updated (if applicable) --- ## Additional Notes Add any important context (deployment instructions, screenshots, security considerations, etc.)
1 parent 54f892f commit 733913f

File tree

4 files changed

+255
-242
lines changed

4 files changed

+255
-242
lines changed
-638 KB
Binary file not shown.

libs/agno/agno/models/anthropic/claude.py

Lines changed: 6 additions & 242 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
from collections.abc import AsyncIterator
33
from dataclasses import dataclass
44
from os import getenv
5-
from typing import Any, Dict, List, Optional, Tuple, Union
5+
from typing import Any, Dict, List, Optional, Union
66

77
from agno.exceptions import ModelProviderError, ModelRateLimitError
8-
from agno.media import File, Image
98
from agno.models.base import Model
109
from agno.models.message import Citations, DocumentCitation, Message
1110
from agno.models.response import ModelResponse
1211
from agno.utils.log import log_error, log_warning
12+
from agno.utils.models.claude import format_messages
1313

1414
try:
1515
from anthropic import Anthropic as AnthropicClient
@@ -21,245 +21,11 @@
2121
ContentBlockStopEvent,
2222
MessageDeltaEvent,
2323
MessageStopEvent,
24-
TextBlock,
25-
ToolUseBlock,
2624
)
2725
from anthropic.types import Message as AnthropicMessage
2826
except (ModuleNotFoundError, ImportError):
2927
raise ImportError("`anthropic` not installed. Please install using `pip install anthropic`")
3028

31-
ROLE_MAP = {
32-
"system": "system",
33-
"user": "user",
34-
"assistant": "assistant",
35-
"tool": "user",
36-
}
37-
38-
39-
def _format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
40-
"""
41-
Add an image to a message by converting it to base64 encoded format.
42-
"""
43-
using_filetype = False
44-
45-
import base64
46-
47-
# 'imghdr' was deprecated in Python 3.11: https://docs.python.org/3/library/imghdr.html
48-
# 'filetype' used as a fallback
49-
try:
50-
import imghdr
51-
except (ModuleNotFoundError, ImportError):
52-
try:
53-
import filetype
54-
55-
using_filetype = True
56-
except (ModuleNotFoundError, ImportError):
57-
raise ImportError("`filetype` not installed. Please install using `pip install filetype`")
58-
59-
type_mapping = {
60-
"jpeg": "image/jpeg",
61-
"jpg": "image/jpeg",
62-
"png": "image/png",
63-
"gif": "image/gif",
64-
"webp": "image/webp",
65-
}
66-
67-
try:
68-
# Case 1: Image is a URL
69-
if image.url is not None:
70-
return {"type": "image", "source": {"type": "url", "url": image.url}}
71-
72-
# Case 2: Image is a local file path
73-
elif image.filepath is not None:
74-
from pathlib import Path
75-
76-
path = Path(image.filepath) if isinstance(image.filepath, str) else image.filepath
77-
if path.exists() and path.is_file():
78-
with open(image.filepath, "rb") as f:
79-
content_bytes = f.read()
80-
else:
81-
log_error(f"Image file not found: {image}")
82-
return None
83-
84-
# Case 3: Image is a bytes object
85-
elif image.content is not None:
86-
content_bytes = image.content
87-
88-
else:
89-
log_error(f"Unsupported image type: {type(image)}")
90-
return None
91-
92-
if using_filetype:
93-
kind = filetype.guess(content_bytes)
94-
if not kind:
95-
log_error("Unable to determine image type")
96-
return None
97-
98-
img_type = kind.extension
99-
else:
100-
img_type = imghdr.what(None, h=content_bytes) # type: ignore
101-
102-
if not img_type:
103-
log_error("Unable to determine image type")
104-
return None
105-
106-
media_type = type_mapping.get(img_type)
107-
if not media_type:
108-
log_error(f"Unsupported image type: {img_type}")
109-
return None
110-
111-
return {
112-
"type": "image",
113-
"source": {
114-
"type": "base64",
115-
"media_type": media_type,
116-
"data": base64.b64encode(content_bytes).decode("utf-8"), # type: ignore
117-
},
118-
}
119-
120-
except Exception as e:
121-
log_error(f"Error processing image: {e}")
122-
return None
123-
124-
125-
def _format_file_for_message(file: File) -> Optional[Dict[str, Any]]:
126-
"""
127-
Add a document url or base64 encoded content to a message.
128-
"""
129-
130-
mime_mapping = {
131-
"application/pdf": "base64",
132-
"text/plain": "text",
133-
}
134-
135-
# Case 1: Document is a URL
136-
if file.url is not None:
137-
return {
138-
"type": "document",
139-
"source": {
140-
"type": "url",
141-
"url": file.url,
142-
},
143-
"citations": {"enabled": True},
144-
}
145-
# Case 2: Document is a local file path
146-
elif file.filepath is not None:
147-
import base64
148-
from pathlib import Path
149-
150-
path = Path(file.filepath) if isinstance(file.filepath, str) else file.filepath
151-
if path.exists() and path.is_file():
152-
file_data = base64.standard_b64encode(path.read_bytes()).decode("utf-8")
153-
154-
# Determine media type
155-
media_type = file.mime_type
156-
if media_type is None:
157-
import mimetypes
158-
159-
media_type = mimetypes.guess_type(file.filepath)[0] or "application/pdf"
160-
161-
# Map media type to type, default to "base64" if no mapping exists
162-
type = mime_mapping.get(media_type, "base64")
163-
164-
return {
165-
"type": "document",
166-
"source": {
167-
"type": type,
168-
"media_type": media_type,
169-
"data": file_data,
170-
},
171-
"citations": {"enabled": True},
172-
}
173-
else:
174-
log_error(f"Document file not found: {file}")
175-
return None
176-
# Case 3: Document is bytes content
177-
elif file.content is not None:
178-
import base64
179-
180-
file_data = base64.standard_b64encode(file.content).decode("utf-8")
181-
return {
182-
"type": "document",
183-
"source": {"type": "base64", "media_type": file.mime_type or "application/pdf", "data": file_data},
184-
"citations": {"enabled": True},
185-
}
186-
return None
187-
188-
189-
def _format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]:
190-
"""
191-
Process the list of messages and separate them into API messages and system messages.
192-
193-
Args:
194-
messages (List[Message]): The list of messages to process.
195-
196-
Returns:
197-
Tuple[List[Dict[str, str]], str]: A tuple containing the list of API messages and the concatenated system messages.
198-
"""
199-
chat_messages: List[Dict[str, str]] = []
200-
system_messages: List[str] = []
201-
202-
for message in messages:
203-
content = message.content or ""
204-
if message.role == "system":
205-
if content is not None:
206-
system_messages.append(content) # type: ignore
207-
continue
208-
elif message.role == "user":
209-
if isinstance(content, str):
210-
content = [{"type": "text", "text": content}]
211-
212-
if message.images is not None:
213-
for image in message.images:
214-
image_content = _format_image_for_message(image)
215-
if image_content:
216-
content.append(image_content)
217-
218-
if message.files is not None:
219-
for file in message.files:
220-
file_content = _format_file_for_message(file)
221-
if file_content:
222-
content.append(file_content)
223-
224-
# Handle tool calls from history
225-
elif message.role == "assistant":
226-
content = []
227-
228-
if message.thinking is not None and message.provider_data is not None:
229-
from anthropic.types import RedactedThinkingBlock, ThinkingBlock
230-
231-
content.append(
232-
ThinkingBlock(
233-
thinking=message.thinking,
234-
signature=message.provider_data.get("signature"),
235-
type="thinking",
236-
)
237-
)
238-
239-
if message.redacted_thinking is not None:
240-
from anthropic.types import RedactedThinkingBlock
241-
242-
content.append(RedactedThinkingBlock(data=message.redacted_thinking, type="redacted_thinking"))
243-
244-
if isinstance(message.content, str) and message.content:
245-
content.append(TextBlock(text=message.content, type="text"))
246-
247-
if message.tool_calls:
248-
for tool_call in message.tool_calls:
249-
content.append(
250-
ToolUseBlock(
251-
id=tool_call["id"],
252-
input=json.loads(tool_call["function"]["arguments"])
253-
if "arguments" in tool_call["function"]
254-
else {},
255-
name=tool_call["function"]["name"],
256-
type="tool_use",
257-
)
258-
)
259-
260-
chat_messages.append({"role": ROLE_MAP[message.role], "content": content}) # type: ignore
261-
return chat_messages, " ".join(system_messages)
262-
26329

26430
@dataclass
26531
class Claude(Model):
@@ -426,7 +192,7 @@ def invoke(self, messages: List[Message]) -> AnthropicMessage:
426192
APIStatusError: For other API-related errors
427193
"""
428194
try:
429-
chat_messages, system_message = _format_messages(messages)
195+
chat_messages, system_message = format_messages(messages)
430196
request_kwargs = self._prepare_request_kwargs(system_message)
431197

432198
return self.get_client().messages.create(
@@ -459,7 +225,7 @@ def invoke_stream(self, messages: List[Message]) -> Any:
459225
Returns:
460226
Any: The streamed response from the model.
461227
"""
462-
chat_messages, system_message = _format_messages(messages)
228+
chat_messages, system_message = format_messages(messages)
463229
request_kwargs = self._prepare_request_kwargs(system_message)
464230

465231
try:
@@ -503,7 +269,7 @@ async def ainvoke(self, messages: List[Message]) -> AnthropicMessage:
503269
APIStatusError: For other API-related errors
504270
"""
505271
try:
506-
chat_messages, system_message = _format_messages(messages)
272+
chat_messages, system_message = format_messages(messages)
507273
request_kwargs = self._prepare_request_kwargs(system_message)
508274

509275
return await self.get_async_client().messages.create(
@@ -537,7 +303,7 @@ async def ainvoke_stream(self, messages: List[Message]) -> AsyncIterator[Any]:
537303
Any: The streamed response from the model.
538304
"""
539305
try:
540-
chat_messages, system_message = _format_messages(messages)
306+
chat_messages, system_message = format_messages(messages)
541307
request_kwargs = self._prepare_request_kwargs(system_message)
542308
async with self.get_async_client().messages.stream(
543309
model=self.id,
@@ -561,7 +327,6 @@ async def ainvoke_stream(self, messages: List[Message]) -> AsyncIterator[Any]:
561327
log_error(f"Unexpected error calling Claude API: {str(e)}")
562328
raise ModelProviderError(message=str(e), model_name=self.name, model_id=self.id) from e
563329

564-
# Overwrite the default from the base model
565330
def format_function_call_results(
566331
self, messages: List[Message], function_call_results: List[Message], tool_ids: List[str]
567332
) -> None:
@@ -585,7 +350,6 @@ def format_function_call_results(
585350
)
586351
messages.append(Message(role="user", content=fc_responses))
587352

588-
# Overwrite the default from the base model
589353
def get_system_message_for_model(self) -> Optional[str]:
590354
if self._functions is not None and len(self._functions) > 0:
591355
tool_call_prompt = "Do not reflect on the quality of the returned search results in your response"

libs/agno/agno/utils/models/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)