22from collections .abc import AsyncIterator
33from dataclasses import dataclass
44from os import getenv
5- from typing import Any , Dict , List , Optional , Tuple , Union
5+ from typing import Any , Dict , List , Optional , Union
66
77from agno .exceptions import ModelProviderError , ModelRateLimitError
8- from agno .media import File , Image
98from agno .models .base import Model
109from agno .models .message import Citations , DocumentCitation , Message
1110from agno .models .response import ModelResponse
1211from agno .utils .log import log_error , log_warning
12+ from agno .utils .models .claude import format_messages
1313
1414try :
1515 from anthropic import Anthropic as AnthropicClient
2121 ContentBlockStopEvent ,
2222 MessageDeltaEvent ,
2323 MessageStopEvent ,
24- TextBlock ,
25- ToolUseBlock ,
2624 )
2725 from anthropic .types import Message as AnthropicMessage
2826except (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
26531class 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"
0 commit comments