From d3e2605ad25a81fbc32b8fb587b9124c00335e5c Mon Sep 17 00:00:00 2001 From: Aman Gupta Date: Fri, 2 Jan 2026 22:14:42 +0530 Subject: [PATCH 1/2] core: refactor trim_messages to validate arguments eagerlyclea --- libs/core/langchain_core/messages/utils.py | 34 +++++++++++++++---- .../tests/unit_tests/messages/test_utils.py | 3 +- libs/core/uv.lock | 10 +++--- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/libs/core/langchain_core/messages/utils.py b/libs/core/langchain_core/messages/utils.py index 6fe390753627a..58ea72b22960a 100644 --- a/libs/core/langchain_core/messages/utils.py +++ b/libs/core/langchain_core/messages/utils.py @@ -730,9 +730,8 @@ def merge_message_runs( # TODO: Update so validation errors (for token_counter, for example) are raised on # init not at runtime. -@_runnable_support def trim_messages( - messages: Iterable[MessageLikeRepresentation] | PromptValue, + messages: Iterable[MessageLikeRepresentation] | PromptValue | None = None, *, max_tokens: int, token_counter: Callable[[list[BaseMessage]], int] @@ -745,7 +744,9 @@ def trim_messages( start_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None, include_system: bool = False, text_splitter: Callable[[str], list[str]] | TextSplitter | None = None, -) -> list[BaseMessage]: +) -> ( + list[BaseMessage] | Runnable[Sequence[MessageLikeRepresentation], list[BaseMessage]] +): r"""Trim messages to be below a token count. `trim_messages` can be used to reduce the size of a chat history to a specified @@ -1047,8 +1048,6 @@ def dummy_token_counter(messages: list[BaseMessage]) -> int: msg = "include_system parameter is only valid with strategy='last'" raise ValueError(msg) - messages = convert_to_messages(messages) - # Handle string shortcuts for token counter if isinstance(token_counter, str): if token_counter in _TOKEN_COUNTER_SHORTCUTS: @@ -1084,11 +1083,32 @@ def list_token_counter(messages: Sequence[BaseMessage]) -> int: else: msg = ( f"'token_counter' expected to be a model that implements " - f"'get_num_tokens_from_messages()' or a function. Received object of type " - f"{type(actual_token_counter)}." + f"'get_num_tokens_from_messages()' or a function. " + f"Received object of type {type(actual_token_counter)}." ) raise ValueError(msg) + if messages is None: + from langchain_core.runnables import RunnableLambda # noqa: PLC0415 + + # Avoid circular import. + return RunnableLambda( + partial( + trim_messages, # type: ignore[arg-type] + max_tokens=max_tokens, + token_counter=token_counter, + strategy=strategy, + allow_partial=allow_partial, + end_on=end_on, + start_on=start_on, + include_system=include_system, + text_splitter=text_splitter, + ), + name="trim_messages", + ) + + messages = convert_to_messages(messages) + if _HAS_LANGCHAIN_TEXT_SPLITTERS and isinstance(text_splitter, TextSplitter): text_splitter_fn = text_splitter.split_text elif text_splitter: diff --git a/libs/core/tests/unit_tests/messages/test_utils.py b/libs/core/tests/unit_tests/messages/test_utils.py index 72a7ed4648302..0932306738211 100644 --- a/libs/core/tests/unit_tests/messages/test_utils.py +++ b/libs/core/tests/unit_tests/messages/test_utils.py @@ -526,7 +526,6 @@ def test_trim_messages_bound_model_token_counter() -> None: def test_trim_messages_bad_token_counter() -> None: - trimmer = trim_messages(max_tokens=10, token_counter={}) # type: ignore[call-overload] with pytest.raises( ValueError, match=re.escape( @@ -535,7 +534,7 @@ def test_trim_messages_bad_token_counter() -> None: "Received object of type ." ), ): - trimmer.invoke([HumanMessage("foobar")]) + trim_messages(max_tokens=10, token_counter={}) # type: ignore[call-overload] def dummy_token_counter(messages: list[BaseMessage]) -> int: diff --git a/libs/core/uv.lock b/libs/core/uv.lock index 7ac212aa7690d..5e6cc8a0b2da2 100644 --- a/libs/core/uv.lock +++ b/libs/core/uv.lock @@ -1112,12 +1112,12 @@ requires-dist = [ ] [package.metadata.requires-dev] -lint = [{ name = "ruff", specifier = ">=0.13.1,<0.14.0" }] +lint = [{ name = "ruff", specifier = ">=0.14.10,<0.15.0" }] test = [{ name = "langchain-core", editable = "." }] test-integration = [] typing = [ { name = "langchain-core", editable = "." }, - { name = "mypy", specifier = ">=1.18.1,<1.19.0" }, + { name = "mypy", specifier = ">=1.19.1,<1.20.0" }, { name = "types-pyyaml", specifier = ">=6.0.12.2,<7.0.0.0" }, ] @@ -1139,7 +1139,7 @@ dev = [ ] lint = [ { name = "langchain-core", editable = "." }, - { name = "ruff", specifier = ">=0.13.1,<0.14.0" }, + { name = "ruff", specifier = ">=0.14.10,<0.15.0" }, ] test = [ { name = "freezegun", specifier = ">=1.2.2,<2.0.0" }, @@ -1156,7 +1156,7 @@ test-integration = [ { name = "nltk", specifier = ">=3.9.1,<4.0.0" }, { name = "scipy", marker = "python_full_version == '3.12.*'", specifier = ">=1.7.0,<2.0.0" }, { name = "scipy", marker = "python_full_version >= '3.13'", specifier = ">=1.14.1,<2.0.0" }, - { name = "sentence-transformers", marker = "python_full_version < '3.14'", specifier = ">=3.0.1,<4.0.0" }, + { name = "sentence-transformers", specifier = ">=3.0.1,<4.0.0" }, { name = "spacy", marker = "python_full_version < '3.14'", specifier = ">=3.8.7,<4.0.0" }, { name = "thinc", specifier = ">=8.3.6,<9.0.0" }, { name = "tiktoken", specifier = ">=0.8.0,<1.0.0" }, @@ -1165,7 +1165,7 @@ test-integration = [ typing = [ { name = "beautifulsoup4", specifier = ">=4.13.5,<5.0.0" }, { name = "lxml-stubs", specifier = ">=0.5.1,<1.0.0" }, - { name = "mypy", specifier = ">=1.18.1,<1.19.0" }, + { name = "mypy", specifier = ">=1.19.1,<1.20.0" }, { name = "tiktoken", specifier = ">=0.8.0,<1.0.0" }, { name = "types-requests", specifier = ">=2.31.0.20240218,<3.0.0.0" }, ] From 3fd93769932304a99e6afd34a99ae176a8641a67 Mon Sep 17 00:00:00 2001 From: Aman Gupta Date: Fri, 2 Jan 2026 22:36:39 +0530 Subject: [PATCH 2/2] style: fix typos in docstrings --- libs/core/langchain_core/messages/utils.py | 46 +++++++++++++++++-- .../tests/unit_tests/messages/test_utils.py | 2 + 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/libs/core/langchain_core/messages/utils.py b/libs/core/langchain_core/messages/utils.py index 58ea72b22960a..06b71ecdf4afb 100644 --- a/libs/core/langchain_core/messages/utils.py +++ b/libs/core/langchain_core/messages/utils.py @@ -730,6 +730,42 @@ def merge_message_runs( # TODO: Update so validation errors (for token_counter, for example) are raised on # init not at runtime. +@overload +def trim_messages( + messages: None = None, + *, + max_tokens: int, + token_counter: Callable[[list[BaseMessage]], int] + | Callable[[BaseMessage], int] + | BaseLanguageModel + | Literal["approximate"], + strategy: Literal["first", "last"] = "last", + allow_partial: bool = False, + end_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None, + start_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None, + include_system: bool = False, + text_splitter: Callable[[str], list[str]] | TextSplitter | None = None, +) -> Runnable[Sequence[MessageLikeRepresentation], list[BaseMessage]]: ... + + +@overload +def trim_messages( + messages: Iterable[MessageLikeRepresentation] | PromptValue, + *, + max_tokens: int, + token_counter: Callable[[list[BaseMessage]], int] + | Callable[[BaseMessage], int] + | BaseLanguageModel + | Literal["approximate"], + strategy: Literal["first", "last"] = "last", + allow_partial: bool = False, + end_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None, + start_on: str | type[BaseMessage] | Sequence[str | type[BaseMessage]] | None = None, + include_system: bool = False, + text_splitter: Callable[[str], list[str]] | TextSplitter | None = None, +) -> list[BaseMessage]: ... + + def trim_messages( messages: Iterable[MessageLikeRepresentation] | PromptValue | None = None, *, @@ -1094,7 +1130,7 @@ def list_token_counter(messages: Sequence[BaseMessage]) -> int: # Avoid circular import. return RunnableLambda( partial( - trim_messages, # type: ignore[arg-type] + trim_messages, max_tokens=max_tokens, token_counter=token_counter, strategy=strategy, @@ -1223,7 +1259,7 @@ def convert_to_openai_messages( { "role": "user", "content": [ - {"type": "text", "text": "whats in this"}, + {"type": "text", "text": "what's in this"}, { "type": "image_url", "image_url": {"url": "data:image/png;base64,'/9j/4AAQSk'"}, @@ -1242,15 +1278,15 @@ def convert_to_openai_messages( ], ), ToolMessage("foobar", tool_call_id="1", name="bar"), - {"role": "assistant", "content": "thats nice"}, + {"role": "assistant", "content": "that's nice"}, ] oai_messages = convert_to_openai_messages(messages) # -> [ # {'role': 'system', 'content': 'foo'}, - # {'role': 'user', 'content': [{'type': 'text', 'text': 'whats in this'}, {'type': 'image_url', 'image_url': {'url': "data:image/png;base64,'/9j/4AAQSk'"}}]}, + # {'role': 'user', 'content': [{'type': 'text', 'text': "what's in this"}, {'type': 'image_url', 'image_url': {'url': "data:image/png;base64,'/9j/4AAQSk'"}}]}, # {'role': 'assistant', 'tool_calls': [{'type': 'function', 'id': '1','function': {'name': 'analyze', 'arguments': '{"baz": "buz"}'}}], 'content': ''}, # {'role': 'tool', 'name': 'bar', 'content': 'foobar'}, - # {'role': 'assistant', 'content': 'thats nice'} + # {'role': 'assistant', 'content': 'that's nice'} # ] ``` diff --git a/libs/core/tests/unit_tests/messages/test_utils.py b/libs/core/tests/unit_tests/messages/test_utils.py index 0932306738211..9ffeea5110e6b 100644 --- a/libs/core/tests/unit_tests/messages/test_utils.py +++ b/libs/core/tests/unit_tests/messages/test_utils.py @@ -669,6 +669,7 @@ def test_trim_messages_start_on_with_allow_partial() -> None: ) assert len(result) == 1 + assert isinstance(result, list) assert result[0].content == "Second human message" assert messages == messages_copy @@ -743,6 +744,7 @@ def test_trim_messages_token_counter_shortcut_with_options() -> None: ) # Should include system message and start on human + assert isinstance(result, list) assert len(result) >= 2 assert isinstance(result[0], SystemMessage) assert any(isinstance(msg, HumanMessage) for msg in result[1:])