From dc6812e1c0025298b6c6fa68b300132446bd5dbf Mon Sep 17 00:00:00 2001 From: rohanvasudev1 Date: Mon, 6 Oct 2025 00:22:03 -0700 Subject: [PATCH 1/2] fix(core): replace lambda functions in with_structured_output to enable serialization --- .../_serializable_structured_output.py | 90 +++++++++++++++++++ .../language_models/chat_models.py | 8 +- 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 libs/core/langchain_core/language_models/_serializable_structured_output.py diff --git a/libs/core/langchain_core/language_models/_serializable_structured_output.py b/libs/core/langchain_core/language_models/_serializable_structured_output.py new file mode 100644 index 0000000000000..63cbbc625fbef --- /dev/null +++ b/libs/core/langchain_core/language_models/_serializable_structured_output.py @@ -0,0 +1,90 @@ +"""Serializable components for structured output functionality.""" + +from __future__ import annotations + +from typing import Any, Optional + +from langchain_core.output_parsers.base import OutputParserLike + + +class SerializableParsingErrorHandler: + """Serializable replacement for `lambda _: None` in parsing error scenarios. + + This class provides the same functionality as the lambda function but can be + pickled for thread persistence in LangGraph Studio. + """ + + def __call__(self, _: Any) -> None: + """Return None for any input, mimicking lambda _: None behavior.""" + return None + + def __repr__(self) -> str: + """Provide a clear representation for debugging.""" + return "SerializableParsingErrorHandler()" + + def __eq__(self, other: object) -> bool: + """Support equality comparison for testing.""" + return isinstance(other, SerializableParsingErrorHandler) + + def __getstate__(self) -> dict[str, Any]: + """Control pickle serialization for production robustness.""" + # This class has no state to preserve, return empty dict + return {} + + def __setstate__(self, state: dict[str, Any]) -> None: + """Control pickle deserialization for production robustness.""" + # This class has no state to restore, nothing to do + pass + + +class SerializableNoneAssigner: + """Serializable replacement for `lambda _: None` in RunnablePassthrough.assign(). + + Used in the fallback chain when parsing fails. + """ + + def __call__(self, _: Any) -> None: + """Return None for any input, used as fallback parsed value.""" + return None + + def __repr__(self) -> str: + """Provide a clear representation for debugging.""" + return "SerializableNoneAssigner()" + + def __eq__(self, other: object) -> bool: + """Support equality comparison for testing.""" + return isinstance(other, SerializableNoneAssigner) + + def __getstate__(self) -> dict[str, Any]: + """Control pickle serialization for production robustness.""" + # This class has no state to preserve, return empty dict + return {} + + def __setstate__(self, state: dict[str, Any]) -> None: + """Control pickle deserialization for production robustness.""" + # This class has no state to restore, nothing to do + pass + + +def get_serializable_error_handler() -> SerializableParsingErrorHandler: + """Get a serializable error handler instance. + + This is a convenience function that can be used directly in place of + lambda _: None in RunnablePassthrough.assign() calls. + + Returns: + SerializableParsingErrorHandler instance. + """ + return SerializableParsingErrorHandler() + + +def get_serializable_none_assigner() -> SerializableNoneAssigner: + """Get a serializable None assigner instance. + + This is a convenience function that can be used directly in place of + lambda _: None in parsed value assignments. + + Returns: + SerializableNoneAssigner instance. + """ + return SerializableNoneAssigner() \ No newline at end of file diff --git a/libs/core/langchain_core/language_models/chat_models.py b/libs/core/langchain_core/language_models/chat_models.py index 070d71f848276..0ffe25a5ea622 100644 --- a/libs/core/langchain_core/language_models/chat_models.py +++ b/libs/core/langchain_core/language_models/chat_models.py @@ -33,6 +33,10 @@ LangSmithParams, LanguageModelInput, ) +from langchain_core.language_models._serializable_structured_output import ( + get_serializable_error_handler, + get_serializable_none_assigner, +) from langchain_core.load import dumpd, dumps from langchain_core.messages import ( AIMessage, @@ -1631,9 +1635,9 @@ class AnswerWithJustification(BaseModel): ) if include_raw: parser_assign = RunnablePassthrough.assign( - parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None + parsed=itemgetter("raw") | output_parser, parsing_error=get_serializable_error_handler() ) - parser_none = RunnablePassthrough.assign(parsed=lambda _: None) + parser_none = RunnablePassthrough.assign(parsed=get_serializable_none_assigner()) parser_with_fallback = parser_assign.with_fallbacks( [parser_none], exception_key="parsing_error" ) From 2205e6c98bae761164006fbe1ebbb396e4cb8e38 Mon Sep 17 00:00:00 2001 From: rohanvasudev1 Date: Mon, 6 Oct 2025 00:34:14 -0700 Subject: [PATCH 2/2] Linting --- .../_serializable_structured_output.py | 20 +++++++++++-------- .../language_models/chat_models.py | 15 ++++++++------ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/libs/core/langchain_core/language_models/_serializable_structured_output.py b/libs/core/langchain_core/language_models/_serializable_structured_output.py index 63cbbc625fbef..94a1f494d7ba8 100644 --- a/libs/core/langchain_core/language_models/_serializable_structured_output.py +++ b/libs/core/langchain_core/language_models/_serializable_structured_output.py @@ -2,9 +2,7 @@ from __future__ import annotations -from typing import Any, Optional - -from langchain_core.output_parsers.base import OutputParserLike +from typing import Any class SerializableParsingErrorHandler: @@ -16,7 +14,7 @@ class SerializableParsingErrorHandler: def __call__(self, _: Any) -> None: """Return None for any input, mimicking lambda _: None behavior.""" - return None + return def __repr__(self) -> str: """Provide a clear representation for debugging.""" @@ -26,6 +24,10 @@ def __eq__(self, other: object) -> bool: """Support equality comparison for testing.""" return isinstance(other, SerializableParsingErrorHandler) + def __hash__(self) -> int: + """Support hashing for set/dict operations.""" + return hash(self.__class__.__name__) + def __getstate__(self) -> dict[str, Any]: """Control pickle serialization for production robustness.""" # This class has no state to preserve, return empty dict @@ -34,7 +36,6 @@ def __getstate__(self) -> dict[str, Any]: def __setstate__(self, state: dict[str, Any]) -> None: """Control pickle deserialization for production robustness.""" # This class has no state to restore, nothing to do - pass class SerializableNoneAssigner: @@ -45,7 +46,7 @@ class SerializableNoneAssigner: def __call__(self, _: Any) -> None: """Return None for any input, used as fallback parsed value.""" - return None + return def __repr__(self) -> str: """Provide a clear representation for debugging.""" @@ -55,6 +56,10 @@ def __eq__(self, other: object) -> bool: """Support equality comparison for testing.""" return isinstance(other, SerializableNoneAssigner) + def __hash__(self) -> int: + """Support hashing for set/dict operations.""" + return hash(self.__class__.__name__) + def __getstate__(self) -> dict[str, Any]: """Control pickle serialization for production robustness.""" # This class has no state to preserve, return empty dict @@ -63,7 +68,6 @@ def __getstate__(self) -> dict[str, Any]: def __setstate__(self, state: dict[str, Any]) -> None: """Control pickle deserialization for production robustness.""" # This class has no state to restore, nothing to do - pass def get_serializable_error_handler() -> SerializableParsingErrorHandler: @@ -87,4 +91,4 @@ def get_serializable_none_assigner() -> SerializableNoneAssigner: Returns: SerializableNoneAssigner instance. """ - return SerializableNoneAssigner() \ No newline at end of file + return SerializableNoneAssigner() diff --git a/libs/core/langchain_core/language_models/chat_models.py b/libs/core/langchain_core/language_models/chat_models.py index 0ffe25a5ea622..834866fd0e42f 100644 --- a/libs/core/langchain_core/language_models/chat_models.py +++ b/libs/core/langchain_core/language_models/chat_models.py @@ -24,6 +24,10 @@ Callbacks, ) from langchain_core.globals import get_llm_cache +from langchain_core.language_models._serializable_structured_output import ( + get_serializable_error_handler, + get_serializable_none_assigner, +) from langchain_core.language_models._utils import ( _normalize_messages, _update_message_content_to_blocks, @@ -33,10 +37,6 @@ LangSmithParams, LanguageModelInput, ) -from langchain_core.language_models._serializable_structured_output import ( - get_serializable_error_handler, - get_serializable_none_assigner, -) from langchain_core.load import dumpd, dumps from langchain_core.messages import ( AIMessage, @@ -1635,9 +1635,12 @@ class AnswerWithJustification(BaseModel): ) if include_raw: parser_assign = RunnablePassthrough.assign( - parsed=itemgetter("raw") | output_parser, parsing_error=get_serializable_error_handler() + parsed=itemgetter("raw") | output_parser, + parsing_error=get_serializable_error_handler(), + ) + parser_none = RunnablePassthrough.assign( + parsed=get_serializable_none_assigner() ) - parser_none = RunnablePassthrough.assign(parsed=get_serializable_none_assigner()) parser_with_fallback = parser_assign.with_fallbacks( [parser_none], exception_key="parsing_error" )