From 754528d23f13780716f2c67429cbb973e271aba6 Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Thu, 7 Aug 2025 15:20:05 -0400 Subject: [PATCH] feat(langchain): add stuff and map reduce chains (#32333) * Add stuff and map reduce chains * We'll need to rename and add unit tests to the chains prior to official release --- .../langchain/_internal/__init__.py | 0 .../langchain/_internal/_documents.py | 39 ++ .../langchain/_internal/_lazy_import.py | 36 ++ .../langchain/_internal/_prompts.py | 166 +++++ .../langchain/_internal/_typing.py | 65 ++ .../langchain/_internal/_utils.py | 7 + .../langchain_v1/langchain/chains/__init__.py | 9 + .../langchain/chains/documents/__init__.py | 17 + .../langchain/chains/documents/map_reduce.py | 586 ++++++++++++++++++ .../langchain/chains/documents/stuff.py | 473 ++++++++++++++ .../langchain/documents/__init__.py | 5 + libs/langchain_v1/pyproject.toml | 3 +- libs/langchain_v1/uv.lock | 22 +- 13 files changed, 1416 insertions(+), 12 deletions(-) create mode 100644 libs/langchain_v1/langchain/_internal/__init__.py create mode 100644 libs/langchain_v1/langchain/_internal/_documents.py create mode 100644 libs/langchain_v1/langchain/_internal/_lazy_import.py create mode 100644 libs/langchain_v1/langchain/_internal/_prompts.py create mode 100644 libs/langchain_v1/langchain/_internal/_typing.py create mode 100644 libs/langchain_v1/langchain/_internal/_utils.py create mode 100644 libs/langchain_v1/langchain/chains/__init__.py create mode 100644 libs/langchain_v1/langchain/chains/documents/__init__.py create mode 100644 libs/langchain_v1/langchain/chains/documents/map_reduce.py create mode 100644 libs/langchain_v1/langchain/chains/documents/stuff.py create mode 100644 libs/langchain_v1/langchain/documents/__init__.py diff --git a/libs/langchain_v1/langchain/_internal/__init__.py b/libs/langchain_v1/langchain/_internal/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/langchain_v1/langchain/_internal/_documents.py b/libs/langchain_v1/langchain/_internal/_documents.py new file mode 100644 index 00000000000..2ec72f77232 --- /dev/null +++ b/libs/langchain_v1/langchain/_internal/_documents.py @@ -0,0 +1,39 @@ +"""Internal document utilities.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from langchain_core.documents import Document + + +def format_document_xml(doc: Document) -> str: + """Format a document as XML-like structure for LLM consumption. + + Args: + doc: Document to format + + Returns: + Document wrapped in XML tags: + + ... + ... + ... + + + Note: + Does not generate valid XML or escape special characters. + Intended for semi-structured LLM input only. + """ + id_str = f"{doc.id}" if doc.id is not None else "" + metadata_str = "" + if doc.metadata: + metadata_items = [f"{k}: {v!s}" for k, v in doc.metadata.items()] + metadata_str = f"{', '.join(metadata_items)}" + return ( + f"{id_str}" + f"{doc.page_content}" + f"{metadata_str}" + f"" + ) diff --git a/libs/langchain_v1/langchain/_internal/_lazy_import.py b/libs/langchain_v1/langchain/_internal/_lazy_import.py new file mode 100644 index 00000000000..5af0bb6e43d --- /dev/null +++ b/libs/langchain_v1/langchain/_internal/_lazy_import.py @@ -0,0 +1,36 @@ +"""Lazy import utilities.""" + +from importlib import import_module +from typing import Union + + +def import_attr( + attr_name: str, + module_name: Union[str, None], + package: Union[str, None], +) -> object: + """Import an attribute from a module located in a package. + + This utility function is used in custom __getattr__ methods within __init__.py + files to dynamically import attributes. + + Args: + attr_name: The name of the attribute to import. + module_name: The name of the module to import from. If None, the attribute + is imported from the package itself. + package: The name of the package where the module is located. + """ + if module_name == "__module__" or module_name is None: + try: + result = import_module(f".{attr_name}", package=package) + except ModuleNotFoundError: + msg = f"module '{package!r}' has no attribute {attr_name!r}" + raise AttributeError(msg) from None + else: + try: + module = import_module(f".{module_name}", package=package) + except ModuleNotFoundError as err: + msg = f"module '{package!r}.{module_name!r}' not found ({err})" + raise ImportError(msg) from None + result = getattr(module, attr_name) + return result diff --git a/libs/langchain_v1/langchain/_internal/_prompts.py b/libs/langchain_v1/langchain/_internal/_prompts.py new file mode 100644 index 00000000000..f2de112ea25 --- /dev/null +++ b/libs/langchain_v1/langchain/_internal/_prompts.py @@ -0,0 +1,166 @@ +"""Internal prompt resolution utilities. + +This module provides utilities for resolving different types of prompt specifications +into standardized message formats for language models. It supports both synchronous +and asynchronous prompt resolution with automatic detection of callable types. + +The module is designed to handle common prompt patterns across LangChain components, +particularly for summarization chains and other document processing workflows. +""" + +from __future__ import annotations + +import inspect +from typing import TYPE_CHECKING, Callable, Union + +if TYPE_CHECKING: + from collections.abc import Awaitable + + from langchain_core.messages import MessageLikeRepresentation + from langgraph.runtime import Runtime + + from langchain._internal._typing import ContextT, StateT + + +def resolve_prompt( + prompt: Union[ + str, + None, + Callable[[StateT, Runtime[ContextT]], list[MessageLikeRepresentation]], + ], + state: StateT, + runtime: Runtime[ContextT], + default_user_content: str, + default_system_content: str, +) -> list[MessageLikeRepresentation]: + """Resolve a prompt specification into a list of messages. + + Handles prompt resolution across different strategies. Supports callable functions, + string system messages, and None for default behavior. + + Args: + prompt: The prompt specification to resolve. Can be: + - Callable: Function taking (state, runtime) returning message list. + - str: A system message string. + - None: Use the provided default system message. + state: Current state, passed to callable prompts. + runtime: LangGraph runtime instance, passed to callable prompts. + default_user_content: User content to include (e.g., document text). + default_system_content: Default system message when prompt is None. + + Returns: + List of message dictionaries for language models, typically containing + a system message and user message with content. + + Raises: + TypeError: If prompt type is not str, None, or callable. + + Example: + ```python + def custom_prompt(state, runtime): + return [{"role": "system", "content": "Custom"}] + + messages = resolve_prompt(custom_prompt, state, runtime, "content", "default") + messages = resolve_prompt("Custom system", state, runtime, "content", "default") + messages = resolve_prompt(None, state, runtime, "content", "Default") + ``` + + Note: + Callable prompts have full control over message structure and content + parameter is ignored. String/None prompts create standard system + user + structure. + """ + if callable(prompt): + return prompt(state, runtime) + if isinstance(prompt, str): + system_msg = prompt + elif prompt is None: + system_msg = default_system_content + else: + msg = f"Invalid prompt type: {type(prompt)}. Expected str, None, or callable." + raise TypeError(msg) + + return [ + {"role": "system", "content": system_msg}, + {"role": "user", "content": default_user_content}, + ] + + +async def aresolve_prompt( + prompt: Union[ + str, + None, + Callable[[StateT, Runtime[ContextT]], list[MessageLikeRepresentation]], + Callable[ + [StateT, Runtime[ContextT]], Awaitable[list[MessageLikeRepresentation]] + ], + ], + state: StateT, + runtime: Runtime[ContextT], + default_user_content: str, + default_system_content: str, +) -> list[MessageLikeRepresentation]: + """Async version of resolve_prompt supporting both sync and async callables. + + Handles prompt resolution across different strategies. Supports sync/async callable + functions, string system messages, and None for default behavior. + + Args: + prompt: The prompt specification to resolve. Can be: + - Callable (sync): Function taking (state, runtime) returning message list. + - Callable (async): Async function taking (state, runtime) returning + awaitable message list. + - str: A system message string. + - None: Use the provided default system message. + state: Current state, passed to callable prompts. + runtime: LangGraph runtime instance, passed to callable prompts. + default_user_content: User content to include (e.g., document text). + default_system_content: Default system message when prompt is None. + + Returns: + List of message dictionaries for language models, typically containing + a system message and user message with content. + + Raises: + TypeError: If prompt type is not str, None, or callable. + + Example: + ```python + async def async_prompt(state, runtime): + return [{"role": "system", "content": "Async"}] + + def sync_prompt(state, runtime): + return [{"role": "system", "content": "Sync"}] + + messages = await aresolve_prompt( + async_prompt, state, runtime, "content", "default" + ) + messages = await aresolve_prompt( + sync_prompt, state, runtime, "content", "default" + ) + messages = await aresolve_prompt("Custom", state, runtime, "content", "default") + ``` + + Note: + Callable prompts have full control over message structure and content + parameter is ignored. Automatically detects and handles async + callables. + """ + if callable(prompt): + result = prompt(state, runtime) + # Check if the result is awaitable (async function) + if inspect.isawaitable(result): + return await result + return result + if isinstance(prompt, str): + system_msg = prompt + elif prompt is None: + system_msg = default_system_content + else: + msg = f"Invalid prompt type: {type(prompt)}. Expected str, None, or callable." + raise TypeError(msg) + + return [ + {"role": "system", "content": system_msg}, + {"role": "user", "content": default_user_content}, + ] diff --git a/libs/langchain_v1/langchain/_internal/_typing.py b/libs/langchain_v1/langchain/_internal/_typing.py new file mode 100644 index 00000000000..9ca0990eb38 --- /dev/null +++ b/libs/langchain_v1/langchain/_internal/_typing.py @@ -0,0 +1,65 @@ +"""Private typing utilities for langchain.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar, Union + +from langgraph.graph._node import StateNode +from pydantic import BaseModel +from typing_extensions import TypeAlias + +if TYPE_CHECKING: + from dataclasses import Field + + +class TypedDictLikeV1(Protocol): + """Protocol to represent types that behave like TypedDicts. + + Version 1: using `ClassVar` for keys. + """ + + __required_keys__: ClassVar[frozenset[str]] + __optional_keys__: ClassVar[frozenset[str]] + + +class TypedDictLikeV2(Protocol): + """Protocol to represent types that behave like TypedDicts. + + Version 2: not using `ClassVar` for keys. + """ + + __required_keys__: frozenset[str] + __optional_keys__: frozenset[str] + + +class DataclassLike(Protocol): + """Protocol to represent types that behave like dataclasses. + + Inspired by the private _DataclassT from dataclasses that uses a similar + protocol as a bound. + """ + + __dataclass_fields__: ClassVar[dict[str, Field[Any]]] + + +StateLike: TypeAlias = Union[TypedDictLikeV1, TypedDictLikeV2, DataclassLike, BaseModel] +"""Type alias for state-like types. + +It can either be a `TypedDict`, `dataclass`, or Pydantic `BaseModel`. +Note: we cannot use either `TypedDict` or `dataclass` directly due to limitations in +type checking. +""" + +StateT = TypeVar("StateT", bound=StateLike) +"""Type variable used to represent the state in a graph.""" + +ContextT = TypeVar("ContextT", bound=Union[StateLike, None]) +"""Type variable for context types.""" + + +__all__ = [ + "ContextT", + "StateLike", + "StateNode", + "StateT", +] diff --git a/libs/langchain_v1/langchain/_internal/_utils.py b/libs/langchain_v1/langchain/_internal/_utils.py new file mode 100644 index 00000000000..9f3d930b2a5 --- /dev/null +++ b/libs/langchain_v1/langchain/_internal/_utils.py @@ -0,0 +1,7 @@ +# Re-exporting internal utilities from LangGraph for internal use in LangChain. +# A different wrapper needs to be created for this purpose in LangChain. +from langgraph._internal._runnable import RunnableCallable + +__all__ = [ + "RunnableCallable", +] diff --git a/libs/langchain_v1/langchain/chains/__init__.py b/libs/langchain_v1/langchain/chains/__init__.py new file mode 100644 index 00000000000..1780a4c8925 --- /dev/null +++ b/libs/langchain_v1/langchain/chains/__init__.py @@ -0,0 +1,9 @@ +from langchain.chains.documents import ( + create_map_reduce_chain, + create_stuff_documents_chain, +) + +__all__ = [ + "create_map_reduce_chain", + "create_stuff_documents_chain", +] diff --git a/libs/langchain_v1/langchain/chains/documents/__init__.py b/libs/langchain_v1/langchain/chains/documents/__init__.py new file mode 100644 index 00000000000..d0a16cd47c9 --- /dev/null +++ b/libs/langchain_v1/langchain/chains/documents/__init__.py @@ -0,0 +1,17 @@ +"""Document extraction chains. + +This module provides different strategies for extracting information from collections +of documents using LangGraph and modern language models. + +Available Strategies: +- Stuff: Processes all documents together in a single context window +- Map-Reduce: Processes documents in parallel (map), then combines results (reduce) +""" + +from langchain.chains.documents.map_reduce import create_map_reduce_chain +from langchain.chains.documents.stuff import create_stuff_documents_chain + +__all__ = [ + "create_map_reduce_chain", + "create_stuff_documents_chain", +] diff --git a/libs/langchain_v1/langchain/chains/documents/map_reduce.py b/libs/langchain_v1/langchain/chains/documents/map_reduce.py new file mode 100644 index 00000000000..eb035c7d44c --- /dev/null +++ b/libs/langchain_v1/langchain/chains/documents/map_reduce.py @@ -0,0 +1,586 @@ +"""Map-Reduce Extraction Implementation using LangGraph Send API.""" + +from __future__ import annotations + +import operator +from typing import ( + TYPE_CHECKING, + Annotated, + Any, + Callable, + Generic, + Literal, + Optional, + Union, + cast, +) + +from langgraph.graph import END, START, StateGraph +from langgraph.types import Send +from typing_extensions import NotRequired, TypedDict + +from langchain._internal._documents import format_document_xml +from langchain._internal._prompts import aresolve_prompt, resolve_prompt +from langchain._internal._typing import ContextT, StateNode +from langchain._internal._utils import RunnableCallable +from langchain.chat_models import init_chat_model + +if TYPE_CHECKING: + from langchain_core.documents import Document + from langchain_core.language_models.chat_models import BaseChatModel + + # Pycharm is unable to identify that AIMessage is used in the cast below + from langchain_core.messages import ( + AIMessage, + MessageLikeRepresentation, + ) + from langchain_core.runnables import RunnableConfig + from langgraph.runtime import Runtime + from pydantic import BaseModel + + +class ExtractionResult(TypedDict): + """Result from processing a document or group of documents.""" + + indexes: list[int] + """Document indexes that contributed to this result.""" + result: Any + """Extracted result from the document(s).""" + + +class MapReduceState(TypedDict): + """State for map-reduce extraction chain. + + This state tracks the map-reduce process where documents are processed + in parallel during the map phase, then combined in the reduce phase. + """ + + documents: list[Document] + """List of documents to process.""" + map_results: Annotated[list[ExtractionResult], operator.add] + """Individual results from the map phase.""" + result: NotRequired[Any] + """Final combined result from the reduce phase if applicable.""" + + +# The payload for the map phase is a list of documents and their indexes. +# The current implementation only supports a single document per map operation, +# but the structure allows for future expansion to process a group of documents. +# A user would provide an input split function that returns groups of documents +# to process together, if desired. +class MapState(TypedDict): + """State for individual map operations.""" + + documents: list[Document] + """List of documents to process in map phase.""" + indexes: list[int] + """List of indexes of the documents in the original list.""" + + +class InputSchema(TypedDict): + """Input schema for the map-reduce extraction chain. + + Defines the expected input format when invoking the extraction chain. + """ + + documents: list[Document] + """List of documents to process.""" + + +class OutputSchema(TypedDict): + """Output schema for the map-reduce extraction chain. + + Defines the format of the final result returned by the chain. + """ + + map_results: list[ExtractionResult] + """List of individual extraction results from the map phase.""" + + result: Any + """Final combined result from all documents.""" + + +class MapReduceNodeUpdate(TypedDict): + """Update returned by map-reduce nodes.""" + + map_results: NotRequired[list[ExtractionResult]] + """Updated results after map phase.""" + result: NotRequired[Any] + """Final result after reduce phase.""" + + +class _MapReduceExtractor(Generic[ContextT]): + """Map-reduce extraction implementation using LangGraph Send API. + + This implementation uses a language model to process documents through up + to two phases: + + 1. **Map Phase**: Each document is processed independently by the LLM using + the configured map_prompt to generate individual extraction results. + 2. **Reduce Phase (Optional)**: Individual results can optionally be + combined using either: + - The default LLM-based reducer with the configured reduce_prompt + - A custom reducer function (which can be non-LLM based) + - Skipped entirely by setting reduce=None + + The map phase processes documents in parallel for efficiency, making this approach + well-suited for large document collections. The reduce phase is flexible and can be + customized or omitted based on your specific requirements. + """ + + def __init__( + self, + model: Union[BaseChatModel, str], + *, + map_prompt: Union[ + str, + None, + Callable[ + [MapState, Runtime[ContextT]], + list[MessageLikeRepresentation], + ], + ] = None, + reduce_prompt: Union[ + str, + None, + Callable[ + [MapReduceState, Runtime[ContextT]], + list[MessageLikeRepresentation], + ], + ] = None, + reduce: Union[ + Literal["default_reducer"], + None, + StateNode, + ] = "default_reducer", + context_schema: type[ContextT] | None = None, + response_format: Optional[type[BaseModel]] = None, + ) -> None: + """Initialize the MapReduceExtractor. + + Args: + model: The language model either a chat model instance + (e.g., `ChatAnthropic()`) or string identifier + (e.g., `"anthropic:claude-sonnet-4-20250514"`) + map_prompt: Prompt for individual document processing. Can be: + - str: A system message string + - None: Use default system message + - Callable: A function that takes (state, runtime) and returns messages + reduce_prompt: Prompt for combining results. Can be: + - str: A system message string + - None: Use default system message + - Callable: A function that takes (state, runtime) and returns messages + reduce: Controls the reduce behavior. Can be: + - "default_reducer": Use the default LLM-based reduce step + - None: Skip the reduce step entirely + - Callable: Custom reduce function (sync or async) + context_schema: Optional context schema for the LangGraph runtime. + response_format: Optional pydantic BaseModel for structured output. + """ + if (reduce is None or callable(reduce)) and reduce_prompt is not None: + msg = ( + "reduce_prompt must be None when reduce is None or a custom " + "callable. Custom reduce functions handle their own logic and " + "should not use reduce_prompt." + ) + raise ValueError(msg) + + self.response_format = response_format + + if isinstance(model, str): + model = init_chat_model(model) + + self.model = ( + model.with_structured_output(response_format) if response_format else model + ) + self.map_prompt = map_prompt + self.reduce_prompt = reduce_prompt + self.reduce = reduce + self.context_schema = context_schema + + def _get_map_prompt( + self, state: MapState, runtime: Runtime[ContextT] + ) -> list[MessageLikeRepresentation]: + """Generate the LLM prompt for processing documents.""" + documents = state["documents"] + user_content = "\n\n".join(format_document_xml(doc) for doc in documents) + default_system = ( + "You are a helpful assistant that processes documents. " + "Please process the following documents and provide a result." + ) + + return resolve_prompt( + self.map_prompt, + state, + runtime, + user_content, + default_system, + ) + + async def _aget_map_prompt( + self, state: MapState, runtime: Runtime[ContextT] + ) -> list[MessageLikeRepresentation]: + """Generate the LLM prompt for processing documents in the map phase. + + Async version. + """ + documents = state["documents"] + user_content = "\n\n".join(format_document_xml(doc) for doc in documents) + default_system = ( + "You are a helpful assistant that processes documents. " + "Please process the following documents and provide a result." + ) + + return await aresolve_prompt( + self.map_prompt, + state, + runtime, + user_content, + default_system, + ) + + def _get_reduce_prompt( + self, state: MapReduceState, runtime: Runtime[ContextT] + ) -> list[MessageLikeRepresentation]: + """Generate the LLM prompt for combining individual results. + + Combines map results in the reduce phase. + """ + map_results = state.get("map_results", []) + if not map_results: + msg = ( + "Internal programming error: Results must exist when reducing. " + "This indicates that the reduce node was reached without " + "first processing the map nodes, which violates " + "the expected graph execution order." + ) + raise AssertionError(msg) + + results_text = "\n\n".join( + f"Result {i + 1} (from documents " + f"{', '.join(map(str, result['indexes']))}):\n{result['result']}" + for i, result in enumerate(map_results) + ) + user_content = ( + f"Please combine the following results into a single, " + f"comprehensive result:\n\n{results_text}" + ) + default_system = ( + "You are a helpful assistant that combines multiple results. " + "Given several individual results, create a single comprehensive " + "result that captures the key information from all inputs while " + "maintaining conciseness and coherence." + ) + + return resolve_prompt( + self.reduce_prompt, + state, + runtime, + user_content, + default_system, + ) + + async def _aget_reduce_prompt( + self, state: MapReduceState, runtime: Runtime[ContextT] + ) -> list[MessageLikeRepresentation]: + """Generate the LLM prompt for combining individual results. + + Async version of reduce phase. + """ + map_results = state.get("map_results", []) + if not map_results: + msg = ( + "Internal programming error: Results must exist when reducing. " + "This indicates that the reduce node was reached without " + "first processing the map nodes, which violates " + "the expected graph execution order." + ) + raise AssertionError(msg) + + results_text = "\n\n".join( + f"Result {i + 1} (from documents " + f"{', '.join(map(str, result['indexes']))}):\n{result['result']}" + for i, result in enumerate(map_results) + ) + user_content = ( + f"Please combine the following results into a single, " + f"comprehensive result:\n\n{results_text}" + ) + default_system = ( + "You are a helpful assistant that combines multiple results. " + "Given several individual results, create a single comprehensive " + "result that captures the key information from all inputs while " + "maintaining conciseness and coherence." + ) + + return await aresolve_prompt( + self.reduce_prompt, + state, + runtime, + user_content, + default_system, + ) + + def create_map_node(self) -> RunnableCallable: + """Create a LangGraph node that processes individual documents using the LLM.""" + + def _map_node( + state: MapState, runtime: Runtime[ContextT], config: RunnableConfig + ) -> dict[str, list[ExtractionResult]]: + prompt = self._get_map_prompt(state, runtime) + response = cast("AIMessage", self.model.invoke(prompt, config=config)) + result = response if self.response_format else response.text() + extraction_result: ExtractionResult = { + "indexes": state["indexes"], + "result": result, + } + return {"map_results": [extraction_result]} + + async def _amap_node( + state: MapState, + runtime: Runtime[ContextT], + config: RunnableConfig, + ) -> dict[str, list[ExtractionResult]]: + prompt = await self._aget_map_prompt(state, runtime) + response = cast( + "AIMessage", await self.model.ainvoke(prompt, config=config) + ) + result = response if self.response_format else response.text() + extraction_result: ExtractionResult = { + "indexes": state["indexes"], + "result": result, + } + return {"map_results": [extraction_result]} + + return RunnableCallable( + _map_node, + _amap_node, + trace=False, + ) + + def create_reduce_node(self) -> RunnableCallable: + """Create a LangGraph node that combines individual results using the LLM.""" + + def _reduce_node( + state: MapReduceState, runtime: Runtime[ContextT], config: RunnableConfig + ) -> MapReduceNodeUpdate: + prompt = self._get_reduce_prompt(state, runtime) + response = cast("AIMessage", self.model.invoke(prompt, config=config)) + result = response if self.response_format else response.text() + return {"result": result} + + async def _areduce_node( + state: MapReduceState, + runtime: Runtime[ContextT], + config: RunnableConfig, + ) -> MapReduceNodeUpdate: + prompt = await self._aget_reduce_prompt(state, runtime) + response = cast( + "AIMessage", await self.model.ainvoke(prompt, config=config) + ) + result = response if self.response_format else response.text() + return {"result": result} + + return RunnableCallable( + _reduce_node, + _areduce_node, + trace=False, + ) + + def continue_to_map(self, state: MapReduceState) -> list[Send]: + """Generate Send objects for parallel map operations.""" + return [ + Send("map_process", {"documents": [doc], "indexes": [i]}) + for i, doc in enumerate(state["documents"]) + ] + + def build( + self, + ) -> StateGraph[MapReduceState, ContextT, InputSchema, OutputSchema]: + """Build and compile the LangGraph for map-reduce summarization.""" + builder = StateGraph( + MapReduceState, + context_schema=self.context_schema, + input_schema=InputSchema, + output_schema=OutputSchema, + ) + + builder.add_node("map_process", self.create_map_node()) + + builder.add_edge(START, "continue_to_map") + # Add-conditional edges doesn't explicitly type Send + builder.add_conditional_edges( + "continue_to_map", + self.continue_to_map, # type: ignore[arg-type] + ["map_process"], + ) + + if self.reduce is None: + builder.add_edge("map_process", END) + elif self.reduce == "default_reducer": + builder.add_node("reduce_process", self.create_reduce_node()) + builder.add_edge("map_process", "reduce_process") + builder.add_edge("reduce_process", END) + else: + reduce_node = cast("StateNode", self.reduce) + # The type is ignored here. Requires parameterizing with generics. + builder.add_node("reduce_process", reduce_node) # type: ignore[arg-type] + builder.add_edge("map_process", "reduce_process") + builder.add_edge("reduce_process", END) + + return builder + + +def create_map_reduce_chain( + model: Union[BaseChatModel, str], + *, + map_prompt: Union[ + str, + None, + Callable[[MapState, Runtime[ContextT]], list[MessageLikeRepresentation]], + ] = None, + reduce_prompt: Union[ + str, + None, + Callable[[MapReduceState, Runtime[ContextT]], list[MessageLikeRepresentation]], + ] = None, + reduce: Union[ + Literal["default_reducer"], + None, + StateNode, + ] = "default_reducer", + context_schema: type[ContextT] | None = None, + response_format: Optional[type[BaseModel]] = None, +) -> StateGraph[MapReduceState, ContextT, InputSchema, OutputSchema]: + """Create a map-reduce document extraction chain. + + This implementation uses a language model to extract information from documents + through a flexible approach that efficiently handles large document collections + by processing documents in parallel. + + **Processing Flow:** + 1. **Map Phase**: Each document is independently processed by the LLM + using the map_prompt to extract relevant information and generate + individual results. + 2. **Reduce Phase (Optional)**: Individual extraction results can + optionally be combined using: + - The default LLM-based reducer with reduce_prompt (default behavior) + - A custom reducer function (can be non-LLM based) + - Skipped entirely by setting reduce=None + 3. **Output**: Returns the individual map results and optionally the final + combined result. + + Example: + >>> from langchain_anthropic import ChatAnthropic + >>> from langchain_core.documents import Document + >>> + >>> model = ChatAnthropic( + ... model="claude-sonnet-4-20250514", + ... temperature=0, + ... max_tokens=62_000, + ... timeout=None, + ... max_retries=2, + ... ) + >>> builder = create_map_reduce_chain(model) + >>> chain = builder.compile() + >>> docs = [ + ... Document(page_content="First document content..."), + ... Document(page_content="Second document content..."), + ... Document(page_content="Third document content..."), + ... ] + >>> result = chain.invoke({"documents": docs}) + >>> print(result["result"]) + + Example with string model: + >>> builder = create_map_reduce_chain("anthropic:claude-sonnet-4-20250514") + >>> chain = builder.compile() + >>> result = chain.invoke({"documents": docs}) + >>> print(result["result"]) + + Example with structured output: + ```python + from pydantic import BaseModel + + class ExtractionModel(BaseModel): + title: str + key_points: list[str] + conclusion: str + + builder = create_map_reduce_chain( + model, + response_format=ExtractionModel + ) + chain = builder.compile() + result = chain.invoke({"documents": docs}) + print(result["result"].title) # Access structured fields + ``` + + Example skipping the reduce phase: + ```python + # Only perform map phase, skip combining results + builder = create_map_reduce_chain(model, reduce=None) + chain = builder.compile() + result = chain.invoke({"documents": docs}) + # result["result"] will be None, only map_results are available + for map_result in result["map_results"]: + print(f"Document {map_result['indexes'][0]}: {map_result['result']}") + ``` + + Example with custom reducer: + ```python + def custom_aggregator(state, runtime): + # Custom non-LLM based reduction logic + map_results = state["map_results"] + combined_text = " | ".join(r["result"] for r in map_results) + word_count = len(combined_text.split()) + return { + "result": f"Combined {len(map_results)} results with " + f"{word_count} total words" + } + + builder = create_map_reduce_chain(model, reduce=custom_aggregator) + chain = builder.compile() + result = chain.invoke({"documents": docs}) + print(result["result"]) # Custom aggregated result + ``` + + Args: + model: The language model either a chat model instance + (e.g., `ChatAnthropic()`) or string identifier + (e.g., `"anthropic:claude-sonnet-4-20250514"`) + map_prompt: Prompt for individual document processing. Can be: + - str: A system message string + - None: Use default system message + - Callable: A function that takes (state, runtime) and returns messages + reduce_prompt: Prompt for combining results. Can be: + - str: A system message string + - None: Use default system message + - Callable: A function that takes (state, runtime) and returns messages + reduce: Controls the reduce behavior. Can be: + - "default_reducer": Use the default LLM-based reduce step + - None: Skip the reduce step entirely + - Callable: Custom reduce function (sync or async) + context_schema: Optional context schema for the LangGraph runtime. + response_format: Optional pydantic BaseModel for structured output. + + Returns: + A LangGraph that can be invoked with documents to get map-reduce + extraction results. + + Note: + This implementation is well-suited for large document collections as it + processes documents in parallel during the map phase. The Send API enables + efficient parallelization while maintaining clean state management. + """ + extractor = _MapReduceExtractor( + model, + map_prompt=map_prompt, + reduce_prompt=reduce_prompt, + reduce=reduce, + context_schema=context_schema, + response_format=response_format, + ) + return extractor.build() + + +__all__ = ["create_map_reduce_chain"] diff --git a/libs/langchain_v1/langchain/chains/documents/stuff.py b/libs/langchain_v1/langchain/chains/documents/stuff.py new file mode 100644 index 00000000000..c30b6f711a8 --- /dev/null +++ b/libs/langchain_v1/langchain/chains/documents/stuff.py @@ -0,0 +1,473 @@ +"""Stuff documents chain for processing documents by putting them all in context.""" + +from __future__ import annotations + +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Generic, + Optional, + Union, + cast, +) + +# Used not only for type checking, but is fetched at runtime by Pydantic. +from langchain_core.documents import Document as Document # noqa: TC002 +from langgraph.graph import START, StateGraph +from typing_extensions import NotRequired, TypedDict + +from langchain._internal._documents import format_document_xml +from langchain._internal._prompts import aresolve_prompt, resolve_prompt +from langchain._internal._typing import ContextT +from langchain._internal._utils import RunnableCallable +from langchain.chat_models import init_chat_model + +if TYPE_CHECKING: + from langchain_core.language_models.chat_models import BaseChatModel + + # Used for type checking, but IDEs may not recognize it inside the cast. + from langchain_core.messages import AIMessage as AIMessage + from langchain_core.messages import MessageLikeRepresentation + from langchain_core.runnables import RunnableConfig + from langgraph.runtime import Runtime + from pydantic import BaseModel + + +# Default system prompts +DEFAULT_INIT_PROMPT = ( + "You are a helpful assistant that summarizes text. " + "Please provide a concise summary of the documents " + "provided by the user." +) + +DEFAULT_STRUCTURED_INIT_PROMPT = ( + "You are a helpful assistant that extracts structured information from documents. " + "Use the provided content and optional question to generate your output, formatted " + "according to the predefined schema." +) + +DEFAULT_REFINE_PROMPT = ( + "You are a helpful assistant that refines summaries. " + "Given an existing summary and new context, produce a refined summary " + "that incorporates the new information while maintaining conciseness." +) + +DEFAULT_STRUCTURED_REFINE_PROMPT = ( + "You are a helpful assistant refining structured information extracted " + "from documents. " + "You are given a previous result and new document context. " + "Update the output to reflect the new context, staying consistent with " + "the expected schema." +) + + +def _format_documents_content(documents: list[Document]) -> str: + """Format documents into content string. + + Args: + documents: List of documents to format. + + Returns: + Formatted document content string. + """ + return "\n\n".join(format_document_xml(doc) for doc in documents) + + +class ExtractionState(TypedDict): + """State for extraction chain. + + This state tracks the extraction process where documents + are processed in batch, with the result being refined if needed. + """ + + documents: list[Document] + """List of documents to process.""" + result: NotRequired[Any] + """Current result, refined with each document.""" + + +class InputSchema(TypedDict): + """Input schema for the extraction chain. + + Defines the expected input format when invoking the extraction chain. + """ + + documents: list[Document] + """List of documents to process.""" + result: NotRequired[Any] + """Existing result to refine (optional).""" + + +class OutputSchema(TypedDict): + """Output schema for the extraction chain. + + Defines the format of the final result returned by the chain. + """ + + result: Any + """Result from processing the documents.""" + + +class ExtractionNodeUpdate(TypedDict): + """Update returned by processing nodes.""" + + result: NotRequired[Any] + """Updated result after processing a document.""" + + +class _Extractor(Generic[ContextT]): + """Stuff documents chain implementation. + + This chain works by putting all the documents in the batch into the context + window of the language model. It processes all documents together in a single + request for extracting information or summaries. Can refine existing results + when provided. + + Important: This chain does not attempt to control for the size of the context + window of the LLM. Ensure your documents fit within the model's context limits. + """ + + def __init__( + self, + model: Union[BaseChatModel, str], + *, + prompt: Union[ + str, + None, + Callable[ + [ExtractionState, Runtime[ContextT]], + list[MessageLikeRepresentation], + ], + ] = None, + refine_prompt: Union[ + str, + None, + Callable[ + [ExtractionState, Runtime[ContextT]], + list[MessageLikeRepresentation], + ], + ] = None, + context_schema: type[ContextT] | None = None, + response_format: Optional[type[BaseModel]] = None, + ) -> None: + """Initialize the Extractor. + + Args: + model: The language model either a chat model instance + (e.g., `ChatAnthropic()`) or string identifier + (e.g., `"anthropic:claude-sonnet-4-20250514"`) + prompt: Prompt for initial processing. Can be: + - str: A system message string + - None: Use default system message + - Callable: A function that takes (state, runtime) and returns messages + refine_prompt: Prompt for refinement steps. Can be: + - str: A system message string + - None: Use default system message + - Callable: A function that takes (state, runtime) and returns messages + context_schema: Optional context schema for the LangGraph runtime. + response_format: Optional pydantic BaseModel for structured output. + """ + self.response_format = response_format + + if isinstance(model, str): + model = init_chat_model(model) + + self.model = ( + model.with_structured_output(response_format) if response_format else model + ) + self.initial_prompt = prompt + self.refine_prompt = refine_prompt + self.context_schema = context_schema + + def _get_initial_prompt( + self, state: ExtractionState, runtime: Runtime[ContextT] + ) -> list[MessageLikeRepresentation]: + """Generate the initial extraction prompt.""" + user_content = _format_documents_content(state["documents"]) + + # Choose default prompt based on structured output format + default_prompt = ( + DEFAULT_STRUCTURED_INIT_PROMPT + if self.response_format + else DEFAULT_INIT_PROMPT + ) + + return resolve_prompt( + self.initial_prompt, + state, + runtime, + user_content, + default_prompt, + ) + + async def _aget_initial_prompt( + self, state: ExtractionState, runtime: Runtime[ContextT] + ) -> list[MessageLikeRepresentation]: + """Generate the initial extraction prompt (async version).""" + user_content = _format_documents_content(state["documents"]) + + # Choose default prompt based on structured output format + default_prompt = ( + DEFAULT_STRUCTURED_INIT_PROMPT + if self.response_format + else DEFAULT_INIT_PROMPT + ) + + return await aresolve_prompt( + self.initial_prompt, + state, + runtime, + user_content, + default_prompt, + ) + + def _get_refine_prompt( + self, state: ExtractionState, runtime: Runtime[ContextT] + ) -> list[MessageLikeRepresentation]: + """Generate the refinement prompt.""" + # Result should be guaranteed to exist at refinement stage + if "result" not in state or state["result"] == "": + msg = ( + "Internal programming error: Result must exist when refining. " + "This indicates that the refinement node was reached without " + "first processing the initial result node, which violates " + "the expected graph execution order." + ) + raise AssertionError(msg) + + new_context = _format_documents_content(state["documents"]) + + user_content = ( + f"Previous result:\n{state['result']}\n\n" + f"New context:\n{new_context}\n\n" + f"Please provide a refined result." + ) + + # Choose default prompt based on structured output format + default_prompt = ( + DEFAULT_STRUCTURED_REFINE_PROMPT + if self.response_format + else DEFAULT_REFINE_PROMPT + ) + + return resolve_prompt( + self.refine_prompt, + state, + runtime, + user_content, + default_prompt, + ) + + async def _aget_refine_prompt( + self, state: ExtractionState, runtime: Runtime[ContextT] + ) -> list[MessageLikeRepresentation]: + """Generate the refinement prompt (async version).""" + # Result should be guaranteed to exist at refinement stage + if "result" not in state or state["result"] == "": + msg = ( + "Internal programming error: Result must exist when refining. " + "This indicates that the refinement node was reached without " + "first processing the initial result node, which violates " + "the expected graph execution order." + ) + raise AssertionError(msg) + + new_context = _format_documents_content(state["documents"]) + + user_content = ( + f"Previous result:\n{state['result']}\n\n" + f"New context:\n{new_context}\n\n" + f"Please provide a refined result." + ) + + # Choose default prompt based on structured output format + default_prompt = ( + DEFAULT_STRUCTURED_REFINE_PROMPT + if self.response_format + else DEFAULT_REFINE_PROMPT + ) + + return await aresolve_prompt( + self.refine_prompt, + state, + runtime, + user_content, + default_prompt, + ) + + def create_document_processor_node(self) -> RunnableCallable: + """Create the main document processing node. + + The node handles both initial processing and refinement of results. + + Refinement is done by providing the existing result and new context. + + If the workflow is run with a checkpointer enabled, the result will be + persisted and available for a given thread id. + """ + + def _process_node( + state: ExtractionState, runtime: Runtime[ContextT], config: RunnableConfig + ) -> ExtractionNodeUpdate: + # Handle empty document list + if not state["documents"]: + return {} + + # Determine if this is initial processing or refinement + if "result" not in state or state["result"] == "": + # Initial processing + prompt = self._get_initial_prompt(state, runtime) + response = cast("AIMessage", self.model.invoke(prompt, config=config)) + result = response if self.response_format else response.text() + return {"result": result} + # Refinement + prompt = self._get_refine_prompt(state, runtime) + response = cast("AIMessage", self.model.invoke(prompt, config=config)) + result = response if self.response_format else response.text() + return {"result": result} + + async def _aprocess_node( + state: ExtractionState, + runtime: Runtime[ContextT], + config: RunnableConfig, + ) -> ExtractionNodeUpdate: + # Handle empty document list + if not state["documents"]: + return {} + + # Determine if this is initial processing or refinement + if "result" not in state or state["result"] == "": + # Initial processing + prompt = await self._aget_initial_prompt(state, runtime) + response = cast( + "AIMessage", await self.model.ainvoke(prompt, config=config) + ) + result = response if self.response_format else response.text() + return {"result": result} + # Refinement + prompt = await self._aget_refine_prompt(state, runtime) + response = cast( + "AIMessage", await self.model.ainvoke(prompt, config=config) + ) + result = response if self.response_format else response.text() + return {"result": result} + + return RunnableCallable( + _process_node, + _aprocess_node, + trace=False, + ) + + def build( + self, + ) -> StateGraph[ExtractionState, ContextT, InputSchema, OutputSchema]: + """Build and compile the LangGraph for batch document extraction.""" + builder = StateGraph( + ExtractionState, + context_schema=self.context_schema, + input_schema=InputSchema, + output_schema=OutputSchema, + ) + builder.add_edge(START, "process") + builder.add_node("process", self.create_document_processor_node()) + return builder + + +def create_stuff_documents_chain( + model: Union[BaseChatModel, str], + *, + prompt: Union[ + str, + None, + Callable[[ExtractionState, Runtime[ContextT]], list[MessageLikeRepresentation]], + ] = None, + refine_prompt: Union[ + str, + None, + Callable[[ExtractionState, Runtime[ContextT]], list[MessageLikeRepresentation]], + ] = None, + context_schema: type[ContextT] | None = None, + response_format: Optional[type[BaseModel]] = None, +) -> StateGraph[ExtractionState, ContextT, InputSchema, OutputSchema]: + """Create a stuff documents chain for processing documents. + + This chain works by putting all the documents in the batch into the context + window of the language model. It processes all documents together in a single + request for extracting information or summaries. Can refine existing results + when provided. The default prompts are optimized for summarization tasks, but + can be customized for other extraction tasks via the prompt parameters or + response_format. + + Strategy: + 1. Put all documents into the context window + 2. Process all documents together in a single request + 3. If an existing result is provided, refine it with all documents at once + 4. Return the result + + Important: + This chain does not attempt to control for the size of the context + window of the LLM. Ensure your documents fit within the model's context limits. + + Example: + ```python + from langchain.chat_models import init_chat_model + from langchain_core.documents import Document + + model = init_chat_model("anthropic:claude-sonnet-4-20250514") + builder = create_stuff_documents_chain(model) + chain = builder.compile() + docs = [ + Document(page_content="First document content..."), + Document(page_content="Second document content..."), + Document(page_content="Third document content..."), + ] + result = chain.invoke({"documents": docs}) + print(result["result"]) + + # Structured summary/extraction by passing a schema + from pydantic import BaseModel + + class Summary(BaseModel): + title: str + key_points: list[str] + + builder = create_stuff_documents_chain(model, response_format=Summary) + chain = builder.compile() + result = chain.invoke({"documents": docs}) + print(result["result"].title) # Access structured fields + ``` + + Args: + model: The language model for document processing. + prompt: Prompt for initial processing. Can be: + - str: A system message string + - None: Use default system message + - Callable: A function that takes (state, runtime) and returns messages + refine_prompt: Prompt for refinement steps. Can be: + - str: A system message string + - None: Use default system message + - Callable: A function that takes (state, runtime) and returns messages + context_schema: Optional context schema for the LangGraph runtime. + response_format: Optional pydantic BaseModel for structured output. + + Returns: + A LangGraph that can be invoked with documents to extract information. + + Note: + This is a "stuff" documents chain that puts all documents into the context + window and processes them together. It supports refining existing results. + Default prompts are optimized for summarization but can be customized for + other tasks. Important: Does not control for context window size. + """ + extractor = _Extractor( + model, + prompt=prompt, + refine_prompt=refine_prompt, + context_schema=context_schema, + response_format=response_format, + ) + return extractor.build() + + +__all__ = ["create_stuff_documents_chain"] diff --git a/libs/langchain_v1/langchain/documents/__init__.py b/libs/langchain_v1/langchain/documents/__init__.py new file mode 100644 index 00000000000..6450cfeef07 --- /dev/null +++ b/libs/langchain_v1/langchain/documents/__init__.py @@ -0,0 +1,5 @@ +from langchain_core.documents import Document + +__all__ = [ + "Document", +] diff --git a/libs/langchain_v1/pyproject.toml b/libs/langchain_v1/pyproject.toml index 3b46da5696d..d1e12040524 100644 --- a/libs/langchain_v1/pyproject.toml +++ b/libs/langchain_v1/pyproject.toml @@ -9,7 +9,7 @@ requires-python = ">=3.9, <4.0" dependencies = [ "langchain-core<1.0.0,>=0.3.66", "langchain-text-splitters<1.0.0,>=0.3.8", - "langgraph>=0.5.4", + "langgraph>=0.6.0", "pydantic>=2.7.4", ] @@ -123,6 +123,7 @@ ignore = [ "UP007", # pyupgrade: non-pep604-annotation-union "PLC0415", # Imports should be at the top. Not always desirable "PLR0913", # Too many arguments in function definition + "PLC0414", # Inconsistent with how type checkers expect to be notified of intentional re-exports ] unfixable = ["B028"] # People should intentionally tune the stacklevel diff --git a/libs/langchain_v1/uv.lock b/libs/langchain_v1/uv.lock index 2e2899efa4e..667913a585e 100644 --- a/libs/langchain_v1/uv.lock +++ b/libs/langchain_v1/uv.lock @@ -1667,7 +1667,7 @@ requires-dist = [ { name = "langchain-text-splitters", editable = "../text-splitters" }, { name = "langchain-together", marker = "extra == 'together'" }, { name = "langchain-xai", marker = "extra == 'xai'" }, - { name = "langgraph", specifier = ">=0.5.4" }, + { name = "langgraph", specifier = ">=0.6.0" }, { name = "pydantic", specifier = ">=2.7.4" }, ] provides-extras = ["anthropic", "openai", "azure-ai", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai", "perplexity"] @@ -1757,7 +1757,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "0.3.70" +version = "0.3.72" source = { editable = "../core" } dependencies = [ { name = "jsonpatch" }, @@ -2133,7 +2133,7 @@ wheels = [ [[package]] name = "langgraph" -version = "0.5.4" +version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -2143,9 +2143,9 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/26/f01ae40ea26f8c723b6ec186869c80cc04de801630d99943018428b46105/langgraph-0.5.4.tar.gz", hash = "sha256:ab8f6b7b9c50fd2ae35a2efb072fbbfe79500dfc18071ac4ba6f5de5fa181931", size = 443149, upload-time = "2025-07-21T18:20:55.63Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/12/4a30f766de571bfc319e70c2c0f4d050c0576e15c2249dc75ad122706a5d/langgraph-0.6.1.tar.gz", hash = "sha256:e4399ac5ad0b70f58fa28d6fe05a41b84c15959f270d6d1a86edab4e92ae148b", size = 449723, upload-time = "2025-07-29T20:45:28.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/82/15184e953234877107bad182b79c9111cb6ce6a79a97fdf36ebcaa11c0d0/langgraph-0.5.4-py3-none-any.whl", hash = "sha256:7122840225623e081be24ac30a691a24e5dac4c0361f593208f912838192d7f6", size = 143942, upload-time = "2025-07-21T18:20:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/6e/90/e37e2bc19bee81f859bfd61526d977f54ec5d8e036fa091f2cfe4f19560b/langgraph-0.6.1-py3-none-any.whl", hash = "sha256:2736027faeb6cd5c0f1ab51a5345594cfcb5eb5beeb5ac1799a58fcecf4b4eae", size = 151874, upload-time = "2025-07-29T20:45:26.998Z" }, ] [[package]] @@ -2163,28 +2163,28 @@ wheels = [ [[package]] name = "langgraph-prebuilt" -version = "0.5.2" +version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph-checkpoint" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/11/98134c47832fbde0caf0e06f1a104577da9215c358d7854093c1d835b272/langgraph_prebuilt-0.5.2.tar.gz", hash = "sha256:2c900a5be0d6a93ea2521e0d931697cad2b646f1fcda7aa5c39d8d7539772465", size = 117808, upload-time = "2025-06-30T19:52:48.307Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/b6/d4f8e800bdfdd75486595203d5c622bba5f1098e4fd4220452c75568f2be/langgraph_prebuilt-0.6.1.tar.gz", hash = "sha256:574c409113e02d3c58157877c5ea638faa80647b259027647ab88830d7ecef00", size = 125057, upload-time = "2025-07-29T20:44:48.634Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/64/6bc45ab9e0e1112698ebff579fe21f5606ea65cd08266995a357e312a4d2/langgraph_prebuilt-0.5.2-py3-none-any.whl", hash = "sha256:1f4cd55deca49dffc3e5127eec12fcd244fc381321002f728afa88642d5ec59d", size = 23776, upload-time = "2025-06-30T19:52:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/cb4d73e99719b7ca0d42503d39dec49c67225779c6a1e689954a5604dbe6/langgraph_prebuilt-0.6.1-py3-none-any.whl", hash = "sha256:a3a970451371ec66509c6969505286a5d92132af7062d0b2b6dab08c2e27b50f", size = 28866, upload-time = "2025-07-29T20:44:47.72Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.1.74" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "orjson" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/f7/3807b72988f7eef5e0eb41e7e695eca50f3ed31f7cab5602db3b651c85ff/langgraph_sdk-0.1.74.tar.gz", hash = "sha256:7450e0db5b226cc2e5328ca22c5968725873630ef47c4206a30707cb25dc3ad6", size = 72190, upload-time = "2025-07-21T16:36:50.032Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/3e/3dc45dc7682c9940db9edaf8773d2e157397c5bd6881f6806808afd8731e/langgraph_sdk-0.2.0.tar.gz", hash = "sha256:cd8b5f6595e5571be5cbffd04cf936978ab8f5d1005517c99715947ef871e246", size = 72510, upload-time = "2025-07-22T17:31:06.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/1a/3eacc4df8127781ee4b0b1e5cad7dbaf12510f58c42cbcb9d1e2dba2a164/langgraph_sdk-0.1.74-py3-none-any.whl", hash = "sha256:3a265c3757fe0048adad4391d10486db63ef7aa5a2cbd22da22d4503554cb890", size = 50254, upload-time = "2025-07-21T16:36:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/a5/03/a8ab0e8ea74be6058cb48bb1d85485b5c65d6ea183e3ee1aa8ca1ac73b3e/langgraph_sdk-0.2.0-py3-none-any.whl", hash = "sha256:150722264f225c4d47bbe7394676be102fdbf04c4400a0dd1bd41a70c6430cc7", size = 50569, upload-time = "2025-07-22T17:31:04.582Z" }, ] [[package]]