From 83f7f6990f55ca76cd522c06d68a011d2dddcfc3 Mon Sep 17 00:00:00 2001 From: Stefano Fiorucci Date: Wed, 18 Dec 2024 10:16:51 +0100 Subject: [PATCH 1/4] fix: fixes to Bedrock Chat Generator for compatibility with the new ChatMessage (#1250) --- .../amazon_bedrock/chat/chat_generator.py | 10 +++--- .../tests/test_chat_generator.py | 36 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/integrations/amazon_bedrock/src/haystack_integrations/components/generators/amazon_bedrock/chat/chat_generator.py b/integrations/amazon_bedrock/src/haystack_integrations/components/generators/amazon_bedrock/chat/chat_generator.py index 499fe1c24..bcf11414c 100644 --- a/integrations/amazon_bedrock/src/haystack_integrations/components/generators/amazon_bedrock/chat/chat_generator.py +++ b/integrations/amazon_bedrock/src/haystack_integrations/components/generators/amazon_bedrock/chat/chat_generator.py @@ -278,12 +278,10 @@ def extract_replies_from_response(self, response_body: Dict[str, Any]) -> List[C # Process each content block separately for content_block in content_blocks: if "text" in content_block: - replies.append(ChatMessage.from_assistant(content=content_block["text"], meta=base_meta.copy())) + replies.append(ChatMessage.from_assistant(content_block["text"], meta=base_meta.copy())) elif "toolUse" in content_block: replies.append( - ChatMessage.from_assistant( - content=json.dumps(content_block["toolUse"]), meta=base_meta.copy() - ) + ChatMessage.from_assistant(json.dumps(content_block["toolUse"]), meta=base_meta.copy()) ) return replies @@ -334,9 +332,9 @@ def process_streaming_response( pass tool_content = json.dumps(current_tool_use) - replies.append(ChatMessage.from_assistant(content=tool_content, meta=base_meta.copy())) + replies.append(ChatMessage.from_assistant(tool_content, meta=base_meta.copy())) elif current_content: - replies.append(ChatMessage.from_assistant(content=current_content, meta=base_meta.copy())) + replies.append(ChatMessage.from_assistant(current_content, meta=base_meta.copy())) elif "messageStop" in event: # not 100% correct for multiple messages but no way around it diff --git a/integrations/amazon_bedrock/tests/test_chat_generator.py b/integrations/amazon_bedrock/tests/test_chat_generator.py index 8eb29729c..c2122163c 100644 --- a/integrations/amazon_bedrock/tests/test_chat_generator.py +++ b/integrations/amazon_bedrock/tests/test_chat_generator.py @@ -163,9 +163,9 @@ def test_default_inference_params(self, model_name, chat_messages): first_reply = replies[0] assert isinstance(first_reply, ChatMessage), "First reply is not a ChatMessage instance" - assert first_reply.content, "First reply has no content" + assert first_reply.text, "First reply has no content" assert ChatMessage.is_from(first_reply, ChatRole.ASSISTANT), "First reply is not from the assistant" - assert "paris" in first_reply.content.lower(), "First reply does not contain 'paris'" + assert "paris" in first_reply.text.lower(), "First reply does not contain 'paris'" assert first_reply.meta, "First reply has no metadata" if first_reply.meta and "usage" in first_reply.meta: @@ -197,9 +197,9 @@ def streaming_callback(chunk: StreamingChunk): first_reply = replies[0] assert isinstance(first_reply, ChatMessage), "First reply is not a ChatMessage instance" - assert first_reply.content, "First reply has no content" + assert first_reply.text, "First reply has no content" assert ChatMessage.is_from(first_reply, ChatRole.ASSISTANT), "First reply is not from the assistant" - assert "paris" in first_reply.content.lower(), "First reply does not contain 'paris'" + assert "paris" in first_reply.text.lower(), "First reply does not contain 'paris'" assert first_reply.meta, "First reply has no metadata" @pytest.mark.parametrize("model_name", MODELS_TO_TEST_WITH_TOOLS) @@ -246,7 +246,7 @@ def test_tools_use(self, model_name): first_reply = replies[0] assert isinstance(first_reply, ChatMessage), "First reply is not a ChatMessage instance" - assert first_reply.content, "First reply has no content" + assert first_reply.text, "First reply has no content" assert ChatMessage.is_from(first_reply, ChatRole.ASSISTANT), "First reply is not from the assistant" assert first_reply.meta, "First reply has no metadata" @@ -254,9 +254,9 @@ def test_tools_use(self, model_name): if len(replies) > 1: second_reply = replies[1] assert isinstance(second_reply, ChatMessage), "Second reply is not a ChatMessage instance" - assert second_reply.content, "Second reply has no content" + assert second_reply.text, "Second reply has no content" assert ChatMessage.is_from(second_reply, ChatRole.ASSISTANT), "Second reply is not from the assistant" - tool_call = json.loads(second_reply.content) + tool_call = json.loads(second_reply.text) assert "toolUseId" in tool_call, "Tool call does not contain 'toolUseId' key" assert tool_call["name"] == "top_song", f"Tool call {tool_call} does not contain the correct 'name' value" assert "input" in tool_call, f"Tool call {tool_call} does not contain 'input' key" @@ -266,7 +266,7 @@ def test_tools_use(self, model_name): else: # case where the model returns the tool call as the first message # double check that the tool call is correct - tool_call = json.loads(first_reply.content) + tool_call = json.loads(first_reply.text) assert "toolUseId" in tool_call, "Tool call does not contain 'toolUseId' key" assert tool_call["name"] == "top_song", f"Tool call {tool_call} does not contain the correct 'name' value" assert "input" in tool_call, f"Tool call {tool_call} does not contain 'input' key" @@ -318,7 +318,7 @@ def test_tools_use_with_streaming(self, model_name): first_reply = replies[0] assert isinstance(first_reply, ChatMessage), "First reply is not a ChatMessage instance" - assert first_reply.content, "First reply has no content" + assert first_reply.text, "First reply has no content" assert ChatMessage.is_from(first_reply, ChatRole.ASSISTANT), "First reply is not from the assistant" assert first_reply.meta, "First reply has no metadata" @@ -326,9 +326,9 @@ def test_tools_use_with_streaming(self, model_name): if len(replies) > 1: second_reply = replies[1] assert isinstance(second_reply, ChatMessage), "Second reply is not a ChatMessage instance" - assert second_reply.content, "Second reply has no content" + assert second_reply.text, "Second reply has no content" assert ChatMessage.is_from(second_reply, ChatRole.ASSISTANT), "Second reply is not from the assistant" - tool_call = json.loads(second_reply.content) + tool_call = json.loads(second_reply.text) assert "toolUseId" in tool_call, "Tool call does not contain 'toolUseId' key" assert tool_call["name"] == "top_song", f"Tool call {tool_call} does not contain the correct 'name' value" assert "input" in tool_call, f"Tool call {tool_call} does not contain 'input' key" @@ -338,7 +338,7 @@ def test_tools_use_with_streaming(self, model_name): else: # case where the model returns the tool call as the first message # double check that the tool call is correct - tool_call = json.loads(first_reply.content) + tool_call = json.loads(first_reply.text) assert "toolUseId" in tool_call, "Tool call does not contain 'toolUseId' key" assert tool_call["name"] == "top_song", f"Tool call {tool_call} does not contain the correct 'name' value" assert "input" in tool_call, f"Tool call {tool_call} does not contain 'input' key" @@ -361,7 +361,7 @@ def test_extract_replies_from_response(self, mock_boto3_session): replies = generator.extract_replies_from_response(text_response) assert len(replies) == 1 - assert replies[0].content == "This is a test response" + assert replies[0].text == "This is a test response" assert replies[0].role == ChatRole.ASSISTANT assert replies[0].meta["model"] == "anthropic.claude-3-5-sonnet-20240620-v1:0" assert replies[0].meta["finish_reason"] == "complete" @@ -381,7 +381,7 @@ def test_extract_replies_from_response(self, mock_boto3_session): replies = generator.extract_replies_from_response(tool_response) assert len(replies) == 1 - tool_content = json.loads(replies[0].content) + tool_content = json.loads(replies[0].text) assert tool_content["toolUseId"] == "123" assert tool_content["name"] == "test_tool" assert tool_content["input"] == {"key": "value"} @@ -405,8 +405,8 @@ def test_extract_replies_from_response(self, mock_boto3_session): replies = generator.extract_replies_from_response(mixed_response) assert len(replies) == 2 - assert replies[0].content == "Let me help you with that. I'll use the search tool to find the answer." - tool_content = json.loads(replies[1].content) + assert replies[0].text == "Let me help you with that. I'll use the search tool to find the answer." + tool_content = json.loads(replies[1].text) assert tool_content["toolUseId"] == "456" assert tool_content["name"] == "search_tool" assert tool_content["input"] == {"query": "test"} @@ -446,13 +446,13 @@ def test_callback(chunk: StreamingChunk): # Verify final replies assert len(replies) == 2 # Check text reply - assert replies[0].content == "Let me help you." + assert replies[0].text == "Let me help you." assert replies[0].meta["model"] == "anthropic.claude-3-5-sonnet-20240620-v1:0" assert replies[0].meta["finish_reason"] == "complete" assert replies[0].meta["usage"] == {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30} # Check tool use reply - tool_content = json.loads(replies[1].content) + tool_content = json.loads(replies[1].text) assert tool_content["toolUseId"] == "123" assert tool_content["name"] == "search_tool" assert tool_content["input"] == {"query": "test"} From be3789f3ddba3b87b1ac8f7ffbe77f951803f0dc Mon Sep 17 00:00:00 2001 From: HaystackBot Date: Wed, 18 Dec 2024 09:17:58 +0000 Subject: [PATCH 2/4] Update the changelog --- integrations/amazon_bedrock/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/integrations/amazon_bedrock/CHANGELOG.md b/integrations/amazon_bedrock/CHANGELOG.md index 46eeea7b7..6a15d4ad2 100644 --- a/integrations/amazon_bedrock/CHANGELOG.md +++ b/integrations/amazon_bedrock/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [integrations/amazon_bedrock-v2.1.1] - 2024-12-18 + +### ๐Ÿ› Bug Fixes + +- Fixes to Bedrock Chat Generator for compatibility with the new ChatMessage (#1250) + + ## [integrations/amazon_bedrock-v2.1.0] - 2024-12-11 ### ๐Ÿš€ Features From b12461d40f907bdf5daf5862c16b4098aa3f9344 Mon Sep 17 00:00:00 2001 From: Stefano Fiorucci Date: Wed, 18 Dec 2024 15:41:16 +0100 Subject: [PATCH 3/4] fix: make Anthropic compatible with new `ChatMessage`; fix prompt caching tests (#1252) * make Anthropic compatible with new chatmessage; fix prompt caching tests * rm print --- .../generators/anthropic/chat/chat_generator.py | 13 ++++++++++--- integrations/anthropic/tests/test_chat_generator.py | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/integrations/anthropic/src/haystack_integrations/components/generators/anthropic/chat/chat_generator.py b/integrations/anthropic/src/haystack_integrations/components/generators/anthropic/chat/chat_generator.py index 43b50495c..56a740146 100644 --- a/integrations/anthropic/src/haystack_integrations/components/generators/anthropic/chat/chat_generator.py +++ b/integrations/anthropic/src/haystack_integrations/components/generators/anthropic/chat/chat_generator.py @@ -1,4 +1,3 @@ -import dataclasses import json from typing import Any, Callable, ClassVar, Dict, List, Optional, Union @@ -275,8 +274,16 @@ def _convert_to_anthropic_format(self, messages: List[ChatMessage]) -> List[Dict """ anthropic_formatted_messages = [] for m in messages: - message_dict = dataclasses.asdict(m) - formatted_message = {k: v for k, v in message_dict.items() if k in {"role", "content"} and v} + message_dict = m.to_dict() + formatted_message = {} + + # legacy format + if "role" in message_dict and "content" in message_dict: + formatted_message = {k: v for k, v in message_dict.items() if k in {"role", "content"} and v} + # new format + elif "_role" in message_dict and "_content" in message_dict: + formatted_message = {"role": m.role.value, "content": m.text} + if m.is_from(ChatRole.SYSTEM): # system messages are treated differently and MUST be in the format expected by the Anthropic API # remove role and content from the message dict, add type and text diff --git a/integrations/anthropic/tests/test_chat_generator.py b/integrations/anthropic/tests/test_chat_generator.py index 9a111fc9d..36622ecd9 100644 --- a/integrations/anthropic/tests/test_chat_generator.py +++ b/integrations/anthropic/tests/test_chat_generator.py @@ -428,5 +428,5 @@ def test_prompt_caching(self, cache_enabled): or token_usage.get("cache_read_input_tokens") > 1024 ) else: - assert "cache_creation_input_tokens" not in token_usage - assert "cache_read_input_tokens" not in token_usage + assert token_usage["cache_creation_input_tokens"] == 0 + assert token_usage["cache_read_input_tokens"] == 0 From 06e9c5615bdba322f769a69717cf2e5ff46bc494 Mon Sep 17 00:00:00 2001 From: HaystackBot Date: Wed, 18 Dec 2024 14:44:01 +0000 Subject: [PATCH 4/4] Update the changelog --- integrations/anthropic/CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/integrations/anthropic/CHANGELOG.md b/integrations/anthropic/CHANGELOG.md index a7cdc7d09..c14a8032d 100644 --- a/integrations/anthropic/CHANGELOG.md +++ b/integrations/anthropic/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -## [unreleased] +## [integrations/anthropic-v1.2.1] - 2024-12-18 + +### ๐Ÿ› Bug Fixes + +- Make Anthropic compatible with new `ChatMessage`; fix prompt caching tests (#1252) ### โš™๏ธ CI @@ -9,10 +13,13 @@ ### ๐Ÿงน Chores - Update ruff linting scripts and settings (#1105) +- Fix linting/isort (#1215) ### ๐ŸŒ€ Miscellaneous - Add AnthropicVertexChatGenerator component (#1192) +- Docs: add AnthropicVertexChatGenerator to pydoc (#1221) +- Chore: use `text` instead of `content` for `ChatMessage` in Cohere and Anthropic (#1237) ## [integrations/anthropic-v1.1.0] - 2024-09-20