From bd48abe54aa89f0476d3f0781e9d9c2bedfbee30 Mon Sep 17 00:00:00 2001 From: Bagatur Date: Wed, 28 Aug 2024 16:54:15 -0700 Subject: [PATCH] fmt --- libs/core/langchain_core/messages/utils.py | 34 ++++-- .../tests/unit_tests/messages/test_utils.py | 109 ++++++++++++------ 2 files changed, 97 insertions(+), 46 deletions(-) diff --git a/libs/core/langchain_core/messages/utils.py b/libs/core/langchain_core/messages/utils.py index 07d84cc260e..0a916253397 100644 --- a/libs/core/langchain_core/messages/utils.py +++ b/libs/core/langchain_core/messages/utils.py @@ -910,7 +910,7 @@ def trim_messages( @_runnable_support(supports_single=True) -def format_content_as( +def format_messages_as( messages: Union[MessageLikeRepresentation, Iterable[MessageLikeRepresentation]], *, format: Literal["openai", "anthropic"], @@ -940,7 +940,7 @@ def format_content_as( .. code-block:: python - from langchain_core.messages import format_content_as + from langchain_core.messages import format_messages_as messages = [ SystemMessage, @@ -949,18 +949,18 @@ def format_content_as( AIMessage(), ToolMessage(), ] - oai_strings = format_content_as(messages, format="openai", text="string") - anthropic_blocks = format_content_as(messages, format="anthropic", text="block") + oai_strings = format_messages_as(messages, format="openai", text="string") + anthropic_blocks = format_messages_as(messages, format="anthropic", text="block") .. dropdown:: Chain usage :open: .. code-block:: python - from langchain_core.messages import format_content_as + from langchain_core.messages import format_messages_as from langchain.chat_models import init_chat_model - formatter = format_content_as(format="openai", text="block") + formatter = format_messages_as(format="openai", text="block") llm = init_chat_model() | formatter llm.invoke( @@ -985,10 +985,10 @@ def format_content_as( .. code-block:: python - from langchain_core.messages import format_content_as + from langchain_core.messages import format_messages_as from langchain.chat_models import init_chat_model - formatter = format_content_as(format="openai", text="block") + formatter = format_messages_as(format="openai", text="block") llm = init_chat_model() | formatter # Will contain a single, completed chunk. @@ -1470,6 +1470,24 @@ def _format_contents_as_anthropic( f"content block:\n\n{block}" ) message.content = content # type: ignore[assignment] + + if isinstance(message, AIMessage) and message.tool_calls: + if isinstance(message.content, str): + message.content = [{"type": "text", "text": message.content}] + for tool_call in message.tool_calls: + if not any( + block.get("type") == "tool_use" + and block.get("id") == tool_call["id"] + for block in cast(List[dict], message.content) + ): + message.content.append( + { + "type": "tool_use", + "input": tool_call["args"], + "id": tool_call["id"], + "name": tool_call["name"], + } + ) updated_messages.append(message) return merge_message_runs(updated_messages) diff --git a/libs/core/tests/unit_tests/messages/test_utils.py b/libs/core/tests/unit_tests/messages/test_utils.py index 72bde93cacf..abdd0a7eb5a 100644 --- a/libs/core/tests/unit_tests/messages/test_utils.py +++ b/libs/core/tests/unit_tests/messages/test_utils.py @@ -16,7 +16,7 @@ from langchain_core.messages.utils import ( _bytes_to_b64_str, convert_to_messages, filter_messages, - format_content_as, + format_messages_as, merge_message_runs, trim_messages, ) @@ -564,20 +564,20 @@ def create_base64_image(format: str = "jpeg") -> str: return f"data:image/{format};base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigD//2Q==" # noqa: E501 -def test_format_content_as_single_message() -> None: +def test_format_messages_as_single_message() -> None: message = HumanMessage(content="Hello") - result = format_content_as(message, format="openai", text="string") + result = format_messages_as(message, format="openai", text="string") assert isinstance(result, BaseMessage) assert result.content == "Hello" -def test_format_content_as_multiple_messages() -> None: +def test_format_messages_as_multiple_messages() -> None: messages = [ SystemMessage(content="System message"), HumanMessage(content="Human message"), AIMessage(content="AI message"), ] - result = format_content_as(messages, format="openai", text="string") + result = format_messages_as(messages, format="openai", text="string") assert isinstance(result, list) assert len(result) == 3 assert all(isinstance(msg, BaseMessage) for msg in result) @@ -588,7 +588,7 @@ def test_format_content_as_multiple_messages() -> None: ] -def test_format_content_as_openai_string() -> None: +def test_format_messages_as_openai_string() -> None: messages = [ HumanMessage( content=[ @@ -600,23 +600,23 @@ def test_format_content_as_openai_string() -> None: content=[{"type": "text", "text": "Hi"}, {"type": "text", "text": "there"}] ), ] - result = format_content_as(messages, format="openai", text="string") + result = format_messages_as(messages, format="openai", text="string") assert [msg.content for msg in result] == ["Hello\nWorld", "Hi\nthere"] -def test_format_content_as_openai_block() -> None: +def test_format_messages_as_openai_block() -> None: messages = [ HumanMessage(content="Hello"), AIMessage(content="Hi there"), ] - result = format_content_as(messages, format="openai", text="block") + result = format_messages_as(messages, format="openai", text="block") assert [msg.content for msg in result] == [ [{"type": "text", "text": "Hello"}], [{"type": "text", "text": "Hi there"}], ] -def test_format_content_as_anthropic_string() -> None: +def test_format_messages_as_anthropic_string() -> None: messages = [ HumanMessage( content=[ @@ -628,30 +628,30 @@ def test_format_content_as_anthropic_string() -> None: content=[{"type": "text", "text": "Hi"}, {"type": "text", "text": "there"}] ), ] - result = format_content_as(messages, format="anthropic", text="string") + result = format_messages_as(messages, format="anthropic", text="string") assert [msg.content for msg in result] == ["Hello\nWorld", "Hi\nthere"] -def test_format_content_as_anthropic_block() -> None: +def test_format_messages_as_anthropic_block() -> None: messages = [ HumanMessage(content="Hello"), AIMessage(content="Hi there"), ] - result = format_content_as(messages, format="anthropic", text="block") + result = format_messages_as(messages, format="anthropic", text="block") assert [msg.content for msg in result] == [ [{"type": "text", "text": "Hello"}], [{"type": "text", "text": "Hi there"}], ] -def test_format_content_as_invalid_format() -> None: +def test_format_messages_as_invalid_format() -> None: with pytest.raises(ValueError, match="Unrecognized format="): - format_content_as( + format_messages_as( [HumanMessage(content="Hello")], format="invalid", text="string" ) -def test_format_content_as_openai_image() -> None: +def test_format_messages_as_openai_image() -> None: base64_image = create_base64_image() messages = [ HumanMessage( @@ -661,12 +661,12 @@ def test_format_content_as_openai_image() -> None: ] ) ] - result = format_content_as(messages, format="openai", text="block") + result = format_messages_as(messages, format="openai", text="block") assert result[0].content[1]["type"] == "image_url" assert result[0].content[1]["image_url"]["url"] == base64_image -def test_format_content_as_anthropic_image() -> None: +def test_format_messages_as_anthropic_image() -> None: base64_image = create_base64_image() messages = [ HumanMessage( @@ -676,21 +676,21 @@ def test_format_content_as_anthropic_image() -> None: ] ) ] - result = format_content_as(messages, format="anthropic", text="block") + result = format_messages_as(messages, format="anthropic", text="block") assert result[0].content[1]["type"] == "image" assert result[0].content[1]["source"]["type"] == "base64" assert result[0].content[1]["source"]["media_type"] == "image/jpeg" -def test_format_content_as_tool_message() -> None: +def test_format_messages_as_tool_message() -> None: tool_message = ToolMessage(content="Tool result", tool_call_id="123") - result = format_content_as([tool_message], format="openai", text="block") + result = format_messages_as([tool_message], format="openai", text="block") assert isinstance(result[0], ToolMessage) assert result[0].content == [{"type": "text", "text": "Tool result"}] assert result[0].tool_call_id == "123" -def test_format_content_as_tool_use() -> None: +def test_format_messages_as_tool_use() -> None: messages = [ AIMessage( content=[ @@ -698,21 +698,21 @@ def test_format_content_as_tool_use() -> None: ] ) ] - result = format_content_as(messages, format="openai", text="block") + result = format_messages_as(messages, format="openai", text="block") assert result[0].tool_calls[0]["id"] == "123" assert result[0].tool_calls[0]["name"] == "calculator" assert result[0].tool_calls[0]["args"] == "2+2" -def test_format_content_as_json() -> None: +def test_format_messages_as_json() -> None: json_data = {"key": "value"} messages = [HumanMessage(content=[{"type": "json", "json": json_data}])] - result = format_content_as(messages, format="openai", text="block") + result = format_messages_as(messages, format="openai", text="block") assert result[0].content[0]["type"] == "text" assert json.loads(result[0].content[0]["text"]) == json_data -def test_format_content_as_guard_content() -> None: +def test_format_messages_as_guard_content() -> None: messages = [ HumanMessage( content=[ @@ -723,12 +723,12 @@ def test_format_content_as_guard_content() -> None: ] ) ] - result = format_content_as(messages, format="openai", text="block") + result = format_messages_as(messages, format="openai", text="block") assert result[0].content[0]["type"] == "text" assert result[0].content[0]["text"] == "Protected content" -def test_format_content_as_vertexai_image() -> None: +def test_format_messages_as_vertexai_image() -> None: messages = [ HumanMessage( content=[ @@ -736,7 +736,7 @@ def test_format_content_as_vertexai_image() -> None: ] ) ] - result = format_content_as(messages, format="openai", text="block") + result = format_messages_as(messages, format="openai", text="block") assert result[0].content[0]["type"] == "image_url" assert ( result[0].content[0]["image_url"]["url"] @@ -744,25 +744,27 @@ def test_format_content_as_vertexai_image() -> None: ) -def test_format_content_as_invalid_block() -> None: +def test_format_messages_as_invalid_block() -> None: messages = [HumanMessage(content=[{"type": "invalid", "foo": "bar"}])] with pytest.raises(ValueError, match="Unrecognized content block"): - format_content_as(messages, format="openai", text="block") + format_messages_as(messages, format="openai", text="block") with pytest.raises(ValueError, match="Unrecognized content block"): - format_content_as(messages, format="anthropic", text="block") + format_messages_as(messages, format="anthropic", text="block") -def test_format_content_as_empty_message() -> None: - result = format_content_as(HumanMessage(content=""), format="openai", text="string") +def test_format_messages_as_empty_message() -> None: + result = format_messages_as( + HumanMessage(content=""), format="openai", text="string" + ) assert result.content == "" -def test_format_content_as_empty_list() -> None: - result = format_content_as([], format="openai", text="string") +def test_format_messages_as_empty_list() -> None: + result = format_messages_as([], format="openai", text="string") assert result == [] -def test_format_content_as_mixed_content_types() -> None: +def test_format_messages_as_mixed_content_types() -> None: messages = [ HumanMessage( content=[ @@ -772,8 +774,39 @@ def test_format_content_as_mixed_content_types() -> None: ] ) ] - result = format_content_as(messages, format="openai", text="block") + result = format_messages_as(messages, format="openai", text="block") assert len(result[0].content) == 3 assert isinstance(result[0].content[0], dict) assert isinstance(result[0].content[1], dict) assert isinstance(result[0].content[2], dict) + + +def test_format_messages_as_anthropic_tool_calls() -> None: + message = AIMessage( + "blah", + tool_calls=[ + {"type": "tool_call", "name": "foo", "id": "1", "args": {"bar": "baz"}} + ], + ) + result = format_messages_as(message, format="anthropic", text="string") + assert result.content == [ + {"type": "text", "text": "blah"}, + {"type": "tool_use", "id": "1", "name": "foo", "input": {"bar": "baz"}}, + ] + assert result.tool_calls == message.tool_calls + + +def test_format_messages_as_declarative() -> None: + formatter = format_messages_as(format="openai", text="block") + base64_image = create_base64_image() + messages = [ + HumanMessage( + content=[ + {"type": "text", "text": "Here's an image:"}, + {"type": "image_url", "image_url": {"url": base64_image}}, + ] + ) + ] + result = formatter.invoke(messages) + assert result[0].content[1]["type"] == "image_url" + assert result[0].content[1]["image_url"]["url"] == base64_image