1
1
from datetime import datetime , timezone
2
- from typing import Any , ClassVar , Dict , List , Literal , Optional , Tuple , Union
2
+ from typing import Any , ClassVar , Dict , List , Literal , Optional , Tuple , Union , cast , get_args
3
3
4
4
from haystack import component , default_from_dict , default_to_dict , logging
5
5
from haystack .components .generators .utils import _convert_streaming_chunks_to_chat_message
6
- from haystack .dataclasses .chat_message import ChatMessage , ChatRole , ToolCall , ToolCallResult
6
+ from haystack .dataclasses .chat_message import ChatMessage , ChatRole , TextContent , ToolCall , ToolCallResult
7
+ from haystack .dataclasses .image_content import ImageContent
7
8
from haystack .dataclasses .streaming_chunk import (
8
9
AsyncStreamingCallbackT ,
9
10
ComponentInfo ,
26
27
27
28
from anthropic import Anthropic , AsyncAnthropic
28
29
from anthropic .resources .messages .messages import Message , RawMessageStreamEvent , Stream
29
- from anthropic .types import MessageParam , TextBlockParam , ToolParam , ToolResultBlockParam , ToolUseBlockParam
30
+ from anthropic .types import (
31
+ ImageBlockParam ,
32
+ MessageParam ,
33
+ TextBlockParam ,
34
+ ToolParam ,
35
+ ToolResultBlockParam ,
36
+ ToolUseBlockParam ,
37
+ )
30
38
31
39
logger = logging .getLogger (__name__ )
32
40
33
41
42
+ # See https://docs.anthropic.com/en/api/messages for supported formats
43
+ ImageFormat = Literal ["image/jpeg" , "image/png" , "image/gif" , "image/webp" ]
44
+ IMAGE_SUPPORTED_FORMATS : list [ImageFormat ] = list (get_args (ImageFormat ))
45
+
46
+
34
47
# Mapping from Anthropic stop reasons to Haystack FinishReason values
35
48
FINISH_REASON_MAPPING : Dict [str , FinishReason ] = {
36
49
"end_turn" : "stop" ,
44
57
45
58
def _update_anthropic_message_with_tool_call_results (
46
59
tool_call_results : List [ToolCallResult ],
47
- content : List [Union [TextBlockParam , ToolUseBlockParam , ToolResultBlockParam ]],
60
+ content : List [Union [TextBlockParam , ToolUseBlockParam , ToolResultBlockParam , ImageBlockParam ]],
48
61
) -> None :
49
62
"""
50
63
Update an Anthropic message content list with tool call results.
@@ -119,13 +132,39 @@ def _convert_messages_to_anthropic_format(
119
132
i += 1
120
133
continue
121
134
122
- content : List [Union [TextBlockParam , ToolUseBlockParam , ToolResultBlockParam ]] = []
123
-
124
- if message .texts and message .texts [0 ]:
125
- text_block = TextBlockParam (type = "text" , text = message .texts [0 ])
126
- if cache_control :
127
- text_block ["cache_control" ] = cache_control
128
- content .append (text_block )
135
+ content : List [Union [TextBlockParam , ToolUseBlockParam , ToolResultBlockParam , ImageBlockParam ]] = []
136
+
137
+ # Handle multimodal content (text and images) preserving order
138
+ for part in message ._content :
139
+ if isinstance (part , TextContent ) and part .text :
140
+ text_block = TextBlockParam (type = "text" , text = part .text )
141
+ if cache_control :
142
+ text_block ["cache_control" ] = cache_control
143
+ content .append (text_block )
144
+ elif isinstance (part , ImageContent ):
145
+ if not message .is_from (ChatRole .USER ):
146
+ msg = "Image content is only supported for user messages"
147
+ raise ValueError (msg )
148
+
149
+ if part .mime_type not in IMAGE_SUPPORTED_FORMATS :
150
+ supported_formats = ", " .join (IMAGE_SUPPORTED_FORMATS )
151
+ msg = (
152
+ f"Unsupported image format: { part .mime_type } . "
153
+ f"Anthropic supports the following formats: { supported_formats } "
154
+ )
155
+ raise ValueError (msg )
156
+
157
+ image_block = ImageBlockParam (
158
+ type = "image" ,
159
+ source = {
160
+ "type" : "base64" ,
161
+ "media_type" : cast (ImageFormat , part .mime_type ),
162
+ "data" : part .base64_image ,
163
+ },
164
+ )
165
+ if cache_control :
166
+ image_block ["cache_control" ] = cache_control
167
+ content .append (image_block )
129
168
130
169
if message .tool_calls :
131
170
tool_use_blocks = _convert_tool_calls_to_anthropic_format (message .tool_calls )
@@ -148,7 +187,10 @@ def _convert_messages_to_anthropic_format(
148
187
blk ["cache_control" ] = cache_control
149
188
150
189
if not content :
151
- msg = "A `ChatMessage` must contain at least one `TextContent`, `ToolCall`, or `ToolCallResult`."
190
+ msg = (
191
+ "A `ChatMessage` must contain at least one `TextContent`, `ImageContent`, "
192
+ "`ToolCall`, or `ToolCallResult`."
193
+ )
152
194
raise ValueError (msg )
153
195
154
196
# Anthropic only supports assistant and user roles in messages. User role is also used for tool messages.
@@ -170,7 +212,7 @@ class AnthropicChatGenerator:
170
212
Completes chats using Anthropic's large language models (LLMs).
171
213
172
214
It uses [ChatMessage](https://docs.haystack.deepset.ai/docs/data-classes#chatmessage)
173
- format in input and output.
215
+ format in input and output. Supports multimodal inputs including text and images.
174
216
175
217
You can customize how the text is generated by passing parameters to the
176
218
Anthropic API. Use the `**generation_kwargs` argument when you initialize
@@ -182,18 +224,41 @@ class AnthropicChatGenerator:
182
224
183
225
Usage example:
184
226
```python
185
- from haystack_integrations.components.generators.anthropic import AnthropicChatGenerator
227
+ from haystack_integrations.components.generators.anthropic import (
228
+ AnthropicChatGenerator,
229
+ )
186
230
from haystack.dataclasses import ChatMessage
187
231
188
- generator = AnthropicChatGenerator(model="claude-sonnet-4-20250514",
189
- generation_kwargs={
190
- "max_tokens": 1000,
191
- "temperature": 0.7,
192
- })
193
-
194
- messages = [ChatMessage.from_system("You are a helpful, respectful and honest assistant"),
195
- ChatMessage.from_user("What's Natural Language Processing?")]
232
+ generator = AnthropicChatGenerator(
233
+ model="claude-sonnet-4-20250514",
234
+ generation_kwargs={
235
+ "max_tokens": 1000,
236
+ "temperature": 0.7,
237
+ },
238
+ )
239
+
240
+ messages = [
241
+ ChatMessage.from_system(
242
+ "You are a helpful, respectful and honest assistant"
243
+ ),
244
+ ChatMessage.from_user("What's Natural Language Processing?"),
245
+ ]
196
246
print(generator.run(messages=messages))
247
+ ```
248
+
249
+ Usage example with images:
250
+ ```python
251
+ from haystack.dataclasses import ChatMessage, ImageContent
252
+
253
+ image_content = ImageContent.from_file_path("path/to/image.jpg")
254
+ messages = [
255
+ ChatMessage.from_user(
256
+ content_parts=["What's in this image?", image_content]
257
+ )
258
+ ]
259
+ generator = AnthropicChatGenerator()
260
+ result = generator.run(messages)
261
+ ```
197
262
"""
198
263
199
264
# The parameters that can be passed to the Anthropic API https://docs.anthropic.com/claude/reference/messages_post
0 commit comments