Skip to content

Commit dd307bc

Browse files
authored
Bedrock - support images in tool results (#2783)
1 parent ce20701 commit dd307bc

4 files changed

Lines changed: 113 additions & 21 deletions

File tree

integrations/amazon_bedrock/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ classifiers = [
2222
"Programming Language :: Python :: Implementation :: CPython",
2323
"Programming Language :: Python :: Implementation :: PyPy",
2424
]
25-
dependencies = ["haystack-ai>=2.22.0", "boto3>=1.28.57", "aioboto3>=14.0.0"]
25+
dependencies = ["haystack-ai>=2.23.0", "boto3>=1.28.57", "aioboto3>=14.0.0"]
2626

2727
[project.urls]
2828
Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/amazon_bedrock#readme"

integrations/amazon_bedrock/src/haystack_integrations/components/generators/amazon_bedrock/chat/utils.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ def _format_tools(tools: list[Tool] | None = None) -> dict[str, Any] | None:
6060
return {"tools": tool_specs} if tool_specs else None
6161

6262

63+
def _convert_image_content_to_bedrock_format(image_content: ImageContent) -> dict[str, Any]:
64+
"""
65+
Convert a Haystack ImageContent to Bedrock format.
66+
"""
67+
68+
image_format = image_content.mime_type.split("/")[-1] if image_content.mime_type else None
69+
if image_format not in IMAGE_SUPPORTED_FORMATS:
70+
err_msg = (
71+
f"Unsupported image format: {image_format}. "
72+
f"Bedrock supports the following image formats: {IMAGE_SUPPORTED_FORMATS}"
73+
)
74+
raise ValueError(err_msg)
75+
76+
source = {"bytes": base64.b64decode(image_content.base64_image)}
77+
78+
return {"image": {"format": image_format, "source": source}}
79+
80+
6381
def _format_tool_call_message(tool_call_message: ChatMessage) -> dict[str, Any]:
6482
"""
6583
Format a Haystack ChatMessage containing tool calls into Bedrock format.
@@ -94,19 +112,30 @@ def _format_tool_result_message(tool_call_result_message: ChatMessage) -> dict[s
94112
"""
95113
# Assuming tool call result messages will only contain tool results
96114
tool_results = []
97-
for result in tool_call_result_message.tool_call_results:
98-
try:
99-
json_result = json.loads(result.result)
100-
content = [{"json": json_result}]
101-
except json.JSONDecodeError:
102-
content = [{"text": result.result}]
115+
for tool_call_result in tool_call_result_message.tool_call_results:
116+
if isinstance(tool_call_result.result, str):
117+
try:
118+
json_result = json.loads(tool_call_result.result)
119+
content = [{"json": json_result}]
120+
except json.JSONDecodeError:
121+
content = [{"text": tool_call_result.result}]
122+
elif isinstance(tool_call_result.result, list):
123+
content = []
124+
for item in tool_call_result.result:
125+
if isinstance(item, TextContent):
126+
content.append({"text": item.text})
127+
elif isinstance(item, ImageContent):
128+
content.append(_convert_image_content_to_bedrock_format(item))
129+
else:
130+
err_msg = "Unsupported content type in tool call result"
131+
raise ValueError(err_msg)
103132

104133
tool_results.append(
105134
{
106135
"toolResult": {
107-
"toolUseId": result.origin.id,
136+
"toolUseId": tool_call_result.origin.id,
108137
"content": content,
109-
**({"status": "error"} if result.error else {}),
138+
**({"status": "error"} if tool_call_result.error else {}),
110139
}
111140
}
112141
)
@@ -217,16 +246,7 @@ def _format_text_image_message(message: ChatMessage) -> dict[str, Any]:
217246
if message.is_from(ChatRole.ASSISTANT):
218247
err_msg = "Image content is not supported for assistant messages"
219248
raise ValueError(err_msg)
220-
221-
image_format = part.mime_type.split("/")[-1] if part.mime_type else None
222-
if image_format not in IMAGE_SUPPORTED_FORMATS:
223-
err_msg = (
224-
f"Unsupported image format: {image_format}. "
225-
f"Bedrock supports the following image formats: {IMAGE_SUPPORTED_FORMATS}"
226-
)
227-
raise ValueError(err_msg)
228-
source = {"bytes": base64.b64decode(part.base64_image)}
229-
bedrock_content_blocks.append({"image": {"format": image_format, "source": source}})
249+
bedrock_content_blocks.append(_convert_image_content_to_bedrock_format(part))
230250

231251
return {"role": message.role.value, "content": bedrock_content_blocks}
232252

integrations/amazon_bedrock/tests/test_chat_generator.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33

44
import pytest
55
from haystack import Pipeline
6+
from haystack.components.agents import Agent
67
from haystack.components.generators.utils import print_streaming_chunk
78
from haystack.components.tools import ToolInvoker
8-
from haystack.dataclasses import ChatMessage, ChatRole, ImageContent, StreamingChunk, ToolCall
9-
from haystack.tools import Tool, Toolset
9+
from haystack.dataclasses import ChatMessage, ChatRole, ImageContent, StreamingChunk, TextContent, ToolCall
10+
from haystack.tools import Tool, Toolset, create_tool_from_function
1011

1112
from haystack_integrations.components.generators.amazon_bedrock import AmazonBedrockChatGenerator
1213

@@ -514,6 +515,30 @@ def test_run_with_image_input(self, model_name, test_files_path):
514515
assert first_reply.text
515516
assert "apple" in first_reply.text.lower()
516517

518+
@pytest.mark.parametrize("model_name", MODELS_TO_TEST_WITH_IMAGE_INPUT)
519+
def test_live_run_agent_with_images_in_tool_result(self, model_name, test_files_path):
520+
def retrieve_image():
521+
return [
522+
TextContent("Here is the retrieved image."),
523+
ImageContent.from_file_path(test_files_path / "apple.jpg", size=(100, 100)),
524+
]
525+
526+
image_retriever_tool = create_tool_from_function(
527+
name="retrieve_image", description="Tool to retrieve an image", function=retrieve_image
528+
)
529+
image_retriever_tool.outputs_to_string = {"raw_result": True}
530+
531+
agent = Agent(
532+
chat_generator=AmazonBedrockChatGenerator(model=model_name),
533+
system_prompt="You are an Agent that can retrieve images and describe them.",
534+
tools=[image_retriever_tool],
535+
)
536+
537+
user_message = ChatMessage.from_user("Retrieve the image and describe it in max 5 words.")
538+
result = agent.run(messages=[user_message])
539+
540+
assert "apple" in result["last_message"].text.lower()
541+
517542
@pytest.mark.parametrize("model_name", MODELS_TO_TEST)
518543
def test_default_inference_with_streaming(self, model_name, chat_messages):
519544
streaming_callback_called = False

integrations/amazon_bedrock/tests/test_chat_generator_utils.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
ImageContent,
1010
ReasoningContent,
1111
StreamingChunk,
12+
TextContent,
1213
ToolCall,
1314
ToolCallDelta,
1415
)
@@ -120,6 +121,52 @@ def test_format_messages(self):
120121
{"role": "assistant", "content": [{"text": "The weather in Paris is sunny and 25°C."}]},
121122
]
122123

124+
def test_format_messages_tool_result_with_image(self):
125+
base64_image = (
126+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="
127+
)
128+
129+
messages = [
130+
ChatMessage.from_user("Retrieve the image and describe it in max 5 words."),
131+
ChatMessage.from_assistant(
132+
tool_calls=[ToolCall(id="123", tool_name="image_retriever", arguments={"query": "random query"})]
133+
),
134+
ChatMessage.from_tool(
135+
tool_result=[
136+
TextContent("Here's the retrieved image"),
137+
ImageContent(base64_image=base64_image, mime_type="image/png"),
138+
],
139+
origin=ToolCall(id="123", tool_name="image_retriever", arguments={"query": "random query"}),
140+
),
141+
ChatMessage.from_assistant("Beautiful landscape with mountains"),
142+
]
143+
formatted_system_prompts, formatted_messages = _format_messages(messages)
144+
assert formatted_system_prompts == []
145+
assert formatted_messages == [
146+
{"role": "user", "content": [{"text": "Retrieve the image and describe it in max 5 words."}]},
147+
{
148+
"role": "assistant",
149+
"content": [
150+
{"toolUse": {"toolUseId": "123", "name": "image_retriever", "input": {"query": "random query"}}}
151+
],
152+
},
153+
{
154+
"role": "user",
155+
"content": [
156+
{
157+
"toolResult": {
158+
"toolUseId": "123",
159+
"content": [
160+
{"text": "Here's the retrieved image"},
161+
{"image": {"format": "png", "source": {"bytes": base64.b64decode(base64_image)}}},
162+
],
163+
}
164+
}
165+
],
166+
},
167+
{"role": "assistant", "content": [{"text": "Beautiful landscape with mountains"}]},
168+
]
169+
123170
def test_format_message_thinking(self):
124171
assistant_message = ChatMessage.from_assistant(
125172
"This is a test message.",

0 commit comments

Comments
 (0)