import logging from typing import cast from browser_use.agent.service import Agent from browser_use.llm.anthropic.chat import ChatAnthropic from browser_use.llm.anthropic.serializer import AnthropicMessageSerializer, NonSystemMessage from browser_use.llm.messages import ( AssistantMessage, BaseMessage, ContentPartImageParam, ContentPartTextParam, Function, ImageURL, SystemMessage, ToolCall, UserMessage, ) logger = logging.getLogger(__name__) class TestAnthropicCache: """Comprehensive test for Anthropic cache serialization.""" def test_cache_basic_functionality(self): """Test basic cache functionality for all message types.""" # Test cache with different message types messages: list[BaseMessage] = [ SystemMessage(content='System message!', cache=True), UserMessage(content='User message!', cache=True), AssistantMessage(content='Assistant message!', cache=False), ] anthropic_messages, system_message = AnthropicMessageSerializer.serialize_messages(messages) assert len(anthropic_messages) == 2 assert isinstance(system_message, list) assert isinstance(anthropic_messages[0]['content'], list) assert isinstance(anthropic_messages[1]['content'], str) # Test cache with assistant message agent_messages: list[BaseMessage] = [ SystemMessage(content='System message!'), UserMessage(content='User message!'), AssistantMessage(content='Assistant message!', cache=True), ] anthropic_messages, system_message = AnthropicMessageSerializer.serialize_messages(agent_messages) assert isinstance(system_message, str) assert isinstance(anthropic_messages[0]['content'], str) assert isinstance(anthropic_messages[1]['content'], list) def test_cache_with_tool_calls(self): """Test cache functionality with tool calls.""" tool_call = ToolCall(id='test_id', function=Function(name='test_function', arguments='{"arg": "value"}')) # Assistant with tool calls and cache assistant_with_tools = AssistantMessage(content='Assistant with tools', tool_calls=[tool_call], cache=True) messages, _ = AnthropicMessageSerializer.serialize_messages([assistant_with_tools]) assert len(messages) == 1 assert isinstance(messages[0]['content'], list) # Should have both text and tool_use blocks assert len(messages[0]['content']) >= 2 def test_cache_with_images(self): """Test cache functionality with image content.""" user_with_image = UserMessage( content=[ ContentPartTextParam(text='Here is an image:', type='text'), ContentPartImageParam(image_url=ImageURL(url='https://example.com/image.jpg'), type='image_url'), ], cache=True, ) messages, _ = AnthropicMessageSerializer.serialize_messages([user_with_image]) assert len(messages) == 1 assert isinstance(messages[0]['content'], list) assert len(messages[0]['content']) == 2 def test_cache_with_base64_images(self): """Test cache functionality with base64 images.""" base64_url = 'data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' user_with_base64 = UserMessage( content=[ ContentPartTextParam(text='Base64 image:', type='text'), ContentPartImageParam(image_url=ImageURL(url=base64_url), type='image_url'), ], cache=True, ) messages, _ = AnthropicMessageSerializer.serialize_messages([user_with_base64]) assert len(messages) == 1 assert isinstance(messages[0]['content'], list) def test_cache_content_types(self): """Test different content types with cache.""" # String content with cache should become list user_string_cached = UserMessage(content='String message', cache=True) messages, _ = AnthropicMessageSerializer.serialize_messages([user_string_cached]) assert isinstance(messages[0]['content'], list) # String content without cache should remain string user_string_no_cache = UserMessage(content='String message', cache=False) messages, _ = AnthropicMessageSerializer.serialize_messages([user_string_no_cache]) assert isinstance(messages[0]['content'], str) # List content maintains list format regardless of cache user_list_cached = UserMessage(content=[ContentPartTextParam(text='List message', type='text')], cache=True) messages, _ = AnthropicMessageSerializer.serialize_messages([user_list_cached]) assert isinstance(messages[0]['content'], list) user_list_no_cache = UserMessage(content=[ContentPartTextParam(text='List message', type='text')], cache=False) messages, _ = AnthropicMessageSerializer.serialize_messages([user_list_no_cache]) assert isinstance(messages[0]['content'], list) def test_assistant_cache_empty_content(self): """Test AssistantMessage with empty content and cache.""" # With cache assistant_empty_cached = AssistantMessage(content=None, cache=True) messages, _ = AnthropicMessageSerializer.serialize_messages([assistant_empty_cached]) assert len(messages) == 1 assert isinstance(messages[0]['content'], list) # Without cache assistant_empty_no_cache = AssistantMessage(content=None, cache=False) messages, _ = AnthropicMessageSerializer.serialize_messages([assistant_empty_no_cache]) assert len(messages) == 1 assert isinstance(messages[0]['content'], str) def test_mixed_cache_scenarios(self): """Test various combinations of cached and non-cached messages.""" messages_list: list[BaseMessage] = [ SystemMessage(content='System with cache', cache=True), UserMessage(content='User with cache', cache=True), AssistantMessage(content='Assistant without cache', cache=False), UserMessage(content='User without cache', cache=False), AssistantMessage(content='Assistant with cache', cache=True), ] serialized_messages, system_message = AnthropicMessageSerializer.serialize_messages(messages_list) # Check system message is cached (becomes list) assert isinstance(system_message, list) # Check serialized messages assert len(serialized_messages) == 4 # User with cache should be list assert isinstance(serialized_messages[0]['content'], list) # Assistant without cache should be string assert isinstance(serialized_messages[1]['content'], str) # User without cache should be string assert isinstance(serialized_messages[2]['content'], str) # Assistant with cache should be list assert isinstance(serialized_messages[3]['content'], list) def test_system_message_cache_behavior(self): """Test SystemMessage specific cache behavior.""" # With cache system_cached = SystemMessage(content='System message with cache', cache=True) result = AnthropicMessageSerializer.serialize(system_cached) assert isinstance(result, SystemMessage) # Test serialization to string format serialized_content = AnthropicMessageSerializer._serialize_content_to_str(result.content, use_cache=True) assert isinstance(serialized_content, list) # Without cache system_no_cache = SystemMessage(content='System message without cache', cache=False) result = AnthropicMessageSerializer.serialize(system_no_cache) assert isinstance(result, SystemMessage) serialized_content = AnthropicMessageSerializer._serialize_content_to_str(result.content, use_cache=False) assert isinstance(serialized_content, str) def test_agent_messages_integration(self): """Test integration with actual agent messages.""" agent = Agent(task='Hello, world!', llm=ChatAnthropic('')) messages = agent.message_manager.get_messages() anthropic_messages, system_message = AnthropicMessageSerializer.serialize_messages(messages) # System message should be properly handled assert system_message is not None def test_cache_cleaning_last_message_only(self): """Test that only the last cache=True message remains cached.""" # Create multiple messages with cache=True messages_list: list[BaseMessage] = [ UserMessage(content='First user message', cache=True), AssistantMessage(content='First assistant message', cache=True), UserMessage(content='Second user message', cache=True), AssistantMessage(content='Second assistant message', cache=False), UserMessage(content='Third user message', cache=True), # This should be the only one cached ] # Test the cleaning method directly (only accepts non-system messages) normal_messages = cast(list[NonSystemMessage], [msg for msg in messages_list if not isinstance(msg, SystemMessage)]) cleaned_messages = AnthropicMessageSerializer._clean_cache_messages(normal_messages) # Verify only the last cache=True message remains cached assert not cleaned_messages[0].cache # First user message should be uncached assert not cleaned_messages[1].cache # First assistant message should be uncached assert not cleaned_messages[2].cache # Second user message should be uncached assert not cleaned_messages[3].cache # Second assistant message was already uncached assert cleaned_messages[4].cache # Third user message should remain cached # Test through serialize_messages serialized_messages, system_message = AnthropicMessageSerializer.serialize_messages(messages_list) # Count how many messages have list content (indicating caching) cached_content_count = sum(1 for msg in serialized_messages if isinstance(msg['content'], list)) # Only one message should have cached content assert cached_content_count == 1 # The last message should be the cached one assert isinstance(serialized_messages[-1]['content'], list) def test_cache_cleaning_with_system_message(self): """Test that system messages are not affected by cache cleaning logic.""" messages_list: list[BaseMessage] = [ SystemMessage(content='System message', cache=True), # System messages are handled separately UserMessage(content='First user message', cache=True), AssistantMessage(content='Assistant message', cache=True), # This should be the only normal message cached ] # Test through serialize_messages to see the full integration serialized_messages, system_message = AnthropicMessageSerializer.serialize_messages(messages_list) # System message should be cached assert isinstance(system_message, list) # Only one normal message should have cached content (the last one) cached_content_count = sum(1 for msg in serialized_messages if isinstance(msg['content'], list)) assert cached_content_count == 1 # The last message should be the cached one assert isinstance(serialized_messages[-1]['content'], list) def test_cache_cleaning_no_cached_messages(self): """Test that messages without cache=True are not affected.""" normal_messages_list = [ UserMessage(content='User message 1', cache=False), AssistantMessage(content='Assistant message 1', cache=False), UserMessage(content='User message 2', cache=False), ] cleaned_messages = AnthropicMessageSerializer._clean_cache_messages(normal_messages_list) # All messages should remain uncached for msg in cleaned_messages: assert not msg.cache def test_max_4_cache_blocks(self): """Test that the max number of cache blocks is 4.""" agent = Agent(task='Hello, world!', llm=ChatAnthropic('')) messages = agent.message_manager.get_messages() anthropic_messages, system_message = AnthropicMessageSerializer.serialize_messages(messages) logger.info(anthropic_messages) logger.info(system_message) if __name__ == '__main__': test_instance = TestAnthropicCache() test_instance.test_cache_basic_functionality() test_instance.test_cache_with_tool_calls() test_instance.test_cache_with_images() test_instance.test_cache_with_base64_images() test_instance.test_cache_content_types() test_instance.test_assistant_cache_empty_content() test_instance.test_mixed_cache_scenarios() test_instance.test_system_message_cache_behavior() test_instance.test_agent_messages_integration() test_instance.test_cache_cleaning_last_message_only() test_instance.test_cache_cleaning_with_system_message() test_instance.test_cache_cleaning_no_cached_messages() test_instance.test_max_4_cache_blocks() print('All cache tests passed!')