From 7b0334bdb46d634592ea057385f1926b1994c4b5 Mon Sep 17 00:00:00 2001 From: Josh Rogers Date: Mon, 9 Feb 2026 15:20:45 -0500 Subject: [PATCH] clean up --- libs/langgraph/langgraph/graph/message.py | 44 +- .../tests/test_messages_state.py.bak | 505 ------------------ 2 files changed, 16 insertions(+), 533 deletions(-) delete mode 100644 libs/langgraph/tests/test_messages_state.py.bak diff --git a/libs/langgraph/langgraph/graph/message.py b/libs/langgraph/langgraph/graph/message.py index 14b7037a7..36ba4ea8e 100644 --- a/libs/langgraph/langgraph/graph/message.py +++ b/libs/langgraph/langgraph/graph/message.py @@ -84,19 +84,10 @@ def add_messages( Must have `langchain-core>=0.3.11` installed to use this feature. - mode: Controls how message updates and removals are handled. Options are: - - `allow_everything` (default): Allows adding new messages, updating - existing messages by ID, and removing messages. This is the standard behavior. - - `append_only`: Only allows adding new messages. If a message in `right` - has the same ID as a message in `left` (update or removal), a `ValueError` - will be raised. This mode is useful when you want to prevent any modification - of message history. - Returns: A new list of messages with the messages from `right` merged into `left`. If a message in `right` has the same ID as a message in `left`, the - message from `right` will replace the message from `left` (in - `allow_everything` mode) or raise a `ValueError` (in `append_only` mode). + message from `right` will replace the message from `left`. Example: Basic usage ```python @@ -310,12 +301,8 @@ def validate_messages_append_only( if not isinstance(input_messages, list): input_messages = [input_messages] - # Convert to messages to get proper IDs - try: - converted_messages = convert_to_messages(input_messages) - except Exception: - # If conversion fails, let it through - the reducer will handle it - return + # Convert to messages to get proper IDs (let conversion errors propagate) + converted_messages = convert_to_messages(input_messages) # Check each input message for msg in converted_messages: @@ -326,18 +313,19 @@ def validate_messages_append_only( "External inputs must only append new messages." ) - # Check for updates or removals of existing messages - if msg.id and msg.id in existing_ids: - if isinstance(msg, RemoveMessage): - raise ValueError( - f"Cannot remove existing message with ID '{msg.id}' in append_only mode. " - "External inputs must only append new messages." - ) - else: - raise ValueError( - f"Cannot update existing message with ID '{msg.id}' in append_only mode. " - "External inputs must only append new messages." - ) + # Check for removals of existing messages + if isinstance(msg, RemoveMessage) and msg.id in existing_ids: + raise ValueError( + f"Cannot remove existing message with ID '{msg.id}' in append_only mode. " + "External inputs must only append new messages." + ) + + # Check for updates of existing messages + if not isinstance(msg, RemoveMessage) and msg.id and msg.id in existing_ids: + raise ValueError( + f"Cannot update existing message with ID '{msg.id}' in append_only mode. " + "External inputs must only append new messages." + ) @deprecated( diff --git a/libs/langgraph/tests/test_messages_state.py.bak b/libs/langgraph/tests/test_messages_state.py.bak deleted file mode 100644 index 7a694213f..000000000 --- a/libs/langgraph/tests/test_messages_state.py.bak +++ /dev/null @@ -1,505 +0,0 @@ -from typing import Annotated -from uuid import UUID - -import langchain_core -import pytest -from langchain_core.messages import ( - AIMessage, - AnyMessage, - HumanMessage, - RemoveMessage, - SystemMessage, - ToolMessage, -) -from pydantic import BaseModel -from typing_extensions import TypedDict - -from langgraph.constants import END, START -from langgraph.graph import add_messages -from langgraph.graph.message import REMOVE_ALL_MESSAGES, MessagesState, push_message -from langgraph.graph.state import StateGraph -from tests.messages import _AnyIdHumanMessage - -_, CORE_MINOR, CORE_PATCH = ( - int("".join(c for c in v if c.isdigit())) - for v in langchain_core.__version__.split(".") -) - - -def test_add_single_message(): - left = [HumanMessage(content="Hello", id="1")] - right = AIMessage(content="Hi there!", id="2") - result = add_messages(left, right) - expected_result = [ - HumanMessage(content="Hello", id="1"), - AIMessage(content="Hi there!", id="2"), - ] - assert result == expected_result - - -def test_add_multiple_messages(): - left = [HumanMessage(content="Hello", id="1")] - right = [ - AIMessage(content="Hi there!", id="2"), - SystemMessage(content="System message", id="3"), - ] - result = add_messages(left, right) - expected_result = [ - HumanMessage(content="Hello", id="1"), - AIMessage(content="Hi there!", id="2"), - SystemMessage(content="System message", id="3"), - ] - assert result == expected_result - - -def test_update_existing_message(): - left = [HumanMessage(content="Hello", id="1")] - right = HumanMessage(content="Hello again", id="1") - result = add_messages(left, right) - expected_result = [HumanMessage(content="Hello again", id="1")] - assert result == expected_result - - -def test_missing_ids(): - left = [HumanMessage(content="Hello")] - right = [AIMessage(content="Hi there!")] - result = add_messages(left, right) - assert len(result) == 2 - assert all(isinstance(m.id, str) and UUID(m.id, version=4) for m in result) - - -def test_duplicates_in_input(): - left = [] - right = [ - AIMessage(id="1", content="Hi there!"), - AIMessage(id="1", content="Hi there again!"), - ] - result = add_messages(left, right) - assert len(result) == 1 - assert result[0].id == "1" - assert result[0].content == "Hi there again!" - - -def test_duplicates_in_input_with_remove(): - left = [AIMessage(id="1", content="Hello!")] - right = [ - RemoveMessage(id="1"), - AIMessage(id="1", content="Hi there!"), - AIMessage(id="1", content="Hi there again!"), - ] - result = add_messages(left, right) - assert len(result) == 1 - assert result[0].id == "1" - assert result[0].content == "Hi there again!" - - -def test_remove_message(): - left = [ - HumanMessage(content="Hello", id="1"), - AIMessage(content="Hi there!", id="2"), - ] - right = RemoveMessage(id="2") - result = add_messages(left, right) - expected_result = [HumanMessage(content="Hello", id="1")] - assert result == expected_result - - -def test_duplicate_remove_message(): - left = [ - HumanMessage(content="Hello", id="1"), - AIMessage(content="Hi there!", id="2"), - ] - right = [RemoveMessage(id="2"), RemoveMessage(id="2")] - result = add_messages(left, right) - expected_result = [HumanMessage(content="Hello", id="1")] - assert result == expected_result - - -def test_remove_nonexistent_message(): - left = [HumanMessage(content="Hello", id="1")] - right = RemoveMessage(id="2") - with pytest.raises( - ValueError, match="Attempting to delete a message with an ID that doesn't exist" - ): - add_messages(left, right) - - -def test_mixed_operations(): - left = [ - HumanMessage(content="Hello", id="1"), - AIMessage(content="Hi there!", id="2"), - ] - right = [ - HumanMessage(content="Updated hello", id="1"), - RemoveMessage(id="2"), - SystemMessage(content="New message", id="3"), - ] - result = add_messages(left, right) - expected_result = [ - HumanMessage(content="Updated hello", id="1"), - SystemMessage(content="New message", id="3"), - ] - assert result == expected_result - - -def test_empty_inputs(): - assert add_messages([], []) == [] - assert add_messages([], [HumanMessage(content="Hello", id="1")]) == [ - HumanMessage(content="Hello", id="1") - ] - assert add_messages([HumanMessage(content="Hello", id="1")], []) == [ - HumanMessage(content="Hello", id="1") - ] - - -def test_non_list_inputs(): - left = HumanMessage(content="Hello", id="1") - right = AIMessage(content="Hi there!", id="2") - result = add_messages(left, right) - expected_result = [ - HumanMessage(content="Hello", id="1"), - AIMessage(content="Hi there!", id="2"), - ] - assert result == expected_result - - -def test_delete_all(): - left = [ - HumanMessage(content="Hello", id="1"), - AIMessage(content="Hi there!", id="2"), - ] - right = [ - RemoveMessage(id="1"), - RemoveMessage(id="2"), - ] - result = add_messages(left, right) - expected_result = [] - assert result == expected_result - - -class MessagesStatePydantic(BaseModel): - messages: Annotated[list[AnyMessage], add_messages] - - -MESSAGES_STATE_SCHEMAS = [MessagesState, MessagesStatePydantic] - - -@pytest.mark.parametrize("state_schema", MESSAGES_STATE_SCHEMAS) -def test_messages_state(state_schema): - def foo(state): - return {"messages": [HumanMessage("foo")]} - - graph = StateGraph(state_schema) - graph.add_edge(START, "foo") - graph.add_edge("foo", END) - graph.add_node(foo) - - app = graph.compile() - - assert app.invoke({"messages": [("user", "meow")]}) == { - "messages": [ - _AnyIdHumanMessage(content="meow"), - _AnyIdHumanMessage(content="foo"), - ] - } - - -@pytest.mark.skipif( - condition=not ((CORE_MINOR == 3 and CORE_PATCH >= 11) or CORE_MINOR > 3), - reason="Requires langchain_core>=0.3.11.", -) -def test_messages_state_format_openai(): - class State(TypedDict): - messages: Annotated[list[AnyMessage], add_messages(format="langchain-openai")] - - def foo(state): - messages = [ - HumanMessage( - content=[ - { - "type": "text", - "text": "Here's an image:", - "cache_control": {"type": "ephemeral"}, - }, - { - "type": "image", - "source": { - "type": "base64", - "media_type": "image/jpeg", - "data": "1234", - }, - }, - ] - ), - AIMessage( - content=[ - { - "type": "tool_use", - "name": "foo", - "input": {"bar": "baz"}, - "id": "1", - } - ] - ), - HumanMessage( - content=[ - { - "type": "tool_result", - "tool_use_id": "1", - "is_error": False, - "content": [ - { - "type": "image", - "source": { - "type": "base64", - "media_type": "image/jpeg", - "data": "1234", - }, - }, - ], - } - ] - ), - ] - return {"messages": messages} - - expected = [ - HumanMessage(content="meow"), - HumanMessage( - content=[ - {"type": "text", "text": "Here's an image:"}, - { - "type": "image_url", - "image_url": {"url": "data:image/jpeg;base64,1234"}, - }, - ], - ), - AIMessage( - content="", - tool_calls=[ - { - "name": "foo", - "type": "tool_calls", - "args": {"bar": "baz"}, - "id": "1", - } - ], - ), - ToolMessage( - content=[ - { - "type": "image_url", - "image_url": {"url": "data:image/jpeg;base64,1234"}, - } - ], - tool_call_id="1", - ), - ] - - graph = StateGraph(State) - graph.add_edge(START, "foo") - graph.add_edge("foo", END) - graph.add_node(foo) - - app = graph.compile() - - result = app.invoke({"messages": [("user", "meow")]}) - for m in result["messages"]: - m.id = None - assert result == {"messages": expected} - - -def test_remove_all_messages(): - # simple removal - left = [HumanMessage(content="Hello"), AIMessage(content="Hi there!")] - right = [RemoveMessage(id=REMOVE_ALL_MESSAGES)] - result = add_messages(left, right) - assert result == [] - - # removal and update (i.e., overwriting) - left = [HumanMessage(content="Hello"), AIMessage(content="Hi there!")] - right = [ - RemoveMessage(id=REMOVE_ALL_MESSAGES), - HumanMessage(content="Updated hello"), - ] - result = add_messages(left, right) - assert result == [_AnyIdHumanMessage(content="Updated hello")] - - # test removing preceding messages in the right list - left = [HumanMessage(content="Hello"), AIMessage(content="Hi there!")] - right = [ - HumanMessage(content="Updated hello"), - RemoveMessage(id=REMOVE_ALL_MESSAGES), - HumanMessage(content="Updated hi there"), - ] - result = add_messages(left, right) - assert result == [ - _AnyIdHumanMessage(content="Updated hi there"), - ] - - -def test_push_messages_in_graph(): - class MessagesState(TypedDict): - messages: Annotated[list[AnyMessage], add_messages] - - def chat(_: MessagesState) -> MessagesState: - with pytest.raises(ValueError, match="Message ID is required"): - push_message(AIMessage(content="No ID")) - - push_message(AIMessage(content="First", id="1")) - push_message(HumanMessage(content="Second", id="2")) - push_message(AIMessage(content="Third", id="3")) - - builder = StateGraph(MessagesState) - builder.add_node(chat) - builder.add_edge(START, "chat") - - graph = builder.compile() - - messages, values = [], None - for event, chunk in graph.stream( - {"messages": []}, stream_mode=["messages", "values"] - ): - if event == "values": - values = chunk - elif event == "messages": - message, _ = chunk - messages.append(message) - - assert values["messages"] == messages - - -def test_append_only_mode_allows_new_messages(): - """Test that append_only mode allows adding new messages.""" - left = [HumanMessage(content="Hello", id="1")] - right = [AIMessage(content="Hi there!", id="2")] - result = add_messages(left, right, mode="append_only") - expected = [ - HumanMessage(content="Hello", id="1"), - AIMessage(content="Hi there!", id="2"), - ] - assert result == expected - - -def test_append_only_mode_prevents_updates(): - """Test that append_only mode raises an error when trying to update an existing message.""" - left = [HumanMessage(content="Hello", id="1")] - right = [HumanMessage(content="Hello again", id="1")] - with pytest.raises( - ValueError, - match="Cannot update existing message with ID '1' in append_only mode", - ): - add_messages(left, right, mode="append_only") - - -def test_append_only_mode_prevents_remove_messages(): - """Test that append_only mode prevents RemoveMessage operations.""" - left = [ - HumanMessage(content="Hello", id="1"), - AIMessage(content="Hi there!", id="2"), - ] - right = [RemoveMessage(id="2")] - with pytest.raises( - ValueError, - match="Cannot remove existing message with ID '2' in append_only mode", - ): - add_messages(left, right, mode="append_only") - - -def test_append_only_mode_prevents_remove_all_messages(): - """Test that append_only mode prevents REMOVE_ALL_MESSAGES operations.""" - left = [ - HumanMessage(content="Hello", id="1"), - AIMessage(content="Hi there!", id="2"), - ] - right = [RemoveMessage(id=REMOVE_ALL_MESSAGES)] - with pytest.raises( - ValueError, - match="Cannot remove all messages in append_only mode", - ): - add_messages(left, right, mode="append_only") - - -def test_append_only_mode_with_multiple_new_messages(): - """Test that append_only mode allows adding multiple new messages.""" - left = [HumanMessage(content="Hello", id="1")] - right = [ - AIMessage(content="Hi there!", id="2"), - SystemMessage(content="System message", id="3"), - HumanMessage(content="Another message", id="4"), - ] - result = add_messages(left, right, mode="append_only") - expected = [ - HumanMessage(content="Hello", id="1"), - AIMessage(content="Hi there!", id="2"), - SystemMessage(content="System message", id="3"), - HumanMessage(content="Another message", id="4"), - ] - assert result == expected - - -def test_append_only_mode_with_mixed_operations(): - """Test that append_only mode fails when mixing new messages with updates.""" - left = [HumanMessage(content="Hello", id="1")] - right = [ - AIMessage(content="Hi there!", id="2"), # new message - HumanMessage(content="Updated hello", id="1"), # update attempt - ] - with pytest.raises( - ValueError, - match="Cannot update existing message with ID '1' in append_only mode", - ): - add_messages(left, right, mode="append_only") - - -def test_allow_everything_mode_default_behavior(): - """Test that allow_everything mode is the default and allows updates.""" - left = [HumanMessage(content="Hello", id="1")] - right = [HumanMessage(content="Hello again", id="1")] - # Test without specifying mode (should default to allow_everything) - result = add_messages(left, right) - expected = [HumanMessage(content="Hello again", id="1")] - assert result == expected - - # Test with explicit mode - result = add_messages(left, right, mode="allow_everything") - assert result == expected - - -def test_append_only_mode_in_state_graph(): - """Test append_only mode works correctly in a StateGraph.""" - - class State(TypedDict): - messages: Annotated[list[AnyMessage], add_messages(mode="append_only")] - - def add_message(state: State): - return {"messages": [AIMessage(content="Response", id="2")]} - - def try_update_message(state: State): - # This should fail because message with id="1" already exists - return {"messages": [HumanMessage(content="Updated", id="1")]} - - # Test successful case - builder = StateGraph(State) - builder.add_node("add_message", add_message) - builder.add_edge(START, "add_message") - builder.add_edge("add_message", END) - graph = builder.compile() - - result = graph.invoke({"messages": [HumanMessage(content="Hello", id="1")]}) - assert len(result["messages"]) == 2 - assert result["messages"][0].content == "Hello" - assert result["messages"][1].content == "Response" - - # Test failure case - builder2 = StateGraph(State) - builder2.add_node("try_update", try_update_message) - builder2.add_edge(START, "try_update") - builder2.add_edge("try_update", END) - graph2 = builder2.compile() - - with pytest.raises( - ValueError, - match="Cannot update existing message with ID '1' in append_only mode", - ): - graph2.invoke({"messages": [HumanMessage(content="Hello", id="1")]})