From e1d4b5552d5c1f1c234f6c087f146096d736cf41 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 3 Apr 2025 16:51:53 -0700 Subject: [PATCH 01/32] Add checkpoint_during arg - This provides a new mode of execution where only the last checkpoint is saved - We save the last checkpoint no matter how the agent run is terminated (success, error, interrupt, etc) - This cuts down on cpu time spent on checkpointing, while not losing any resilience benefits, given individual task writes are still saved - If an error occurs and the run is retried, any tasks that completed successfully before will be skipped (as currently) - checkpoint_during=True is useful when you want to time-travel to inner steps of a run - The default value will remain the current behavior, ie. checkpoint_during=True --- libs/langgraph/bench/__main__.py | 4 +++ libs/langgraph/langgraph/pregel/__init__.py | 10 +++++++ libs/langgraph/langgraph/pregel/loop.py | 33 ++++++++++++++------- libs/langgraph/tests/test_interruption.py | 21 ++++++------- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/libs/langgraph/bench/__main__.py b/libs/langgraph/bench/__main__.py index 033b5034b..8be3cf5c8 100644 --- a/libs/langgraph/bench/__main__.py +++ b/libs/langgraph/bench/__main__.py @@ -25,6 +25,7 @@ async def arun(graph: Pregel, input: dict): "configurable": {"thread_id": str(uuid4())}, "recursion_limit": 1000000000, }, + checkpoint_during=False, ) ] ) @@ -41,6 +42,7 @@ async def arun_first_event_latency(graph: Pregel, input: dict) -> None: "configurable": {"thread_id": str(uuid4())}, "recursion_limit": 1000000000, }, + checkpoint_during=False, ) try: @@ -60,6 +62,7 @@ def run(graph: Pregel, input: dict): "configurable": {"thread_id": str(uuid4())}, "recursion_limit": 1000000000, }, + checkpoint_during=False, ) ] ) @@ -76,6 +79,7 @@ def run_first_event_latency(graph: Pregel, input: dict) -> None: "configurable": {"thread_id": str(uuid4())}, "recursion_limit": 1000000000, }, + checkpoint_during=False, ) try: diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 545c8faf1..cbfbd12b9 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -2094,6 +2094,7 @@ class Pregel(PregelProtocol): output_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, + checkpoint_during: bool = True, debug: Optional[bool] = None, subgraphs: bool = False, ) -> Iterator[Union[dict[str, Any], Any]]: @@ -2115,6 +2116,7 @@ class Pregel(PregelProtocol): output_keys: The keys to stream, defaults to all non-context channels. interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph. interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph. + checkpoint_during: Whether to checkpoint intermediate steps, defaults to True. If False, only the final checkpoint is saved. debug: Whether to print debug information during execution, defaults to False. subgraphs: Whether to stream subgraphs, defaults to False. @@ -2291,6 +2293,7 @@ class Pregel(PregelProtocol): interrupt_after=interrupt_after_, manager=run_manager, debug=debug, + checkpoint_during=checkpoint_during, trigger_to_nodes=self.trigger_to_nodes, migrate_checkpoint=self._migrate_checkpoint, ) as loop: @@ -2373,6 +2376,7 @@ class Pregel(PregelProtocol): output_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, + checkpoint_during: bool = True, debug: Optional[bool] = None, subgraphs: bool = False, ) -> AsyncIterator[Union[dict[str, Any], Any]]: @@ -2394,6 +2398,7 @@ class Pregel(PregelProtocol): output_keys: The keys to stream, defaults to all non-context channels. interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph. interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph. + checkpoint_during: Whether to checkpoint intermediate steps, defaults to True. If False, only the final checkpoint is saved. debug: Whether to print debug information during execution, defaults to False. subgraphs: Whether to stream subgraphs, defaults to False. @@ -2586,6 +2591,7 @@ class Pregel(PregelProtocol): interrupt_after=interrupt_after_, manager=run_manager, debug=debug, + checkpoint_during=checkpoint_during, trigger_to_nodes=self.trigger_to_nodes, migrate_checkpoint=self._migrate_checkpoint, ) as loop: @@ -2661,6 +2667,7 @@ class Pregel(PregelProtocol): output_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, + checkpoint_during: bool = True, debug: Optional[bool] = None, **kwargs: Any, ) -> Union[dict[str, Any], Any]: @@ -2692,6 +2699,7 @@ class Pregel(PregelProtocol): output_keys=output_keys, interrupt_before=interrupt_before, interrupt_after=interrupt_after, + checkpoint_during=checkpoint_during, debug=debug, **kwargs, ): @@ -2713,6 +2721,7 @@ class Pregel(PregelProtocol): output_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, + checkpoint_during: bool = True, debug: Optional[bool] = None, **kwargs: Any, ) -> Union[dict[str, Any], Any]: @@ -2745,6 +2754,7 @@ class Pregel(PregelProtocol): output_keys=output_keys, interrupt_before=interrupt_before, interrupt_after=interrupt_after, + checkpoint_during=checkpoint_during, debug=debug, **kwargs, ): diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 4c4ed6dde..00469fbbf 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -155,7 +155,7 @@ class PregelLoop(LoopProtocol): manager: Union[None, AsyncParentRunManager, ParentRunManager] interrupt_after: Union[All, Sequence[str]] interrupt_before: Union[All, Sequence[str]] - checkpoint_every_step: bool + checkpoint_during: bool debug: bool checkpointer_get_next_version: GetNextVersion @@ -215,7 +215,7 @@ class PregelLoop(LoopProtocol): debug: bool = False, migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None, trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None, - checkpoint_every_step: bool = True, + checkpoint_during: bool = True, ) -> None: super().__init__( step=0, @@ -241,7 +241,7 @@ class PregelLoop(LoopProtocol): ) self._migrate_checkpoint = migrate_checkpoint self.trigger_to_nodes = trigger_to_nodes - self.checkpoint_every_step = checkpoint_every_step + self.checkpoint_during = checkpoint_during self.debug = debug if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]: self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM]) @@ -709,10 +709,13 @@ class PregelLoop(LoopProtocol): ) return updated_channels - def _put_checkpoint(self, metadata: CheckpointMetadata) -> None: + def _put_checkpoint( + self, metadata: CheckpointMetadata, force: bool = False + ) -> None: # assign step and parents metadata["step"] = self.step metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {}) + self.checkpoint_metadata = metadata # debug flag if self.debug: print_step_checkpoint( @@ -724,19 +727,21 @@ class PregelLoop(LoopProtocol): else self.stream_keys ), ) + # do checkpoint? + do_checkpoint = self._checkpointer_put_after_previous is not None and ( + force or self.checkpoint_during + ) + # create new checkpoint + self.checkpoint = create_checkpoint( + self.checkpoint, self.channels if do_checkpoint else None, self.step + ) # bail if no checkpointer - if self._checkpointer_put_after_previous is not None: + if do_checkpoint: for k, v in self.config["metadata"].items(): if k in EXCLUDED_METADATA_KEYS: continue metadata.setdefault(k, v) # type: ignore - # create new checkpoint - self.checkpoint = create_checkpoint( - self.checkpoint, self.channels, self.step - ) - self.checkpoint_metadata = metadata - self.prev_checkpoint_config = ( self.checkpoint_config if CONFIG_KEY_CHECKPOINT_ID in self.checkpoint_config[CONF] @@ -789,6 +794,8 @@ class PregelLoop(LoopProtocol): exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> Optional[bool]: + if not self.checkpoint_during: + self._put_checkpoint(self.checkpoint_metadata, force=True) # suppress interrupt suppress = isinstance(exc_value, GraphInterrupt) and not self.is_nested if suppress: @@ -907,6 +914,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): debug: bool = False, migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None, trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None, + checkpoint_during: bool = True, ) -> None: super().__init__( input, @@ -925,6 +933,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): debug=debug, migrate_checkpoint=migrate_checkpoint, trigger_to_nodes=trigger_to_nodes, + checkpoint_during=checkpoint_during, ) self.stack = ExitStack() if checkpointer: @@ -1054,6 +1063,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): debug: bool = False, migrate_checkpoint: Optional[Callable[[Checkpoint], None]] = None, trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None, + checkpoint_during: bool = True, ) -> None: super().__init__( input, @@ -1072,6 +1082,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): debug=debug, migrate_checkpoint=migrate_checkpoint, trigger_to_nodes=trigger_to_nodes, + checkpoint_during=checkpoint_during, ) self.stack = AsyncExitStack() if checkpointer: diff --git a/libs/langgraph/tests/test_interruption.py b/libs/langgraph/tests/test_interruption.py index aa543cf00..9d262bffc 100644 --- a/libs/langgraph/tests/test_interruption.py +++ b/libs/langgraph/tests/test_interruption.py @@ -1,5 +1,4 @@ import pytest -from pytest_mock import MockerFixture from typing_extensions import TypedDict from langgraph.graph import END, START, StateGraph @@ -12,9 +11,10 @@ from tests.conftest import ( pytestmark = pytest.mark.anyio +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_interruption_without_state_updates( - request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture + request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool ) -> None: """Test interruption without state updates. This test confirms that interrupting doesn't require a state key having been updated in the prev step""" @@ -40,20 +40,21 @@ def test_interruption_without_state_updates( initial_input = {"input": "hello world"} thread = {"configurable": {"thread_id": "1"}} - graph.invoke(initial_input, thread, debug=True) + graph.invoke(initial_input, thread, checkpoint_during=checkpoint_during) assert graph.get_state(thread).next == ("step_2",) - graph.invoke(None, thread, debug=True) + graph.invoke(None, thread, checkpoint_during=checkpoint_during) assert graph.get_state(thread).next == ("step_3",) - graph.invoke(None, thread, debug=True) + graph.invoke(None, thread, checkpoint_during=checkpoint_during) assert graph.get_state(thread).next == () +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_interruption_without_state_updates_async( - checkpointer_name: str, mocker: MockerFixture -): + checkpointer_name: str, checkpoint_during: bool +) -> None: """Test interruption without state updates. This test confirms that interrupting doesn't require a state key having been updated in the prev step""" @@ -78,11 +79,11 @@ async def test_interruption_without_state_updates_async( initial_input = {"input": "hello world"} thread = {"configurable": {"thread_id": "1"}} - await graph.ainvoke(initial_input, thread, debug=True) + await graph.ainvoke(initial_input, thread, checkpoint_during=checkpoint_during) assert (await graph.aget_state(thread)).next == ("step_2",) - await graph.ainvoke(None, thread, debug=True) + await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during) assert (await graph.aget_state(thread)).next == ("step_3",) - await graph.ainvoke(None, thread, debug=True) + await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during) assert (await graph.aget_state(thread)).next == () From 7e08339335b46f4cb820b5691f223de0ffea2178 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 3 Apr 2025 16:55:23 -0700 Subject: [PATCH 02/32] mypy is dumb --- libs/langgraph/langgraph/pregel/loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 00469fbbf..302bb4744 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -736,7 +736,7 @@ class PregelLoop(LoopProtocol): self.checkpoint, self.channels if do_checkpoint else None, self.step ) # bail if no checkpointer - if do_checkpoint: + if do_checkpoint and self._checkpointer_put_after_previous is not None: for k, v in self.config["metadata"].items(): if k in EXCLUDED_METADATA_KEYS: continue From 0a1dd7a01aba32443338f1d455f8bb3505632860 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 3 Apr 2025 17:31:34 -0700 Subject: [PATCH 03/32] Do same thing for writes --- libs/langgraph/langgraph/pregel/loop.py | 44 ++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 302bb4744..4a42f95a5 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -316,7 +316,7 @@ class PregelLoop(LoopProtocol): self.checkpoint_pending_writes[idx] = (task_id, c, v) else: self.checkpoint_pending_writes.append((task_id, c, v)) - if self.checkpointer_put_writes is not None: + if self.checkpoint_during and self.checkpointer_put_writes is not None: config = patch_configurable( self.checkpoint_config, { @@ -349,6 +349,46 @@ class PregelLoop(LoopProtocol): if hasattr(self, "tasks"): self._output_writes(task_id, writes) + def _put_pending_writes(self) -> None: + if self.checkpointer_put_writes is None: + return + if not self.checkpoint_pending_writes: + return + # group by task id + by_task = defaultdict(list) + for task_id, channel, value in self.checkpoint_pending_writes: + by_task[task_id].append((channel, value)) + # patch config with checkpoint id + config = patch_configurable( + self.checkpoint_config, + { + CONFIG_KEY_CHECKPOINT_NS: self.config[CONF].get( + CONFIG_KEY_CHECKPOINT_NS, "" + ), + CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"], + }, + ) + # submit writes to checkpointer + for task_id, writes in by_task.items(): + if self.checkpointer_put_writes_accepts_task_path and hasattr( + self, "tasks" + ): + task = self.tasks.get(task_id) + self.submit( + self.checkpointer_put_writes, + config, + writes, + task_id, + task_path_str(task.path) if task else "", + ) + else: + self.submit( + self.checkpointer_put_writes, + config, + writes, + task_id, + ) + def accept_push( self, task: PregelExecutableTask, write_idx: int, call: Optional[Call] = None ) -> Optional[PregelExecutableTask]: @@ -794,7 +834,9 @@ class PregelLoop(LoopProtocol): exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> Optional[bool]: + # persist current checkpoint and writes if not self.checkpoint_during: + self._put_pending_writes() self._put_checkpoint(self.checkpoint_metadata, force=True) # suppress interrupt suppress = isinstance(exc_value, GraphInterrupt) and not self.is_nested From 7ebd6f5e1f9d463f78f9b77d072e991a70540e6c Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 4 Apr 2025 10:01:35 -0700 Subject: [PATCH 04/32] Better test --- libs/langgraph/tests/test_interruption.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/libs/langgraph/tests/test_interruption.py b/libs/langgraph/tests/test_interruption.py index 9d262bffc..f305d94d6 100644 --- a/libs/langgraph/tests/test_interruption.py +++ b/libs/langgraph/tests/test_interruption.py @@ -3,8 +3,8 @@ from typing_extensions import TypedDict from langgraph.graph import END, START, StateGraph from tests.conftest import ( - ALL_CHECKPOINTERS_ASYNC, - ALL_CHECKPOINTERS_SYNC, + REGULAR_CHECKPOINTERS_ASYNC, + REGULAR_CHECKPOINTERS_SYNC, awith_checkpointer, ) @@ -12,7 +12,7 @@ pytestmark = pytest.mark.anyio @pytest.mark.parametrize("checkpoint_during", [True, False]) -@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_SYNC) def test_interruption_without_state_updates( request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool ) -> None: @@ -42,16 +42,22 @@ def test_interruption_without_state_updates( graph.invoke(initial_input, thread, checkpoint_during=checkpoint_during) assert graph.get_state(thread).next == ("step_2",) + n_checkpoints = len([c for c in graph.get_state_history(thread)]) + assert n_checkpoints == (3 if checkpoint_during else 1) graph.invoke(None, thread, checkpoint_during=checkpoint_during) assert graph.get_state(thread).next == ("step_3",) + n_checkpoints = len([c for c in graph.get_state_history(thread)]) + assert n_checkpoints == (4 if checkpoint_during else 2) graph.invoke(None, thread, checkpoint_during=checkpoint_during) assert graph.get_state(thread).next == () + n_checkpoints = len([c for c in graph.get_state_history(thread)]) + assert n_checkpoints == (5 if checkpoint_during else 3) @pytest.mark.parametrize("checkpoint_during", [True, False]) -@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC) async def test_interruption_without_state_updates_async( checkpointer_name: str, checkpoint_during: bool ) -> None: @@ -81,9 +87,15 @@ async def test_interruption_without_state_updates_async( await graph.ainvoke(initial_input, thread, checkpoint_during=checkpoint_during) assert (await graph.aget_state(thread)).next == ("step_2",) + n_checkpoints = len([c async for c in graph.aget_state_history(thread)]) + assert n_checkpoints == (3 if checkpoint_during else 1) await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during) assert (await graph.aget_state(thread)).next == ("step_3",) + n_checkpoints = len([c async for c in graph.aget_state_history(thread)]) + assert n_checkpoints == (4 if checkpoint_during else 2) await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during) assert (await graph.aget_state(thread)).next == () + n_checkpoints = len([c async for c in graph.aget_state_history(thread)]) + assert n_checkpoints == (5 if checkpoint_during else 3) From 4f353dac3107f03e4ce2bab298ee695337aecf0d Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 4 Apr 2025 14:41:10 -0700 Subject: [PATCH 05/32] Fix assignment of pending writes --- libs/langgraph/langgraph/pregel/loop.py | 62 +++++++++++-------------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 4a42f95a5..6333c932f 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -358,16 +358,6 @@ class PregelLoop(LoopProtocol): by_task = defaultdict(list) for task_id, channel, value in self.checkpoint_pending_writes: by_task[task_id].append((channel, value)) - # patch config with checkpoint id - config = patch_configurable( - self.checkpoint_config, - { - CONFIG_KEY_CHECKPOINT_NS: self.config[CONF].get( - CONFIG_KEY_CHECKPOINT_NS, "" - ), - CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"], - }, - ) # submit writes to checkpointer for task_id, writes in by_task.items(): if self.checkpointer_put_writes_accepts_task_path and hasattr( @@ -376,7 +366,7 @@ class PregelLoop(LoopProtocol): task = self.tasks.get(task_id) self.submit( self.checkpointer_put_writes, - config, + self.checkpoint_config, writes, task_id, task_path_str(task.path) if task else "", @@ -384,7 +374,7 @@ class PregelLoop(LoopProtocol): else: self.submit( self.checkpointer_put_writes, - config, + self.checkpoint_config, writes, task_id, ) @@ -749,31 +739,34 @@ class PregelLoop(LoopProtocol): ) return updated_channels - def _put_checkpoint( - self, metadata: CheckpointMetadata, force: bool = False - ) -> None: + def _put_checkpoint(self, metadata: CheckpointMetadata) -> None: # assign step and parents - metadata["step"] = self.step - metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {}) - self.checkpoint_metadata = metadata - # debug flag - if self.debug: - print_step_checkpoint( - metadata, - self.channels, - ( - [self.stream_keys] - if isinstance(self.stream_keys, str) - else self.stream_keys - ), - ) + exiting = metadata is self.checkpoint_metadata + if not exiting: + metadata["step"] = self.step + metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {}) + self.checkpoint_metadata = metadata + # debug flag + if self.debug: + print_step_checkpoint( + metadata, + self.channels, + ( + [self.stream_keys] + if isinstance(self.stream_keys, str) + else self.stream_keys + ), + ) # do checkpoint? do_checkpoint = self._checkpointer_put_after_previous is not None and ( - force or self.checkpoint_during + exiting or self.checkpoint_during ) # create new checkpoint self.checkpoint = create_checkpoint( - self.checkpoint, self.channels if do_checkpoint else None, self.step + self.checkpoint, + self.channels if do_checkpoint else None, + self.step, + id=self.checkpoint["id"] if exiting else None, ) # bail if no checkpointer if do_checkpoint and self._checkpointer_put_after_previous is not None: @@ -822,8 +815,9 @@ class PregelLoop(LoopProtocol): CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"], }, } - # increment step - self.step += 1 + if not exiting: + # increment step + self.step += 1 def _update_mv(self, key: str, values: Sequence[Any]) -> None: raise NotImplementedError @@ -836,8 +830,8 @@ class PregelLoop(LoopProtocol): ) -> Optional[bool]: # persist current checkpoint and writes if not self.checkpoint_during: + self._put_checkpoint(self.checkpoint_metadata) self._put_pending_writes() - self._put_checkpoint(self.checkpoint_metadata, force=True) # suppress interrupt suppress = isinstance(exc_value, GraphInterrupt) and not self.is_nested if suppress: From a5495e84c8d2a1824ec23efa032d09b6d9ce7267 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 4 Apr 2025 15:42:11 -0700 Subject: [PATCH 06/32] Add another test --- libs/langgraph/langgraph/pregel/loop.py | 29 +++++++----------- libs/langgraph/tests/test_pregel.py | 35 ++++++++++++++++----- libs/langgraph/tests/test_pregel_async.py | 37 ++++++++++++++++++----- 3 files changed, 67 insertions(+), 34 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 6333c932f..86b9ba8dc 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -180,6 +180,7 @@ class PregelLoop(LoopProtocol): channels: Mapping[str, BaseChannel] managed: ManagedValueMapping checkpoint: Checkpoint + checkpoint_id_saved: str checkpoint_ns: tuple[str, ...] checkpoint_config: RunnableConfig checkpoint_metadata: CheckpointMetadata @@ -297,25 +298,12 @@ class PregelLoop(LoopProtocol): # deduplicate writes to special channels, last write wins if all(w[0] in WRITES_IDX_MAP for w in writes): writes = list({w[0]: w for w in writes}.values()) + # remove existing writes for this task + self.checkpoint_pending_writes = [ + w for w in self.checkpoint_pending_writes if w[0] != task_id + ] # save writes - for c, v in writes: - if ( - c in WRITES_IDX_MAP - and ( - idx := next( - ( - i - for i, w in enumerate(self.checkpoint_pending_writes) - if w[0] == task_id and w[1] == c - ), - None, - ) - ) - is not None - ): - self.checkpoint_pending_writes[idx] = (task_id, c, v) - else: - self.checkpoint_pending_writes.append((task_id, c, v)) + self.checkpoint_pending_writes.extend((task_id, c, v) for c, v in writes) if self.checkpoint_during and self.checkpointer_put_writes is not None: config = patch_configurable( self.checkpoint_config, @@ -742,6 +730,9 @@ class PregelLoop(LoopProtocol): def _put_checkpoint(self, metadata: CheckpointMetadata) -> None: # assign step and parents exiting = metadata is self.checkpoint_metadata + if exiting and self.checkpoint["id"] == self.checkpoint_id_saved: + # checkpoint already saved + return if not exiting: metadata["step"] = self.step metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {}) @@ -1049,6 +1040,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): }, } self.prev_checkpoint_config = saved.parent_config + self.checkpoint_id_saved = saved.checkpoint["id"] self.checkpoint = saved.checkpoint self.checkpoint_metadata = saved.metadata self.checkpoint_pending_writes = ( @@ -1198,6 +1190,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): }, } self.prev_checkpoint_config = saved.parent_config + self.checkpoint_id_saved = saved.checkpoint["id"] self.checkpoint = saved.checkpoint self.checkpoint_metadata = saved.metadata self.checkpoint_pending_writes = ( diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 6d47f36e7..e03ceeb72 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -1115,10 +1115,14 @@ def test_invoke_checkpoint_two( assert checkpoint["channel_values"].get("total") == 5 +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_pending_writes_resume( - request: pytest.FixtureRequest, checkpointer_name: str + request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool ) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Checkpointing during execution not supported") + checkpointer: BaseCheckpointSaver = request.getfixturevalue( f"checkpointer_{checkpointer_name}" ) @@ -1144,17 +1148,19 @@ def test_pending_writes_resume( self.calls = 0 one = AwhileMaker(0.1, {"value": 2}) - two = AwhileMaker(0.3, ConnectionError("I'm not good")) + two = AwhileMaker(0.2, ConnectionError("I'm not good")) builder = StateGraph(State) builder.add_node("one", one) - builder.add_node("two", two, retry=RetryPolicy(max_attempts=2)) + builder.add_node( + "two", two, retry=RetryPolicy(max_attempts=2, initial_interval=0, jitter=False) + ) builder.add_edge(START, "one") builder.add_edge(START, "two") graph = builder.compile(checkpointer=checkpointer) thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} with pytest.raises(ConnectionError, match="I'm not good"): - graph.invoke({"value": 1}, thread1) + graph.invoke({"value": 1}, thread1, checkpoint_during=checkpoint_during) # both nodes should have been called once assert one.calls == 1 @@ -1200,7 +1206,7 @@ def test_pending_writes_resume( # resume execution with pytest.raises(ConnectionError, match="I'm not good"): - graph.invoke(None, thread1) + graph.invoke(None, thread1, checkpoint_during=checkpoint_during) # node "one" succeeded previously, so shouldn't be called again assert one.calls == 1 @@ -1214,7 +1220,9 @@ def test_pending_writes_resume( # resume execution, without exception two.rtn = {"value": 3} # both the pending write and the new write were applied, 1 + 2 + 3 = 6 - assert graph.invoke(None, thread1) == {"value": 6} + assert graph.invoke(None, thread1, checkpoint_during=checkpoint_during) == { + "value": 6 + } if "shallow" in checkpointer_name: assert len(list(checkpointer.list(thread1))) == 1 @@ -1223,7 +1231,7 @@ def test_pending_writes_resume( # check all final checkpoints checkpoints = [c for c in checkpointer.list(thread1)] # we should have 3 - assert len(checkpoints) == 3 + assert len(checkpoints) == (3 if checkpoint_during else 2) # the last one not too interesting for this test assert checkpoints[0] == CheckpointTuple( config={ @@ -1327,13 +1335,24 @@ def test_pending_writes_resume( "checkpoint_ns": "", "checkpoint_id": checkpoints[2].config["configurable"]["checkpoint_id"], } - }, + } + if checkpoint_during + else None, pending_writes=UnsortedSequence( (AnyStr(), "value", 2), (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), (AnyStr(), "value", 3), + ) + if checkpoint_during + else UnsortedSequence( + (AnyStr(), "value", 2), + (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), + # the write against the previous checkpoint is not saved, as it is + # produced in a run where only the next checkpoint (the last) is saved ), ) + if not checkpoint_during: + return assert checkpoints[2] == CheckpointTuple( config={ "configurable": { diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 327236d7f..9fbf042fc 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1947,10 +1947,14 @@ async def test_invoke_checkpoint(mocker: MockerFixture, checkpointer_name: str) assert checkpoint["channel_values"].get("total") == 5 +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_pending_writes_resume( - request: pytest.FixtureRequest, checkpointer_name: str + checkpointer_name: str, checkpoint_during: bool ) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Checkpointing during execution not supported") + class State(TypedDict): value: Annotated[int, operator.add] @@ -1972,10 +1976,12 @@ async def test_pending_writes_resume( self.calls = 0 one = AwhileMaker(0.1, {"value": 2}) - two = AwhileMaker(0.3, ConnectionError("I'm not good")) + two = AwhileMaker(0.2, ConnectionError("I'm not good")) builder = StateGraph(State) builder.add_node("one", one) - builder.add_node("two", two, retry=RetryPolicy(max_attempts=2)) + builder.add_node( + "two", two, retry=RetryPolicy(max_attempts=2, initial_interval=0, jitter=False) + ) builder.add_edge(START, "one") builder.add_edge(START, "two") async with awith_checkpointer(checkpointer_name) as checkpointer: @@ -1983,7 +1989,9 @@ async def test_pending_writes_resume( thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} with pytest.raises(ConnectionError, match="I'm not good"): - await graph.ainvoke({"value": 1}, thread1) + await graph.ainvoke( + {"value": 1}, thread1, checkpoint_during=checkpoint_during + ) # both nodes should have been called once assert one.calls == 1 @@ -2034,7 +2042,7 @@ async def test_pending_writes_resume( # resume execution with pytest.raises(ConnectionError, match="I'm not good"): - await graph.ainvoke(None, thread1) + await graph.ainvoke(None, thread1, checkpoint_during=checkpoint_during) # node "one" succeeded previously, so shouldn't be called again assert one.calls == 1 @@ -2048,7 +2056,9 @@ async def test_pending_writes_resume( # resume execution, without exception two.rtn = {"value": 3} # both the pending write and the new write were applied, 1 + 2 + 3 = 6 - assert await graph.ainvoke(None, thread1) == {"value": 6} + assert await graph.ainvoke( + None, thread1, checkpoint_during=checkpoint_during + ) == {"value": 6} if "shallow" in checkpointer_name: assert len([c async for c in checkpointer.alist(thread1)]) == 1 @@ -2057,7 +2067,7 @@ async def test_pending_writes_resume( # check all final checkpoints checkpoints = [c async for c in checkpointer.alist(thread1)] # we should have 3 - assert len(checkpoints) == 3 + assert len(checkpoints) == (3 if checkpoint_during else 2) # the last one not too interesting for this test assert checkpoints[0] == CheckpointTuple( config={ @@ -2165,13 +2175,24 @@ async def test_pending_writes_resume( "checkpoint_id" ], } - }, + } + if checkpoint_during + else None, pending_writes=UnsortedSequence( (AnyStr(), "value", 2), (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), (AnyStr(), "value", 3), + ) + if checkpoint_during + else UnsortedSequence( + (AnyStr(), "value", 2), + (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), + # the write against the previous checkpoint is not saved, as it is + # produced in a run where only the next checkpoint (the last) is saved ), ) + if not checkpoint_during: + return assert checkpoints[2] == CheckpointTuple( config={ "configurable": { From 4abfc7702d140a2e6cb2d2679f223c63a4c2b5f9 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 4 Apr 2025 16:00:22 -0700 Subject: [PATCH 07/32] Subgraphs inherit checkpoint mode --- libs/langgraph/langgraph/constants.py | 2 ++ libs/langgraph/langgraph/pregel/__init__.py | 23 +++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index 4fde5e7dd..8bcbb2b94 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -83,6 +83,8 @@ CONFIG_KEY_PREVIOUS = sys.intern("__pregel_previous") # holds the previous return value from a stateful Pregel graph. CONFIG_KEY_RUNNER_SUBMIT = sys.intern("__pregel_runner_submit") # holds a function that receives tasks from runner, executes them and returns results +CONFIG_KEY_CHECKPOINT_DURING = sys.intern("__pregel_checkpoint_during") +# holds a boolean indicating whether to checkpoint during the run (or only at the end) # --- Other constants --- PUSH = sys.intern("__pregel_push") diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index cbfbd12b9..3a182ea3c 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -54,6 +54,7 @@ from langgraph.checkpoint.base import ( ) from langgraph.constants import ( CONF, + CONFIG_KEY_CHECKPOINT_DURING, CONFIG_KEY_CHECKPOINT_ID, CONFIG_KEY_CHECKPOINT_NS, CONFIG_KEY_CHECKPOINTER, @@ -2094,7 +2095,7 @@ class Pregel(PregelProtocol): output_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, - checkpoint_during: bool = True, + checkpoint_during: Optional[bool] = None, debug: Optional[bool] = None, subgraphs: bool = False, ) -> Iterator[Union[dict[str, Any], Any]]: @@ -2278,6 +2279,9 @@ class Pregel(PregelProtocol): config[CONF][CONFIG_KEY_STREAM_WRITER] = lambda c: stream.put( ((), "custom", c) ) + # set checkpointing mode for subgraphs + if checkpoint_during is not None: + config[CONF][CONFIG_KEY_CHECKPOINT_DURING] = checkpoint_during with SyncPregelLoop( input, input_model=self.input_model, @@ -2293,7 +2297,9 @@ class Pregel(PregelProtocol): interrupt_after=interrupt_after_, manager=run_manager, debug=debug, - checkpoint_during=checkpoint_during, + checkpoint_during=checkpoint_during + if checkpoint_during is not None + else config[CONF].get(CONFIG_KEY_CHECKPOINT_DURING, True), trigger_to_nodes=self.trigger_to_nodes, migrate_checkpoint=self._migrate_checkpoint, ) as loop: @@ -2376,7 +2382,7 @@ class Pregel(PregelProtocol): output_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, - checkpoint_during: bool = True, + checkpoint_during: Optional[bool] = None, debug: Optional[bool] = None, subgraphs: bool = False, ) -> AsyncIterator[Union[dict[str, Any], Any]]: @@ -2576,6 +2582,9 @@ class Pregel(PregelProtocol): stream.put_nowait, ((), "custom", c) ) ) + # set checkpointing mode for subgraphs + if checkpoint_during is not None: + config[CONF][CONFIG_KEY_CHECKPOINT_DURING] = checkpoint_during async with AsyncPregelLoop( input, input_model=self.input_model, @@ -2591,7 +2600,9 @@ class Pregel(PregelProtocol): interrupt_after=interrupt_after_, manager=run_manager, debug=debug, - checkpoint_during=checkpoint_during, + checkpoint_during=checkpoint_during + if checkpoint_during is not None + else config[CONF].get(CONFIG_KEY_CHECKPOINT_DURING, True), trigger_to_nodes=self.trigger_to_nodes, migrate_checkpoint=self._migrate_checkpoint, ) as loop: @@ -2667,7 +2678,7 @@ class Pregel(PregelProtocol): output_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, - checkpoint_during: bool = True, + checkpoint_during: Optional[bool] = None, debug: Optional[bool] = None, **kwargs: Any, ) -> Union[dict[str, Any], Any]: @@ -2721,7 +2732,7 @@ class Pregel(PregelProtocol): output_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, - checkpoint_during: bool = True, + checkpoint_during: Optional[bool] = None, debug: Optional[bool] = None, **kwargs: Any, ) -> Union[dict[str, Any], Any]: From 5a0228cb13b88d91a7f94d7273e28d48a45a8dcf Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 4 Apr 2025 16:00:28 -0700 Subject: [PATCH 08/32] Add test --- libs/langgraph/tests/test_pregel.py | 14 +++++++++++--- libs/langgraph/tests/test_pregel_async.py | 21 ++++++++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index e03ceeb72..97405e41c 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -1510,8 +1510,14 @@ def test_send_sequences() -> None: ] +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) -def test_imp_task(request: pytest.FixtureRequest, checkpointer_name: str) -> None: +def test_imp_task( + request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool +) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Checkpointing during execution not supported") + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") mapper_calls = 0 @@ -1577,7 +1583,7 @@ def test_imp_task(request: pytest.FixtureRequest, checkpointer_name: str) -> Non } thread1 = {"configurable": {"thread_id": "1"}} - assert [*graph.stream([0, 1], thread1)] == [ + assert [*graph.stream([0, 1], thread1, checkpoint_during=checkpoint_during)] == [ {"mapper": "00"}, {"mapper": "11"}, { @@ -1593,7 +1599,9 @@ def test_imp_task(request: pytest.FixtureRequest, checkpointer_name: str) -> Non ] assert mapper_calls == 2 - assert graph.invoke(Command(resume="answer"), thread1) == [ + assert graph.invoke( + Command(resume="answer"), thread1, checkpoint_during=checkpoint_during + ) == [ "00answer", "11answer", ] diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 9fbf042fc..467e97f53 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -2230,7 +2230,7 @@ async def test_pending_writes_resume( @pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC) async def test_run_from_checkpoint_id_retains_previous_writes( - request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture + checkpointer_name: str, ) -> None: class MyState(TypedDict): myval: Annotated[int, operator.add] @@ -2275,8 +2275,8 @@ async def test_run_from_checkpoint_id_retains_previous_writes( history = [c async for c in graph.aget_state_history(thread1)] assert len(history) == 4 - assert history[-1].values == {"myval": 0} assert history[0].values == {"myval": 4, "otherval": False} + assert history[-1].values == {"myval": 0} second_run_config = { **thread1, @@ -2453,8 +2453,12 @@ async def test_send_sequences(checkpointer_name: str) -> None: @NEEDS_CONTEXTVARS +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_imp_task(checkpointer_name: str) -> None: +async def test_imp_task(checkpointer_name: str, checkpoint_during: bool) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Checkpointing during execution not supported") + async with awith_checkpointer(checkpointer_name) as checkpointer: mapper_calls = 0 @@ -2474,7 +2478,12 @@ async def test_imp_task(checkpointer_name: str) -> None: tracer = FakeTracer() thread1 = {"configurable": {"thread_id": "1"}, "callbacks": [tracer]} - assert [c async for c in graph.astream([0, 1], thread1)] == [ + assert [ + c + async for c in graph.astream( + [0, 1], thread1, checkpoint_during=checkpoint_during + ) + ] == [ {"mapper": "00"}, {"mapper": "11"}, { @@ -2498,7 +2507,9 @@ async def test_imp_task(checkpointer_name: str) -> None: assert any(r.inputs == {"input": 0} for r in mapper_runs) assert any(r.inputs == {"input": 1} for r in mapper_runs) - assert await graph.ainvoke(Command(resume="answer"), thread1) == [ + assert await graph.ainvoke( + Command(resume="answer"), thread1, checkpoint_during=checkpoint_during + ) == [ "00answer", "11answer", ] From c757247858938894e73fc2272edc0fb7f865585e Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Mon, 7 Apr 2025 20:29:14 +0200 Subject: [PATCH 09/32] feat(sdk-js): add auth types --- libs/sdk-js/langchain.config.js | 1 + libs/sdk-js/src/auth/error.ts | 74 +++++++++ libs/sdk-js/src/auth/index.ts | 267 ++++++++++++++++++++++++++++++++ libs/sdk-js/tsconfig.json | 24 +-- 4 files changed, 347 insertions(+), 19 deletions(-) create mode 100644 libs/sdk-js/src/auth/error.ts create mode 100644 libs/sdk-js/src/auth/index.ts diff --git a/libs/sdk-js/langchain.config.js b/libs/sdk-js/langchain.config.js index 21d796c87..0c8a691cd 100644 --- a/libs/sdk-js/langchain.config.js +++ b/libs/sdk-js/langchain.config.js @@ -14,6 +14,7 @@ export const config = { entrypoints: { index: "index", client: "client", + auth: "auth/index", react: "react/index", "react-ui": "react-ui/index", "react-ui/server": "react-ui/server/index", diff --git a/libs/sdk-js/src/auth/error.ts b/libs/sdk-js/src/auth/error.ts new file mode 100644 index 000000000..bd26c8912 --- /dev/null +++ b/libs/sdk-js/src/auth/error.ts @@ -0,0 +1,74 @@ +const HTTP_STATUS_MAPPING: { [key: number]: string } = { + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 103: "Early Hints", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a Teapot", + 421: "Misdirected Request", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 425: "Too Early", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 451: "Unavailable For Legal Reasons", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 510: "Not Extended", + 511: "Network Authentication Required", +}; + +export class HTTPException extends Error { + status: number; + constructor(status: number, options?: { message?: string; cause?: Error }) { + super(options?.message ?? HTTP_STATUS_MAPPING[status] ?? "Unknown error", { + cause: options?.cause, + }); + this.status = status; + } +} diff --git a/libs/sdk-js/src/auth/index.ts b/libs/sdk-js/src/auth/index.ts new file mode 100644 index 000000000..900a3adbb --- /dev/null +++ b/libs/sdk-js/src/auth/index.ts @@ -0,0 +1,267 @@ +type Maybe = T | null | undefined; + +interface AssistantConfig { + tags?: Maybe; + recursion_limit?: Maybe; + configurable?: Maybe<{ + thread_id?: Maybe; + thread_ts?: Maybe; + [key: string]: unknown; + }>; + [key: string]: unknown; +} + +interface AssistantCreate { + assistant_id?: Maybe; + metadata?: Maybe>; + config?: Maybe; + if_exists?: Maybe<"raise" | "do_nothing">; + name?: Maybe; + graph_id: string; +} + +interface AssistantRead { + assistant_id: string; + metadata?: Maybe>; +} + +interface AssistantUpdate { + assistant_id: string; + metadata?: Maybe>; + config?: Maybe; + graph_id?: Maybe; + name?: Maybe; + version?: Maybe; +} + +interface AssistantDelete { + assistant_id: string; +} + +interface AssistantSearch { + graph_id?: Maybe; + metadata?: Maybe>; + limit?: Maybe; + offset?: Maybe; +} + +// TODO: add missing types +interface ThreadCreate {} +interface ThreadRead {} +interface ThreadUpdate {} +interface ThreadDelete {} +interface ThreadSearch {} + +interface CronCreate {} +interface CronRead {} +interface CronUpdate {} +interface CronDelete {} +interface CronSearch {} + +interface StorePut {} +interface StoreGet {} +interface StoreSearch {} +interface StoreListNamespaces {} +interface StoreDelete {} + +interface RunsCreate {} + +interface ResourceActionType { + ["threads:create"]: ThreadCreate; + ["threads:read"]: ThreadRead; + ["threads:update"]: ThreadUpdate; + ["threads:delete"]: ThreadDelete; + ["threads:search"]: ThreadSearch; + ["threads:create_run"]: RunsCreate; + + ["assistants:create"]: AssistantCreate; + ["assistants:read"]: AssistantRead; + ["assistants:update"]: AssistantUpdate; + ["assistants:delete"]: AssistantDelete; + ["assistants:search"]: AssistantSearch; + + ["crons:create"]: CronCreate; + ["crons:read"]: CronRead; + ["crons:update"]: CronUpdate; + ["crons:delete"]: CronDelete; + ["crons:search"]: CronSearch; + + ["store:put"]: StorePut; + ["store:get"]: StoreGet; + ["store:search"]: StoreSearch; + ["store:list_namespaces"]: StoreListNamespaces; + ["store:delete"]: StoreDelete; +} + +interface ResourceType { + threads: + | "threads:create" + | "threads:read" + | "threads:update" + | "threads:delete" + | "threads:search" + | "threads:create_run"; + + assistants: + | "assistants:create" + | "assistants:read" + | "assistants:update" + | "assistants:delete" + | "assistants:search"; + crons: + | "crons:create" + | "crons:read" + | "crons:update" + | "crons:delete" + | "crons:search"; + + store: + | "store:put" + | "store:get" + | "store:search" + | "store:list_namespaces" + | "store:delete"; +} + +interface ActionType { + "*:create": "threads:create" | "assistants:create" | "crons:create"; + + "*:read": "threads:read" | "assistants:read" | "crons:read"; + + "*:update": "threads:update" | "assistants:update" | "crons:update"; + + "*:delete": + | "threads:delete" + | "assistants:delete" + | "crons:delete" + | "store:delete"; + + "*:search": + | "threads:search" + | "assistants:search" + | "crons:search" + | "store:search"; + + "*:create_run": "threads:create_run"; + + "*:put": "store:put"; + + "*:get": "store:get"; + + "*:list_namespaces": "store:list_namespaces"; +} + +interface BaseAuthContext { + permissions?: string[]; + user?: { + is_authenticated: boolean; + display_name: string; + identity: string; + permissions: string[]; + }; +} + +type ContextMap = { + [ActionType in keyof ResourceActionType]: { + resource: ActionType extends `${infer Resource}:${string}` + ? Resource + : never; + action: ActionType; + data: ResourceActionType[ActionType]; + context: BaseAuthContext; + }; +}; + +type ActionCallbackParameter< + T extends keyof ActionType, + AuthContext = {}, +> = ContextMap[ActionType[T]] & { context: AuthContext }; + +type AuthCallbackParameter< + T extends keyof ResourceActionType, + AuthContext = {}, +> = ContextMap[T] & { context: AuthContext }; + +type ResourceCallbackParameter< + T extends keyof ResourceType, + AuthContext = {}, +> = ContextMap[ResourceType[T]] & { context: AuthContext }; + +type Filters = { + [key in TKey]: string | { [op in "$contains" | "$eq"]?: string }; +}; + +interface AuthenticateCallback> { + (request: Request): AuthContext; +} + +export class Auth< + Metadata extends Record = {}, + AuthContext extends Record = {}, +> { + protected __lg_type = Symbol.for("lg:auth"); + + "~handlerCache": { + authenticate?: AuthenticateCallback; + callbacks?: Record< + string, + (request: any) => void | boolean | Filters + >; + } = {}; + + authenticate(cb: AuthenticateCallback): this { + this["~handlerCache"].authenticate = cb; + return this; + } + + /** + * Global handler for all requests + */ + on( + event: "*", + callback: ( + data: AuthCallbackParameter, + ) => void | boolean | Filters, + ): this; + + /** + * Resource-specific handler + */ + on( + event: T, + callback: ( + data: ResourceCallbackParameter, + ) => void | boolean | Filters, + ): this; + + /** + * Action-specific handler + */ + on( + event: T, + callback: ( + data: ActionCallbackParameter, + ) => void | boolean | Filters, + ): this; + + /** + * Resource-action specific handler + */ + on( + event: T, + callback: ( + data: AuthCallbackParameter, + ) => void | boolean | Filters, + ): this; + + on( + event: string, + callback: ( + data: AuthCallbackParameter, + ) => void | boolean | Filters, + ): this { + this["~handlerCache"].callbacks ??= {}; + this["~handlerCache"].callbacks[event] = callback; + return this; + } +} diff --git a/libs/sdk-js/tsconfig.json b/libs/sdk-js/tsconfig.json index 9c6561a09..d5c1626ab 100644 --- a/libs/sdk-js/tsconfig.json +++ b/libs/sdk-js/tsconfig.json @@ -2,11 +2,7 @@ "extends": "@tsconfig/recommended", "compilerOptions": { "target": "ES2021", - "lib": [ - "ES2021", - "ES2022.Object", - "DOM" - ], + "lib": ["ES2021", "ES2022.Object", "ES2022.Error", "DOM"], "module": "NodeNext", "moduleResolution": "nodenext", "esModuleInterop": true, @@ -22,24 +18,14 @@ "jsx": "react-jsx", "outDir": "dist" }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist", - "coverage" - ], + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "coverage"], "includeVersion": true, "typedocOptions": { - "entryPoints": [ - "src/client.ts" - ], + "entryPoints": ["src/client.ts"], "readme": "none", "out": "docs", - "plugin": [ - "typedoc-plugin-markdown" - ], + "plugin": ["typedoc-plugin-markdown"], "excludePrivate": true, "excludeProtected": true, "excludeExternals": false From d541ed90d571efeb783ba3d723efe1c294c06527 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 7 Apr 2025 16:45:26 -0700 Subject: [PATCH 10/32] Save Sends unconditionally --- libs/langgraph/langgraph/pregel/loop.py | 20 ++++- libs/langgraph/tests/test_large_cases.py | 32 ++++--- libs/langgraph/tests/test_pregel.py | 31 +++++-- libs/langgraph/tests/test_pregel_async.py | 101 +++++++++++++++++----- 4 files changed, 131 insertions(+), 53 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 86b9ba8dc..772564a50 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -63,6 +63,7 @@ from langgraph.constants import ( RESUME, SCHEDULED, TAG_HIDDEN, + TASKS, ) from langgraph.errors import ( CheckpointNotLatest, @@ -295,6 +296,7 @@ class PregelLoop(LoopProtocol): """Put writes for a task, to be read by the next tick.""" if not writes: return + checkpoint_during = self.checkpoint_during or any(w[0] == TASKS for w in writes) # deduplicate writes to special channels, last write wins if all(w[0] in WRITES_IDX_MAP for w in writes): writes = list({w[0]: w for w in writes}.values()) @@ -304,7 +306,7 @@ class PregelLoop(LoopProtocol): ] # save writes self.checkpoint_pending_writes.extend((task_id, c, v) for c, v in writes) - if self.checkpoint_during and self.checkpointer_put_writes is not None: + if checkpoint_during and self.checkpointer_put_writes is not None: config = patch_configurable( self.checkpoint_config, { @@ -342,6 +344,16 @@ class PregelLoop(LoopProtocol): return if not self.checkpoint_pending_writes: return + # patch config + config = patch_configurable( + self.checkpoint_config, + { + CONFIG_KEY_CHECKPOINT_NS: self.config[CONF].get( + CONFIG_KEY_CHECKPOINT_NS, "" + ), + CONFIG_KEY_CHECKPOINT_ID: self.checkpoint["id"], + }, + ) # group by task id by_task = defaultdict(list) for task_id, channel, value in self.checkpoint_pending_writes: @@ -354,7 +366,7 @@ class PregelLoop(LoopProtocol): task = self.tasks.get(task_id) self.submit( self.checkpointer_put_writes, - self.checkpoint_config, + config, writes, task_id, task_path_str(task.path) if task else "", @@ -362,7 +374,7 @@ class PregelLoop(LoopProtocol): else: self.submit( self.checkpointer_put_writes, - self.checkpoint_config, + config, writes, task_id, ) @@ -748,6 +760,7 @@ class PregelLoop(LoopProtocol): else self.stream_keys ), ) + self.checkpoint_id_prev = self.checkpoint["id"] if self.step > -1 else None # do checkpoint? do_checkpoint = self._checkpointer_put_after_previous is not None and ( exiting or self.checkpoint_during @@ -776,6 +789,7 @@ class PregelLoop(LoopProtocol): **self.checkpoint_config, CONF: { **self.checkpoint_config[CONF], + CONFIG_KEY_CHECKPOINT_ID: self.checkpoint_id_prev, CONFIG_KEY_CHECKPOINT_NS: self.config[CONF].get( CONFIG_KEY_CHECKPOINT_NS, "" ), diff --git a/libs/langgraph/tests/test_large_cases.py b/libs/langgraph/tests/test_large_cases.py index f15cd5c23..f5be91cee 100644 --- a/libs/langgraph/tests/test_large_cases.py +++ b/libs/langgraph/tests/test_large_cases.py @@ -7258,9 +7258,10 @@ def test_branch_then( ) -@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +@pytest.mark.parametrize("checkpoint_during", [True, False]) +@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_SYNC) def test_send_dedupe_on_resume( - request: pytest.FixtureRequest, checkpointer_name: str + request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool ) -> None: checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") @@ -7316,7 +7317,7 @@ def test_send_dedupe_on_resume( graph = builder.compile(checkpointer=checkpointer) thread1 = {"configurable": {"thread_id": "1"}} - assert graph.invoke(["0"], thread1, debug=1) == [ + assert graph.invoke(["0"], thread1, checkpoint_during=checkpoint_during) == [ "0", "1", "3.1", @@ -7333,12 +7334,11 @@ def test_send_dedupe_on_resume( pytest.xfail("TODO: shallow checkpointer reports wrong next set") assert state.next == ("flaky",) # check history - if "shallow" not in checkpointer_name: - history = [c for c in graph.get_state_history(thread1)] - assert len(history) == 4 + history = [c for c in graph.get_state_history(thread1)] + assert len(history) == (4 if checkpoint_during else 1) # resume execution - assert graph.invoke(None, thread1, debug=1) == [ + assert graph.invoke(None, thread1, checkpoint_during=checkpoint_during) == [ "0", "1", "3.1", @@ -7358,6 +7358,7 @@ def test_send_dedupe_on_resume( assert state.next == () # check history history = [c for c in graph.get_state_history(thread1)] + assert len(history) == (6 if checkpoint_during else 2) expected_history = [ StateSnapshot( values=[ @@ -7494,13 +7495,9 @@ def test_send_dedupe_on_resume( name="flaky", path=("__pregel_push", 1), error=None, - interrupts=( - Interrupt( - value="Bahh", resumable=False, ns=None, when="during" - ), - ), + interrupts=(Interrupt(value="Bahh", resumable=False, ns=None),), state=None, - result=["flaky|4"], + result=["flaky|4"] if checkpoint_during else None, ), PregelTask( id=AnyStr(), @@ -7637,10 +7634,11 @@ def test_send_dedupe_on_resume( ), ), ] - if "shallow" in checkpointer_name: - expected_history = expected_history[:1] - - assert history == expected_history + if checkpoint_during: + assert history == expected_history + else: + assert history[0] == expected_history[0] + assert history[1] == expected_history[2] @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 97405e41c..f1d45c192 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -1333,11 +1333,11 @@ def test_pending_writes_resume( "configurable": { "thread_id": "1", "checkpoint_ns": "", - "checkpoint_id": checkpoints[2].config["configurable"]["checkpoint_id"], + "checkpoint_id": checkpoints[2].config["configurable"]["checkpoint_id"] + if checkpoint_during + else AnyStr(), } - } - if checkpoint_during - else None, + }, pending_writes=UnsortedSequence( (AnyStr(), "value", 2), (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), @@ -1608,10 +1608,14 @@ def test_imp_task( assert mapper_calls == 2 +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_imp_nested( - request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion + request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool ) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Checkpointing during execution not supported") + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") def mynode(input: list[str]) -> list[str]: @@ -1653,7 +1657,7 @@ def test_imp_nested( } thread1 = {"configurable": {"thread_id": "1"}} - assert [*graph.stream([0, 1], thread1)] == [ + assert [*graph.stream([0, 1], thread1, checkpoint_during=checkpoint_during)] == [ {"submapper": "0"}, {"mapper": "00"}, {"submapper": "1"}, @@ -1670,16 +1674,22 @@ def test_imp_nested( }, ] - assert graph.invoke(Command(resume="answer"), thread1) == [ + assert graph.invoke( + Command(resume="answer"), thread1, checkpoint_during=checkpoint_during + ) == [ "00answera", "11answera", ] +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_imp_stream_order( - request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion + request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool ) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Checkpointing during execution not supported") + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") @task() @@ -1702,7 +1712,10 @@ def test_imp_stream_order( return fut_baz.result() thread1 = {"configurable": {"thread_id": "1"}} - assert [c for c in graph.stream({"a": "0"}, thread1)] == [ + assert [ + c + for c in graph.stream({"a": "0"}, thread1, checkpoint_during=checkpoint_during) + ] == [ { "foo": ( "0foo", diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 467e97f53..4cc115f6f 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -2173,11 +2173,11 @@ async def test_pending_writes_resume( "checkpoint_ns": "", "checkpoint_id": checkpoints[2].config["configurable"][ "checkpoint_id" - ], + ] + if checkpoint_during + else AnyStr(), } - } - if checkpoint_during - else None, + }, pending_writes=UnsortedSequence( (AnyStr(), "value", 2), (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), @@ -2517,8 +2517,12 @@ async def test_imp_task(checkpointer_name: str, checkpoint_during: bool) -> None @NEEDS_CONTEXTVARS +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_imp_nested(checkpointer_name: str) -> None: +async def test_imp_nested(checkpointer_name: str, checkpoint_during: bool) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Checkpointing during execution not supported") + async def mynode(input: list[str]) -> list[str]: return [it + "a" for it in input] @@ -2558,7 +2562,12 @@ async def test_imp_nested(checkpointer_name: str) -> None: } thread1 = {"configurable": {"thread_id": "1"}} - assert [c async for c in graph.astream([0, 1], thread1)] == [ + assert [ + c + async for c in graph.astream( + [0, 1], thread1, checkpoint_during=checkpoint_during + ) + ] == [ {"submapper": "0"}, {"mapper": "00"}, {"submapper": "1"}, @@ -2575,15 +2584,21 @@ async def test_imp_nested(checkpointer_name: str) -> None: }, ] - assert await graph.ainvoke(Command(resume="answer"), thread1) == [ + assert await graph.ainvoke( + Command(resume="answer"), thread1, checkpoint_during=checkpoint_during + ) == [ "00answera", "11answera", ] @NEEDS_CONTEXTVARS +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_imp_task_cancel(checkpointer_name: str) -> None: +async def test_imp_task_cancel(checkpointer_name: str, checkpoint_during: bool) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Checkpointing during execution not supported") + async with awith_checkpointer(checkpointer_name) as checkpointer: mapper_calls = 0 mapper_cancels = 0 @@ -2609,7 +2624,12 @@ async def test_imp_task_cancel(checkpointer_name: str) -> None: return [m + answer for m in mapped] thread1 = {"configurable": {"thread_id": "1"}} - assert [c async for c in graph.astream([0, 1], thread1)] == [ + assert [ + c + async for c in graph.astream( + [0, 1], thread1, checkpoint_during=checkpoint_during + ) + ] == [ {"mapper": "00"}, { "__interrupt__": ( @@ -2625,7 +2645,9 @@ async def test_imp_task_cancel(checkpointer_name: str) -> None: assert mapper_calls == 2 assert mapper_cancels == 1 - assert await graph.ainvoke(Command(resume="answer"), thread1) == [ + assert await graph.ainvoke( + Command(resume="answer"), thread1, checkpoint_during=checkpoint_during + ) == [ "00answer", ] assert mapper_calls == 3 @@ -2633,8 +2655,14 @@ async def test_imp_task_cancel(checkpointer_name: str) -> None: @NEEDS_CONTEXTVARS +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_imp_sync_from_async(checkpointer_name: str) -> None: +async def test_imp_sync_from_async( + checkpointer_name: str, checkpoint_during: bool +) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Checkpointing during execution not supported") + async with awith_checkpointer(checkpointer_name) as checkpointer: @task() @@ -2657,7 +2685,12 @@ async def test_imp_sync_from_async(checkpointer_name: str) -> None: return fut_baz.result() thread1 = {"configurable": {"thread_id": "1"}} - assert [c async for c in graph.astream({"a": "0"}, thread1)] == [ + assert [ + c + async for c in graph.astream( + {"a": "0"}, thread1, checkpoint_during=checkpoint_during + ) + ] == [ {"foo": {"a": "0foo", "b": "bar"}}, {"bar": {"a": "0foobar", "c": "bark"}}, {"baz": {"a": "0foobarbaz", "c": "something else"}}, @@ -2666,8 +2699,14 @@ async def test_imp_sync_from_async(checkpointer_name: str) -> None: @NEEDS_CONTEXTVARS +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_imp_stream_order(checkpointer_name: str) -> None: +async def test_imp_stream_order( + checkpointer_name: str, checkpoint_during: bool +) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Checkpointing during execution not supported") + async with awith_checkpointer(checkpointer_name) as checkpointer: @task() @@ -2691,7 +2730,12 @@ async def test_imp_stream_order(checkpointer_name: str) -> None: return await fut_baz thread1 = {"configurable": {"thread_id": "1"}} - assert [c async for c in graph.astream({"a": "0"}, thread1)] == [ + assert [ + c + async for c in graph.astream( + {"a": "0"}, thread1, checkpoint_during=checkpoint_during + ) + ] == [ {"foo": {"a": "0foo", "b": "bar"}}, {"bar": {"a": "0foobar", "c": "bark"}}, {"baz": {"a": "0foobarbaz", "c": "something else"}}, @@ -2699,8 +2743,11 @@ async def test_imp_stream_order(checkpointer_name: str) -> None: ] +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC) -async def test_send_dedupe_on_resume(checkpointer_name: str) -> None: +async def test_send_dedupe_on_resume( + checkpointer_name: str, checkpoint_during: bool +) -> None: class InterruptOnce: ticks: int = 0 @@ -2751,7 +2798,9 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None: async with awith_checkpointer(checkpointer_name) as checkpointer: graph = builder.compile(checkpointer=checkpointer) thread1 = {"configurable": {"thread_id": "1"}} - assert await graph.ainvoke(["0"], thread1, debug=1) == [ + assert await graph.ainvoke( + ["0"], thread1, checkpoint_during=checkpoint_during + ) == [ "0", "1", "3.1", @@ -2763,7 +2812,9 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None: assert builder.nodes["2"].runnable.func.ticks == 3 assert builder.nodes["flaky"].runnable.func.ticks == 1 # resume execution - assert await graph.ainvoke(None, thread1, debug=1) == [ + assert await graph.ainvoke( + None, thread1, checkpoint_during=checkpoint_during + ) == [ "0", "1", "3.1", @@ -2780,7 +2831,8 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None: assert builder.nodes["flaky"].runnable.func.ticks == 2 # check history history = [c async for c in graph.aget_state_history(thread1)] - assert history == [ + assert len(history) == (6 if checkpoint_during else 2) + expected_history = [ StateSnapshot( values=[ "0", @@ -2916,13 +2968,9 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None: name="flaky", path=("__pregel_push", 1), error=None, - interrupts=( - Interrupt( - value="Bahh", resumable=False, ns=None, when="during" - ), - ), + interrupts=(Interrupt(value="Bahh", resumable=False, ns=None),), state=None, - result=["flaky|4"], + result=["flaky|4"] if checkpoint_during else None, ), PregelTask( id=AnyStr(), @@ -3059,6 +3107,11 @@ async def test_send_dedupe_on_resume(checkpointer_name: str) -> None: ), ), ] + if checkpoint_during: + assert history == expected_history + else: + assert history[0] == expected_history[0] + assert history[1] == expected_history[2] @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) From aee39605e096a2d19f76a3fa3e618f3da5a3061a Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Tue, 8 Apr 2025 14:13:59 +0200 Subject: [PATCH 11/32] Add missing types --- libs/sdk-js/.gitignore | 4 + libs/sdk-js/package.json | 13 +++ libs/sdk-js/src/auth/index.ts | 187 ++++++++++++++++++++++++++-------- 3 files changed, 163 insertions(+), 41 deletions(-) diff --git a/libs/sdk-js/.gitignore b/libs/sdk-js/.gitignore index 128c6dc98..db1a582d0 100644 --- a/libs/sdk-js/.gitignore +++ b/libs/sdk-js/.gitignore @@ -6,6 +6,10 @@ client.cjs client.js client.d.ts client.d.cts +auth.cjs +auth.js +auth.d.ts +auth.d.cts react.cjs react.js react.d.ts diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json index 3be321092..2ff1c8282 100644 --- a/libs/sdk-js/package.json +++ b/libs/sdk-js/package.json @@ -72,6 +72,15 @@ "import": "./client.js", "require": "./client.cjs" }, + "./auth": { + "types": { + "import": "./auth.d.ts", + "require": "./auth.d.cts", + "default": "./auth.d.ts" + }, + "import": "./auth.js", + "require": "./auth.cjs" + }, "./react": { "types": { "import": "./react.d.ts", @@ -111,6 +120,10 @@ "client.js", "client.d.ts", "client.d.cts", + "auth.cjs", + "auth.js", + "auth.d.ts", + "auth.d.cts", "react.cjs", "react.js", "react.d.ts", diff --git a/libs/sdk-js/src/auth/index.ts b/libs/sdk-js/src/auth/index.ts index 900a3adbb..04a84d06b 100644 --- a/libs/sdk-js/src/auth/index.ts +++ b/libs/sdk-js/src/auth/index.ts @@ -5,10 +5,9 @@ interface AssistantConfig { recursion_limit?: Maybe; configurable?: Maybe<{ thread_id?: Maybe; - thread_ts?: Maybe; + thread_ts?: Maybe; [key: string]: unknown; }>; - [key: string]: unknown; } interface AssistantCreate { @@ -24,7 +23,6 @@ interface AssistantRead { assistant_id: string; metadata?: Maybe>; } - interface AssistantUpdate { assistant_id: string; metadata?: Maybe>; @@ -33,11 +31,9 @@ interface AssistantUpdate { name?: Maybe; version?: Maybe; } - interface AssistantDelete { assistant_id: string; } - interface AssistantSearch { graph_id?: Maybe; metadata?: Maybe>; @@ -45,28 +41,114 @@ interface AssistantSearch { offset?: Maybe; } -// TODO: add missing types -interface ThreadCreate {} -interface ThreadRead {} -interface ThreadUpdate {} -interface ThreadDelete {} -interface ThreadSearch {} +interface ThreadCreate { + thread_id?: Maybe; + metadata?: Maybe>; + if_exists?: Maybe<"raise" | "do_nothing">; +} -interface CronCreate {} -interface CronRead {} -interface CronUpdate {} -interface CronDelete {} -interface CronSearch {} +interface ThreadRead { + thread_id?: Maybe; +} -interface StorePut {} -interface StoreGet {} -interface StoreSearch {} -interface StoreListNamespaces {} -interface StoreDelete {} +interface ThreadUpdate { + thread_id?: Maybe; + metadata?: Maybe>; + action?: Maybe<"interrupt" | "rollback">; +} -interface RunsCreate {} +interface ThreadDelete { + thread_id?: Maybe; + run_id?: Maybe; +} -interface ResourceActionType { +interface ThreadSearch { + thread_id?: Maybe; + status?: Maybe<"idle" | "busy" | "interrupted" | "error" | (string & {})>; + metadata?: Maybe>; + values?: Maybe>; + limit?: Maybe; + offset?: Maybe; +} + +interface CronCreate { + payload?: Maybe>; + schedule: string; + cron_id?: Maybe; + thread_id?: Maybe; + user_id?: Maybe; + end_time?: Maybe; +} + +interface CronRead { + cron_id: string; +} + +interface CronUpdate { + cron_id: string; + payload?: Maybe>; + schedule?: Maybe; +} + +interface CronDelete { + cron_id: string; +} + +interface CronSearch { + assistant_id?: Maybe; + thread_id?: Maybe; + limit?: Maybe; + offset?: Maybe; +} + +interface StorePut { + namespace: string[]; + key: string; + value: Record; +} + +interface StoreGet { + namespace: Maybe; + key: string; +} + +interface StoreSearch { + namespace?: Maybe; + filter?: Maybe>; + limit?: Maybe; + offset?: Maybe; + query?: Maybe; +} + +interface StoreListNamespaces { + namespace?: Maybe; + suffix?: Maybe; + max_depth?: Maybe; + limit?: Maybe; + offset?: Maybe; +} + +interface StoreDelete { + namespace?: Maybe; + key: string; +} + +interface RunsCreate { + thread_id?: Maybe; + assistant_id: string; + run_id: string; + status: Maybe< + "pending" | "running" | "error" | "success" | "timeout" | "interrupted" + >; + metadata?: Maybe>; + prevent_insert_if_inflight?: Maybe; + multitask_strategy?: Maybe<"interrupt" | "rollback" | "reject" | "enqueue">; + if_not_exists?: Maybe<"reject" | "create">; + after_seconds?: Maybe; + kwargs: Record; +} + +export interface ResourceActionType { ["threads:create"]: ThreadCreate; ["threads:read"]: ThreadRead; ["threads:update"]: ThreadUpdate; @@ -187,7 +269,7 @@ type ResourceCallbackParameter< AuthContext = {}, > = ContextMap[ResourceType[T]] & { context: AuthContext }; -type Filters = { +export type Filters = { [key in TKey]: string | { [op in "$contains" | "$eq"]?: string }; }; @@ -195,12 +277,46 @@ interface AuthenticateCallback> { (request: Request): AuthContext; } +interface GlobalCallback< + Metadata extends Record = {}, + AuthContext extends Record = {}, +> { + ( + data: AuthCallbackParameter, + ): void | boolean | Filters; +} + +interface ResourceCallback< + Metadata extends Record = {}, + AuthContext extends Record = {}, +> { + ( + data: ResourceCallbackParameter, + ): void | boolean | Filters; +} + +interface ActionCallback< + Metadata extends Record = {}, + AuthContext extends Record = {}, +> { + ( + data: ActionCallbackParameter, + ): void | boolean | Filters; +} + +interface ResourceActionCallback< + Metadata extends Record = {}, + AuthContext extends Record = {}, +> { + ( + data: AuthCallbackParameter, + ): void | boolean | Filters; +} + export class Auth< Metadata extends Record = {}, AuthContext extends Record = {}, > { - protected __lg_type = Symbol.for("lg:auth"); - "~handlerCache": { authenticate?: AuthenticateCallback; callbacks?: Record< @@ -217,21 +333,14 @@ export class Auth< /** * Global handler for all requests */ - on( - event: "*", - callback: ( - data: AuthCallbackParameter, - ) => void | boolean | Filters, - ): this; + on(event: "*", callback: GlobalCallback): this; /** * Resource-specific handler */ on( event: T, - callback: ( - data: ResourceCallbackParameter, - ) => void | boolean | Filters, + callback: ResourceCallback, ): this; /** @@ -239,9 +348,7 @@ export class Auth< */ on( event: T, - callback: ( - data: ActionCallbackParameter, - ) => void | boolean | Filters, + callback: ActionCallback, ): this; /** @@ -249,9 +356,7 @@ export class Auth< */ on( event: T, - callback: ( - data: AuthCallbackParameter, - ) => void | boolean | Filters, + callback: ResourceActionCallback, ): this; on( From a98f9542fa3d2cb864017cfa5c5e82c885f2bd44 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Tue, 8 Apr 2025 17:06:51 +0200 Subject: [PATCH 12/32] Add unused extra generic for future typing of metadata --- libs/sdk-js/src/auth/error.ts | 8 +- libs/sdk-js/src/auth/index.ts | 384 +++------------------------------- libs/sdk-js/src/auth/types.ts | 345 ++++++++++++++++++++++++++++++ 3 files changed, 377 insertions(+), 360 deletions(-) create mode 100644 libs/sdk-js/src/auth/types.ts diff --git a/libs/sdk-js/src/auth/error.ts b/libs/sdk-js/src/auth/error.ts index bd26c8912..ace6df54c 100644 --- a/libs/sdk-js/src/auth/error.ts +++ b/libs/sdk-js/src/auth/error.ts @@ -65,10 +65,16 @@ const HTTP_STATUS_MAPPING: { [key: number]: string } = { export class HTTPException extends Error { status: number; - constructor(status: number, options?: { message?: string; cause?: Error }) { + headers: HeadersInit; + + constructor( + status: number, + options?: { message?: string; headers?: HeadersInit; cause?: unknown }, + ) { super(options?.message ?? HTTP_STATUS_MAPPING[status] ?? "Unknown error", { cause: options?.cause, }); this.status = status; + this.headers = options?.headers ?? {}; } } diff --git a/libs/sdk-js/src/auth/index.ts b/libs/sdk-js/src/auth/index.ts index 04a84d06b..726e638d4 100644 --- a/libs/sdk-js/src/auth/index.ts +++ b/libs/sdk-js/src/auth/index.ts @@ -1,372 +1,38 @@ -type Maybe = T | null | undefined; - -interface AssistantConfig { - tags?: Maybe; - recursion_limit?: Maybe; - configurable?: Maybe<{ - thread_id?: Maybe; - thread_ts?: Maybe; - [key: string]: unknown; - }>; -} - -interface AssistantCreate { - assistant_id?: Maybe; - metadata?: Maybe>; - config?: Maybe; - if_exists?: Maybe<"raise" | "do_nothing">; - name?: Maybe; - graph_id: string; -} - -interface AssistantRead { - assistant_id: string; - metadata?: Maybe>; -} -interface AssistantUpdate { - assistant_id: string; - metadata?: Maybe>; - config?: Maybe; - graph_id?: Maybe; - name?: Maybe; - version?: Maybe; -} -interface AssistantDelete { - assistant_id: string; -} -interface AssistantSearch { - graph_id?: Maybe; - metadata?: Maybe>; - limit?: Maybe; - offset?: Maybe; -} - -interface ThreadCreate { - thread_id?: Maybe; - metadata?: Maybe>; - if_exists?: Maybe<"raise" | "do_nothing">; -} - -interface ThreadRead { - thread_id?: Maybe; -} - -interface ThreadUpdate { - thread_id?: Maybe; - metadata?: Maybe>; - action?: Maybe<"interrupt" | "rollback">; -} - -interface ThreadDelete { - thread_id?: Maybe; - run_id?: Maybe; -} - -interface ThreadSearch { - thread_id?: Maybe; - status?: Maybe<"idle" | "busy" | "interrupted" | "error" | (string & {})>; - metadata?: Maybe>; - values?: Maybe>; - limit?: Maybe; - offset?: Maybe; -} - -interface CronCreate { - payload?: Maybe>; - schedule: string; - cron_id?: Maybe; - thread_id?: Maybe; - user_id?: Maybe; - end_time?: Maybe; -} - -interface CronRead { - cron_id: string; -} - -interface CronUpdate { - cron_id: string; - payload?: Maybe>; - schedule?: Maybe; -} - -interface CronDelete { - cron_id: string; -} - -interface CronSearch { - assistant_id?: Maybe; - thread_id?: Maybe; - limit?: Maybe; - offset?: Maybe; -} - -interface StorePut { - namespace: string[]; - key: string; - value: Record; -} - -interface StoreGet { - namespace: Maybe; - key: string; -} - -interface StoreSearch { - namespace?: Maybe; - filter?: Maybe>; - limit?: Maybe; - offset?: Maybe; - query?: Maybe; -} - -interface StoreListNamespaces { - namespace?: Maybe; - suffix?: Maybe; - max_depth?: Maybe; - limit?: Maybe; - offset?: Maybe; -} - -interface StoreDelete { - namespace?: Maybe; - key: string; -} - -interface RunsCreate { - thread_id?: Maybe; - assistant_id: string; - run_id: string; - status: Maybe< - "pending" | "running" | "error" | "success" | "timeout" | "interrupted" - >; - metadata?: Maybe>; - prevent_insert_if_inflight?: Maybe; - multitask_strategy?: Maybe<"interrupt" | "rollback" | "reject" | "enqueue">; - if_not_exists?: Maybe<"reject" | "create">; - after_seconds?: Maybe; - kwargs: Record; -} - -export interface ResourceActionType { - ["threads:create"]: ThreadCreate; - ["threads:read"]: ThreadRead; - ["threads:update"]: ThreadUpdate; - ["threads:delete"]: ThreadDelete; - ["threads:search"]: ThreadSearch; - ["threads:create_run"]: RunsCreate; - - ["assistants:create"]: AssistantCreate; - ["assistants:read"]: AssistantRead; - ["assistants:update"]: AssistantUpdate; - ["assistants:delete"]: AssistantDelete; - ["assistants:search"]: AssistantSearch; - - ["crons:create"]: CronCreate; - ["crons:read"]: CronRead; - ["crons:update"]: CronUpdate; - ["crons:delete"]: CronDelete; - ["crons:search"]: CronSearch; - - ["store:put"]: StorePut; - ["store:get"]: StoreGet; - ["store:search"]: StoreSearch; - ["store:list_namespaces"]: StoreListNamespaces; - ["store:delete"]: StoreDelete; -} - -interface ResourceType { - threads: - | "threads:create" - | "threads:read" - | "threads:update" - | "threads:delete" - | "threads:search" - | "threads:create_run"; - - assistants: - | "assistants:create" - | "assistants:read" - | "assistants:update" - | "assistants:delete" - | "assistants:search"; - crons: - | "crons:create" - | "crons:read" - | "crons:update" - | "crons:delete" - | "crons:search"; - - store: - | "store:put" - | "store:get" - | "store:search" - | "store:list_namespaces" - | "store:delete"; -} - -interface ActionType { - "*:create": "threads:create" | "assistants:create" | "crons:create"; - - "*:read": "threads:read" | "assistants:read" | "crons:read"; - - "*:update": "threads:update" | "assistants:update" | "crons:update"; - - "*:delete": - | "threads:delete" - | "assistants:delete" - | "crons:delete" - | "store:delete"; - - "*:search": - | "threads:search" - | "assistants:search" - | "crons:search" - | "store:search"; - - "*:create_run": "threads:create_run"; - - "*:put": "store:put"; - - "*:get": "store:get"; - - "*:list_namespaces": "store:list_namespaces"; -} - -interface BaseAuthContext { - permissions?: string[]; - user?: { - is_authenticated: boolean; - display_name: string; - identity: string; - permissions: string[]; - }; -} - -type ContextMap = { - [ActionType in keyof ResourceActionType]: { - resource: ActionType extends `${infer Resource}:${string}` - ? Resource - : never; - action: ActionType; - data: ResourceActionType[ActionType]; - context: BaseAuthContext; - }; -}; - -type ActionCallbackParameter< - T extends keyof ActionType, - AuthContext = {}, -> = ContextMap[ActionType[T]] & { context: AuthContext }; - -type AuthCallbackParameter< - T extends keyof ResourceActionType, - AuthContext = {}, -> = ContextMap[T] & { context: AuthContext }; - -type ResourceCallbackParameter< - T extends keyof ResourceType, - AuthContext = {}, -> = ContextMap[ResourceType[T]] & { context: AuthContext }; - -export type Filters = { - [key in TKey]: string | { [op in "$contains" | "$eq"]?: string }; -}; - -interface AuthenticateCallback> { - (request: Request): AuthContext; -} - -interface GlobalCallback< - Metadata extends Record = {}, - AuthContext extends Record = {}, -> { - ( - data: AuthCallbackParameter, - ): void | boolean | Filters; -} - -interface ResourceCallback< - Metadata extends Record = {}, - AuthContext extends Record = {}, -> { - ( - data: ResourceCallbackParameter, - ): void | boolean | Filters; -} - -interface ActionCallback< - Metadata extends Record = {}, - AuthContext extends Record = {}, -> { - ( - data: ActionCallbackParameter, - ): void | boolean | Filters; -} - -interface ResourceActionCallback< - Metadata extends Record = {}, - AuthContext extends Record = {}, -> { - ( - data: AuthCallbackParameter, - ): void | boolean | Filters; -} +import type { + AuthenticateCallback, + AnyCallback, + CallbackEvent, + OnCallback, + BaseAuthReturn, + ToUserLike, + BaseUser, +} from "./types.js"; export class Auth< - Metadata extends Record = {}, - AuthContext extends Record = {}, + TExtra, + TAuthReturn extends BaseAuthReturn = BaseAuthReturn, + TUser extends BaseUser = ToUserLike, > { + extra: TExtra; + "~handlerCache": { - authenticate?: AuthenticateCallback; - callbacks?: Record< - string, - (request: any) => void | boolean | Filters - >; + authenticate?: AuthenticateCallback; + callbacks?: Record; } = {}; - authenticate(cb: AuthenticateCallback): this { + authenticate( + cb: AuthenticateCallback, + ): Auth { this["~handlerCache"].authenticate = cb; - return this; + return this as unknown as Auth; } - /** - * Global handler for all requests - */ - on(event: "*", callback: GlobalCallback): this; - - /** - * Resource-specific handler - */ - on( - event: T, - callback: ResourceCallback, - ): this; - - /** - * Action-specific handler - */ - on( - event: T, - callback: ActionCallback, - ): this; - - /** - * Resource-action specific handler - */ - on( - event: T, - callback: ResourceActionCallback, - ): this; - - on( - event: string, - callback: ( - data: AuthCallbackParameter, - ) => void | boolean | Filters, - ): this { + on(event: T, callback: OnCallback): this { this["~handlerCache"].callbacks ??= {}; - this["~handlerCache"].callbacks[event] = callback; + this["~handlerCache"].callbacks[event as string] = callback as AnyCallback; return this; } } + +export type { Filters, ResourceActionType } from "./types.js"; +export { HTTPException } from "./error.js"; diff --git a/libs/sdk-js/src/auth/types.ts b/libs/sdk-js/src/auth/types.ts new file mode 100644 index 000000000..d2a39e05b --- /dev/null +++ b/libs/sdk-js/src/auth/types.ts @@ -0,0 +1,345 @@ +type Maybe = T | null | undefined; +type PromiseMaybe = Promise | T; + +interface AssistantConfig { + tags?: Maybe; + recursion_limit?: Maybe; + configurable?: Maybe<{ + thread_id?: Maybe; + thread_ts?: Maybe; + [key: string]: unknown; + }>; +} + +interface AssistantCreate { + assistant_id?: Maybe; + metadata?: Maybe>; + config?: Maybe; + if_exists?: Maybe<"raise" | "do_nothing">; + name?: Maybe; + graph_id: string; +} + +interface AssistantRead { + assistant_id: string; + metadata?: Maybe>; +} + +interface AssistantUpdate { + assistant_id: string; + metadata?: Maybe>; + config?: Maybe; + graph_id?: Maybe; + name?: Maybe; + version?: Maybe; +} + +interface AssistantDelete { + assistant_id: string; +} + +interface AssistantSearch { + graph_id?: Maybe; + metadata?: Maybe>; + limit?: Maybe; + offset?: Maybe; +} + +interface ThreadCreate { + thread_id?: Maybe; + metadata?: Maybe>; + if_exists?: Maybe<"raise" | "do_nothing">; +} + +interface ThreadRead { + thread_id?: Maybe; +} + +interface ThreadUpdate { + thread_id?: Maybe; + metadata?: Maybe>; + action?: Maybe<"interrupt" | "rollback">; +} + +interface ThreadDelete { + thread_id?: Maybe; + run_id?: Maybe; +} + +interface ThreadSearch { + thread_id?: Maybe; + status?: Maybe<"idle" | "busy" | "interrupted" | "error" | (string & {})>; + metadata?: Maybe>; + values?: Maybe>; + limit?: Maybe; + offset?: Maybe; +} + +interface CronCreate { + payload?: Maybe>; + schedule: string; + cron_id?: Maybe; + thread_id?: Maybe; + user_id?: Maybe; + end_time?: Maybe; +} + +interface CronRead { + cron_id: string; +} + +interface CronUpdate { + cron_id: string; + payload?: Maybe>; + schedule?: Maybe; +} + +interface CronDelete { + cron_id: string; +} + +interface CronSearch { + assistant_id?: Maybe; + thread_id?: Maybe; + limit?: Maybe; + offset?: Maybe; +} + +interface StorePut { + namespace: string[]; + key: string; + value: Record; +} + +interface StoreGet { + namespace: Maybe; + key: string; +} + +interface StoreSearch { + namespace?: Maybe; + filter?: Maybe>; + limit?: Maybe; + offset?: Maybe; + query?: Maybe; +} + +interface StoreListNamespaces { + namespace?: Maybe; + suffix?: Maybe; + max_depth?: Maybe; + limit?: Maybe; + offset?: Maybe; +} + +interface StoreDelete { + namespace?: Maybe; + key: string; +} + +interface RunsCreate { + thread_id?: Maybe; + assistant_id: string; + run_id: string; + status: Maybe< + "pending" | "running" | "error" | "success" | "timeout" | "interrupted" + >; + metadata?: Maybe>; + prevent_insert_if_inflight?: Maybe; + multitask_strategy?: Maybe<"interrupt" | "rollback" | "reject" | "enqueue">; + if_not_exists?: Maybe<"reject" | "create">; + after_seconds?: Maybe; + kwargs: Record; +} + +export interface ResourceActionType { + ["threads:create"]: ThreadCreate; + ["threads:read"]: ThreadRead; + ["threads:update"]: ThreadUpdate; + ["threads:delete"]: ThreadDelete; + ["threads:search"]: ThreadSearch; + ["threads:create_run"]: RunsCreate; + + ["assistants:create"]: AssistantCreate; + ["assistants:read"]: AssistantRead; + ["assistants:update"]: AssistantUpdate; + ["assistants:delete"]: AssistantDelete; + ["assistants:search"]: AssistantSearch; + + ["crons:create"]: CronCreate; + ["crons:read"]: CronRead; + ["crons:update"]: CronUpdate; + ["crons:delete"]: CronDelete; + ["crons:search"]: CronSearch; + + ["store:put"]: StorePut; + ["store:get"]: StoreGet; + ["store:search"]: StoreSearch; + ["store:list_namespaces"]: StoreListNamespaces; + ["store:delete"]: StoreDelete; +} +interface ResourceType { + threads: + | "threads:create" + | "threads:read" + | "threads:update" + | "threads:delete" + | "threads:search" + | "threads:create_run"; + + assistants: + | "assistants:create" + | "assistants:read" + | "assistants:update" + | "assistants:delete" + | "assistants:search"; + crons: + | "crons:create" + | "crons:read" + | "crons:update" + | "crons:delete" + | "crons:search"; + + store: + | "store:put" + | "store:get" + | "store:search" + | "store:list_namespaces" + | "store:delete"; +} +interface ActionType { + "*:create": "threads:create" | "assistants:create" | "crons:create"; + + "*:read": "threads:read" | "assistants:read" | "crons:read"; + + "*:update": "threads:update" | "assistants:update" | "crons:update"; + + "*:delete": + | "threads:delete" + | "assistants:delete" + | "crons:delete" + | "store:delete"; + + "*:search": + | "threads:search" + | "assistants:search" + | "crons:search" + | "store:search"; + + "*:create_run": "threads:create_run"; + + "*:put": "store:put"; + + "*:get": "store:get"; + + "*:list_namespaces": "store:list_namespaces"; +} + +export type BaseAuthReturn = + | { + is_authenticated?: boolean; + display_name?: string; + identity: string; + permissions: string[]; + } + | string; + +export interface BaseUser { + is_authenticated: boolean; + display_name: string; + identity: string; + permissions: string[]; +} + +export type ToUserLike = T extends string + ? { + is_authenticated: boolean; + display_name: string; + identity: string; + permissions: string[]; + } + : Omit & { + is_authenticated: boolean; + display_name: string; + }; + +type CallbackParameter< + Resource extends string = string, + Action extends string = string, + Value extends unknown = unknown, + TUser extends BaseUser = BaseUser, +> = { + resource: Resource; + action: Action; + value: Value; + user: TUser; + permissions: string[]; +}; + +type ContextMap = { + [ActionType in keyof ResourceActionType]: CallbackParameter< + ActionType extends `${infer Resource}:${string}` ? Resource : never, + ActionType, + ResourceActionType[ActionType], + BaseUser + >; +}; + +type ActionCallbackParameter< + T extends keyof ActionType, + TUser extends BaseUser = BaseUser, +> = ContextMap[ActionType[T]] & { user: TUser }; +type AuthCallbackParameter< + T extends keyof ResourceActionType, + TUser extends BaseUser = BaseUser, +> = ContextMap[T] & { user: TUser }; +type ResourceCallbackParameter< + T extends keyof ResourceType, + TUser extends BaseUser = BaseUser, +> = ContextMap[ResourceType[T]] & { user: TUser }; + +export type Filters = { + [key in TKey]: string | { [op in "$contains" | "$eq"]?: string }; +}; + +export interface AuthenticateCallback { + (request: Request): PromiseMaybe; +} + +type OnKey = keyof ResourceType | keyof ActionType | keyof ResourceActionType; + +type OnSingleParameter< + T extends OnKey, + TUser extends BaseUser = BaseUser, +> = T extends keyof ResourceType + ? ResourceCallbackParameter + : T extends keyof ActionType + ? ActionCallbackParameter + : T extends keyof ResourceActionType + ? AuthCallbackParameter + : never; + +type OnParameter< + T extends "*" | OnKey | OnKey[], + TUser extends BaseUser = BaseUser, +> = T extends OnKey[] + ? OnSingleParameter + : T extends "*" + ? AuthCallbackParameter + : T extends OnKey + ? OnSingleParameter + : never; + +export type AnyCallback = ( + request: CallbackParameter, +) => void | boolean | Filters; + +export type CallbackEvent = "*" | OnKey | OnKey[]; + +export type OnCallback< + T extends CallbackEvent, + TUser extends BaseUser = BaseUser, + TMetadata extends Record = Record, +> = ( + request: OnParameter, +) => void | boolean | Filters; From 5f1213a1c72316c0a6443d3da9d1872179cc4208 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Tue, 8 Apr 2025 17:07:04 +0200 Subject: [PATCH 13/32] Remove extra --- libs/sdk-js/src/auth/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/sdk-js/src/auth/index.ts b/libs/sdk-js/src/auth/index.ts index 726e638d4..5229b43df 100644 --- a/libs/sdk-js/src/auth/index.ts +++ b/libs/sdk-js/src/auth/index.ts @@ -13,8 +13,6 @@ export class Auth< TAuthReturn extends BaseAuthReturn = BaseAuthReturn, TUser extends BaseUser = ToUserLike, > { - extra: TExtra; - "~handlerCache": { authenticate?: AuthenticateCallback; callbacks?: Record; From 7d5621a84fb803066f93762d642eff33e8ec08da Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Tue, 8 Apr 2025 17:10:43 +0200 Subject: [PATCH 14/32] Default type for TExtra --- libs/sdk-js/src/auth/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/sdk-js/src/auth/index.ts b/libs/sdk-js/src/auth/index.ts index 5229b43df..c736f5cbb 100644 --- a/libs/sdk-js/src/auth/index.ts +++ b/libs/sdk-js/src/auth/index.ts @@ -9,7 +9,7 @@ import type { } from "./types.js"; export class Auth< - TExtra, + TExtra = {}, TAuthReturn extends BaseAuthReturn = BaseAuthReturn, TUser extends BaseUser = ToUserLike, > { From 5b73e38c3888321c97c050c18ff76ffcb2aaf8e6 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 8 Apr 2025 10:44:12 -0700 Subject: [PATCH 15/32] Make compatible with langchain-core 0.1 by conditionally importing _StreamingCallbackHandler --- libs/langgraph/langgraph/pregel/__init__.py | 24 ++++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 545c8faf1..1b28260d1 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -39,7 +39,11 @@ from langchain_core.runnables.utils import ( ConfigurableFieldSpec, get_unique_config_specs, ) -from langchain_core.tracers._streaming import _StreamingCallbackHandler + +try: + from langchain_core.tracers._streaming import _StreamingCallbackHandler +except ImportError: + _StreamingCallbackHandler = None from pydantic import BaseModel from typing_extensions import Self @@ -2529,13 +2533,17 @@ class Pregel(PregelProtocol): run_id=config.get("run_id"), ) # if running from astream_log() run each proc with streaming - do_stream = next( - ( - cast(_StreamingCallbackHandler, h) - for h in run_manager.handlers - if isinstance(h, _StreamingCallbackHandler) - ), - None, + do_stream = ( + next( + ( + cast(_StreamingCallbackHandler, h) # type: ignore + for h in run_manager.handlers + if isinstance(h, _StreamingCallbackHandler) + ), + None, + ) + if _StreamingCallbackHandler is not None + else False ) try: # assign defaults From 0b3bf37a558457f3e166f8a7227b446244e99efa Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 8 Apr 2025 10:47:55 -0700 Subject: [PATCH 16/32] Fix the rest --- libs/langgraph/langgraph/pregel/__init__.py | 10 +++--- libs/langgraph/langgraph/pregel/messages.py | 8 ++++- libs/langgraph/langgraph/utils/runnable.py | 38 +++++++++++++-------- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 1b28260d1..cb0a89a79 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -39,11 +39,6 @@ from langchain_core.runnables.utils import ( ConfigurableFieldSpec, get_unique_config_specs, ) - -try: - from langchain_core.tracers._streaming import _StreamingCallbackHandler -except ImportError: - _StreamingCallbackHandler = None from pydantic import BaseModel from typing_extensions import Self @@ -129,6 +124,11 @@ from langgraph.utils.fields import get_enhanced_type_hints from langgraph.utils.pydantic import create_model, is_supported_by_pydantic from langgraph.utils.queue import AsyncQueue, SyncQueue # type: ignore[attr-defined] +try: + from langchain_core.tracers._streaming import _StreamingCallbackHandler +except ImportError: + _StreamingCallbackHandler = None + WriteValue = Union[Callable[[Input], Output], Any] diff --git a/libs/langgraph/langgraph/pregel/messages.py b/libs/langgraph/langgraph/pregel/messages.py index 867012fa6..d53598220 100644 --- a/libs/langgraph/langgraph/pregel/messages.py +++ b/libs/langgraph/langgraph/pregel/messages.py @@ -7,6 +7,7 @@ from typing import ( List, Optional, Sequence, + TypeVar, Union, cast, ) @@ -15,11 +16,16 @@ from uuid import UUID, uuid4 from langchain_core.callbacks import BaseCallbackHandler from langchain_core.messages import BaseMessage from langchain_core.outputs import ChatGenerationChunk, LLMResult -from langchain_core.tracers._streaming import T, _StreamingCallbackHandler from langgraph.constants import NS_SEP, TAG_HIDDEN, TAG_NOSTREAM from langgraph.types import StreamChunk +try: + from langchain_core.tracers._streaming import _StreamingCallbackHandler +except ImportError: + _StreamingCallbackHandler = object + +T = TypeVar("T") Meta = tuple[tuple[str, ...], dict[str, Any]] diff --git a/libs/langgraph/langgraph/utils/runnable.py b/libs/langgraph/langgraph/utils/runnable.py index f21420ad0..f51da12d9 100644 --- a/libs/langgraph/langgraph/utils/runnable.py +++ b/libs/langgraph/langgraph/utils/runnable.py @@ -36,7 +36,6 @@ from langchain_core.runnables.config import ( var_child_runnable_config, ) from langchain_core.runnables.utils import Input, Output -from langchain_core.tracers._streaming import _StreamingCallbackHandler from typing_extensions import TypeGuard from langgraph.constants import ( @@ -54,6 +53,11 @@ from langgraph.utils.config import ( patch_config, ) +try: + from langchain_core.tracers._streaming import _StreamingCallbackHandler +except ImportError: + _StreamingCallbackHandler = None + def _set_config_context( config: RunnableConfig, @@ -683,13 +687,15 @@ class RunnableSeq(Runnable): iterator = step.stream(input, config, **kwargs) else: iterator = step.transform(iterator, config) - if stream_handler := next( - ( - cast(_StreamingCallbackHandler, h) - for h in run_manager.handlers - if isinstance(h, _StreamingCallbackHandler) - ), - None, + if _StreamingCallbackHandler is not None and ( + stream_handler := next( + ( + cast(_StreamingCallbackHandler, h) # type: ignore + for h in run_manager.handlers + if isinstance(h, _StreamingCallbackHandler) + ), + None, + ) ): # populates streamed_output in astream_log() output if needed iterator = stream_handler.tap_output_iter(run_manager.run_id, iterator) @@ -749,13 +755,15 @@ class RunnableSeq(Runnable): aiterator = step.atransform(aiterator, config) if hasattr(aiterator, "aclose"): stack.push_async_callback(aiterator.aclose) - if stream_handler := next( - ( - cast(_StreamingCallbackHandler, h) - for h in run_manager.handlers - if isinstance(h, _StreamingCallbackHandler) - ), - None, + if _StreamingCallbackHandler is not None and ( + stream_handler := next( + ( + cast(_StreamingCallbackHandler, h) # type: ignore + for h in run_manager.handlers + if isinstance(h, _StreamingCallbackHandler) + ), + None, + ) ): # populates streamed_output in astream_log() output if needed aiterator = stream_handler.tap_output_aiter( From cee6a450dc9bd4fcecd32bee14a7ba279939fdd8 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 8 Apr 2025 10:52:28 -0700 Subject: [PATCH 17/32] Lint --- libs/langgraph/langgraph/pregel/__init__.py | 4 ++-- libs/langgraph/langgraph/pregel/messages.py | 2 +- libs/langgraph/langgraph/utils/runnable.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index cb0a89a79..4711965f6 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -127,7 +127,7 @@ from langgraph.utils.queue import AsyncQueue, SyncQueue # type: ignore[attr-def try: from langchain_core.tracers._streaming import _StreamingCallbackHandler except ImportError: - _StreamingCallbackHandler = None + _StreamingCallbackHandler = None # type: ignore WriteValue = Union[Callable[[Input], Output], Any] @@ -2536,7 +2536,7 @@ class Pregel(PregelProtocol): do_stream = ( next( ( - cast(_StreamingCallbackHandler, h) # type: ignore + cast(_StreamingCallbackHandler, h) for h in run_manager.handlers if isinstance(h, _StreamingCallbackHandler) ), diff --git a/libs/langgraph/langgraph/pregel/messages.py b/libs/langgraph/langgraph/pregel/messages.py index d53598220..5766c2b63 100644 --- a/libs/langgraph/langgraph/pregel/messages.py +++ b/libs/langgraph/langgraph/pregel/messages.py @@ -23,7 +23,7 @@ from langgraph.types import StreamChunk try: from langchain_core.tracers._streaming import _StreamingCallbackHandler except ImportError: - _StreamingCallbackHandler = object + _StreamingCallbackHandler = object # type: ignore T = TypeVar("T") Meta = tuple[tuple[str, ...], dict[str, Any]] diff --git a/libs/langgraph/langgraph/utils/runnable.py b/libs/langgraph/langgraph/utils/runnable.py index f51da12d9..4dab44023 100644 --- a/libs/langgraph/langgraph/utils/runnable.py +++ b/libs/langgraph/langgraph/utils/runnable.py @@ -56,7 +56,7 @@ from langgraph.utils.config import ( try: from langchain_core.tracers._streaming import _StreamingCallbackHandler except ImportError: - _StreamingCallbackHandler = None + _StreamingCallbackHandler = None # type: ignore def _set_config_context( @@ -690,7 +690,7 @@ class RunnableSeq(Runnable): if _StreamingCallbackHandler is not None and ( stream_handler := next( ( - cast(_StreamingCallbackHandler, h) # type: ignore + cast(_StreamingCallbackHandler, h) for h in run_manager.handlers if isinstance(h, _StreamingCallbackHandler) ), @@ -758,7 +758,7 @@ class RunnableSeq(Runnable): if _StreamingCallbackHandler is not None and ( stream_handler := next( ( - cast(_StreamingCallbackHandler, h) # type: ignore + cast(_StreamingCallbackHandler, h) for h in run_manager.handlers if isinstance(h, _StreamingCallbackHandler) ), From 3a17df6106154bbcadc4e5a7a97ebe11fc396884 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Tue, 8 Apr 2025 14:53:10 -0400 Subject: [PATCH 18/32] langgraph: release 0.3.26 (#4204) --- libs/langgraph/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index cece38afb..a15580b0b 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.3.25" +version = "0.3.26" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From ccc21974e0fc84f17a3ec79372baa0b68c6b73cc Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 8 Apr 2025 13:44:31 -0700 Subject: [PATCH 19/32] Implement simpler filtering of config keys in RemoteGraph --- libs/langgraph/langgraph/pregel/remote.py | 68 ++++++++++------------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/remote.py b/libs/langgraph/langgraph/pregel/remote.py index 07d44cb66..13cc7af4f 100644 --- a/libs/langgraph/langgraph/pregel/remote.py +++ b/libs/langgraph/langgraph/pregel/remote.py @@ -10,7 +10,6 @@ from typing import ( cast, ) -import orjson from langchain_core.runnables import RunnableConfig from langchain_core.runnables.graph import ( Edge as DrawableEdge, @@ -35,6 +34,8 @@ from typing_extensions import Self from langgraph.checkpoint.base import CheckpointMetadata from langgraph.constants import ( CONF, + CONFIG_KEY_CHECKPOINT_ID, + CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_CHECKPOINT_NS, CONFIG_KEY_STREAM, INTERRUPT, @@ -46,6 +47,14 @@ from langgraph.pregel.types import All, PregelTask, StateSnapshot, StreamMode from langgraph.types import Command, Interrupt, StreamProtocol from langgraph.utils.config import merge_configs +CONF_DROPLIST = frozenset( + ( + CONFIG_KEY_CHECKPOINT_MAP, + CONFIG_KEY_CHECKPOINT_ID, + CONFIG_KEY_CHECKPOINT_NS, + ), +) + class RemoteException(Exception): """Exception raised when an error occurs in the remote graph.""" @@ -290,47 +299,26 @@ class RemoteGraph(PregelProtocol): } def _sanitize_config(self, config: RunnableConfig) -> RunnableConfig: - reserved_configurable_keys = frozenset( - [ - "callbacks", - "checkpoint_map", - "checkpoint_id", - "checkpoint_ns", - ] - ) - - def _sanitize_obj(obj: Any) -> Any: - """Remove non-JSON serializable fields from the given object.""" - if isinstance(obj, dict): - return {k: _sanitize_obj(v) for k, v in obj.items()} - elif isinstance(obj, list): - return [_sanitize_obj(v) for v in obj] - else: - try: - orjson.dumps(obj) - return obj - except orjson.JSONEncodeError: - return None - - # Remove non-JSON serializable fields from the config. - config = _sanitize_obj(config) - - # Only include configurable keys that are not reserved and - # not starting with "__pregel_" prefix. - new_configurable = { - k: v - for k, v in config["configurable"].items() - if k not in reserved_configurable_keys and not k.startswith("__pregel_") - } - - sanitized: RunnableConfig = { - "tags": config.get("tags") or [], - "metadata": config.get("metadata") or {}, - "configurable": new_configurable, - } + """Sanitize the config to remove non-serializable fields.""" + sanitized: RunnableConfig = {} if "recursion_limit" in config: sanitized["recursion_limit"] = config["recursion_limit"] - + if "tags" in config: + sanitized["tags"] = [tag for tag in config["tags"] if isinstance(tag, str)] + if "metadata" in config: + sanitized["metadata"] = {} + for k, v in config["metadata"].items(): + if isinstance(k, str) and isinstance(v, (str, int, float, bool)): + sanitized["metadata"][k] = v + if "configurable" in config: + sanitized["configurable"] = {} + for k, v in config["configurable"].items(): + if ( + isinstance(k, str) + and k not in CONF_DROPLIST + and isinstance(v, (str, int, float, bool)) + ): + sanitized["configurable"][k] = v return sanitized def get_state( From e9aec77893932072cd0ca7f6d526f0416c5085de Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 8 Apr 2025 14:07:02 -0700 Subject: [PATCH 20/32] Add more tests --- libs/langgraph/tests/test_pregel.py | 138 +++++++++--- libs/langgraph/tests/test_pregel_async.py | 247 ++++++++++++++++++++-- 2 files changed, 337 insertions(+), 48 deletions(-) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index f1d45c192..486b4cdf9 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -3683,10 +3683,14 @@ def test_nested_graph(snapshot: SnapshotAssertion) -> None: ] +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_subgraph_checkpoint_true( - request: pytest.FixtureRequest, checkpointer_name: str + request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool ) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Unsupported combo") + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) class InnerState(TypedDict): @@ -3718,7 +3722,12 @@ def test_subgraph_checkpoint_true( app = graph.compile(checkpointer=checkpointer) config = {"configurable": {"thread_id": "2"}} - assert [c for c in app.stream({"my_key": ""}, config, subgraphs=True)] == [ + assert [ + c + for c in app.stream( + {"my_key": ""}, config, subgraphs=True, checkpoint_during=checkpoint_during + ) + ] == [ (("inner",), {"inner_1": {"my_key": " got here", "my_other_key": ""}}), (("inner",), {"inner_2": {"my_key": " and there"}}), ((), {"inner": {"my_key": " got here and there"}}), @@ -3743,10 +3752,14 @@ def test_subgraph_checkpoint_true( ] +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_subgraph_checkpoint_true_interrupt( - request: pytest.FixtureRequest, checkpointer_name: str + request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool ) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Unsupported combo") + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) # Define subgraph @@ -3785,15 +3798,18 @@ def test_subgraph_checkpoint_true_interrupt( builder.add_edge(START, "node_1") builder.add_edge("node_1", "node_2") - checkpointer = MemorySaver() graph = builder.compile(checkpointer=checkpointer) config = {"configurable": {"thread_id": "1"}} - assert graph.invoke({"foo": "foo"}, config) == {"foo": "hi! foo"} + assert graph.invoke( + {"foo": "foo"}, config, checkpoint_during=checkpoint_during + ) == {"foo": "hi! foo"} assert graph.get_state(config, subgraphs=True).tasks[0].state.values == { "bar": "hi! foo" } - assert graph.invoke(Command(resume="baz"), config) == {"foo": "hi! foobaz"} + assert graph.invoke( + Command(resume="baz"), config, checkpoint_during=checkpoint_during + ) == {"foo": "hi! foobaz"} @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) @@ -3909,10 +3925,14 @@ def test_stream_buffering_single_node( ] +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_nested_graph_interrupts_parallel( - request: pytest.FixtureRequest, checkpointer_name: str + request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool ) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Unsupported combo") + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) class InnerState(TypedDict): @@ -3959,11 +3979,11 @@ def test_nested_graph_interrupts_parallel( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} - assert app.invoke({"my_key": ""}, config, debug=True) == { + assert app.invoke({"my_key": ""}, config, checkpoint_during=checkpoint_during) == { "my_key": " and parallel", } - assert app.invoke(None, config, debug=True) == { + assert app.invoke(None, config, checkpoint_during=checkpoint_during) == { "my_key": "got here and there and parallel and back again", } @@ -3972,13 +3992,17 @@ def test_nested_graph_interrupts_parallel( # - the writes of outer are persisted in 1st call and used in 2nd call, ie outer isn't called again (because we dont see outer_1 output again in 2nd stream) # test stream updates w/ nested interrupt config = {"configurable": {"thread_id": "2"}} - assert [*app.stream({"my_key": ""}, config, subgraphs=True)] == [ + assert [ + *app.stream( + {"my_key": ""}, config, subgraphs=True, checkpoint_during=checkpoint_during + ) + ] == [ # we got to parallel node first ((), {"outer_1": {"my_key": " and parallel"}}), ((AnyStr("inner:"),), {"inner_1": {"my_key": "got here", "my_other_key": ""}}), ((), {"__interrupt__": ()}), ] - assert [*app.stream(None, config)] == [ + assert [*app.stream(None, config, checkpoint_during=checkpoint_during)] == [ {"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}}, {"inner": {"my_key": "got here and there"}}, {"outer_2": {"my_key": " and back again"}}, @@ -3986,11 +4010,22 @@ def test_nested_graph_interrupts_parallel( # test stream values w/ nested interrupt config = {"configurable": {"thread_id": "3"}} - assert [*app.stream({"my_key": ""}, config, stream_mode="values")] == [ + assert [ + *app.stream( + {"my_key": ""}, + config, + stream_mode="values", + checkpoint_during=checkpoint_during, + ) + ] == [ {"my_key": ""}, {"my_key": " and parallel"}, ] - assert [*app.stream(None, config, stream_mode="values")] == [ + assert [ + *app.stream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": ""}, {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, @@ -3999,15 +4034,28 @@ def test_nested_graph_interrupts_parallel( # test interrupts BEFORE the parallel node app = graph.compile(checkpointer=checkpointer, interrupt_before=["outer_1"]) config = {"configurable": {"thread_id": "4"}} - assert [*app.stream({"my_key": ""}, config, stream_mode="values")] == [ - {"my_key": ""} - ] + assert [ + *app.stream( + {"my_key": ""}, + config, + stream_mode="values", + checkpoint_during=checkpoint_during, + ) + ] == [{"my_key": ""}] # while we're waiting for the node w/ interrupt inside to finish - assert [*app.stream(None, config, stream_mode="values")] == [ + assert [ + *app.stream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": ""}, {"my_key": " and parallel"}, ] - assert [*app.stream(None, config, stream_mode="values")] == [ + assert [ + *app.stream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": ""}, {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, @@ -4016,24 +4064,43 @@ def test_nested_graph_interrupts_parallel( # test interrupts AFTER the parallel node app = graph.compile(checkpointer=checkpointer, interrupt_after=["outer_1"]) config = {"configurable": {"thread_id": "5"}} - assert [*app.stream({"my_key": ""}, config, stream_mode="values")] == [ + assert [ + *app.stream( + {"my_key": ""}, + config, + stream_mode="values", + checkpoint_during=checkpoint_during, + ) + ] == [ {"my_key": ""}, {"my_key": " and parallel"}, ] - assert [*app.stream(None, config, stream_mode="values")] == [ + assert [ + *app.stream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": ""}, {"my_key": "got here and there and parallel"}, ] - assert [*app.stream(None, config, stream_mode="values")] == [ + assert [ + *app.stream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, ] +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_doubly_nested_graph_interrupts( - request: pytest.FixtureRequest, checkpointer_name: str + request: pytest.FixtureRequest, checkpointer_name: str, checkpoint_during: bool ) -> None: + if not checkpoint_during and "shallow" in checkpointer_name: + pytest.skip("Unsupported combo") + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) class State(TypedDict): @@ -4087,11 +4154,13 @@ def test_doubly_nested_graph_interrupts( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} - assert app.invoke({"my_key": "my value"}, config, debug=True) == { + assert app.invoke( + {"my_key": "my value"}, config, checkpoint_during=checkpoint_during + ) == { "my_key": "hi my value", } - assert app.invoke(None, config, debug=True) == { + assert app.invoke(None, config, checkpoint_during=checkpoint_during) == { "my_key": "hi my value here and there and back again", } @@ -4100,12 +4169,14 @@ def test_doubly_nested_graph_interrupts( config = { "configurable": {"thread_id": "2", CONFIG_KEY_NODE_FINISHED: nodes.append} } - assert [*app.stream({"my_key": "my value"}, config)] == [ + assert [ + *app.stream({"my_key": "my value"}, config, checkpoint_during=checkpoint_during) + ] == [ {"parent_1": {"my_key": "hi my value"}}, {"__interrupt__": ()}, ] assert nodes == ["parent_1", "grandchild_1"] - assert [*app.stream(None, config)] == [ + assert [*app.stream(None, config, checkpoint_during=checkpoint_during)] == [ {"child": {"my_key": "hi my value here and there"}}, {"parent_2": {"my_key": "hi my value here and there and back again"}}, ] @@ -4120,11 +4191,22 @@ def test_doubly_nested_graph_interrupts( # test stream values w/ nested interrupt config = {"configurable": {"thread_id": "3"}} - assert [*app.stream({"my_key": "my value"}, config, stream_mode="values")] == [ + assert [ + *app.stream( + {"my_key": "my value"}, + config, + stream_mode="values", + checkpoint_during=checkpoint_during, + ) + ] == [ {"my_key": "my value"}, {"my_key": "hi my value"}, ] - assert [*app.stream(None, config, stream_mode="values")] == [ + assert [ + *app.stream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": "hi my value"}, {"my_key": "hi my value here and there"}, {"my_key": "hi my value here and there and back again"}, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 4cc115f6f..20e2b96b5 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -5433,6 +5433,131 @@ async def test_nested_graph(snapshot: SnapshotAssertion) -> None: assert times_called == 1 +@pytest.mark.parametrize("checkpoint_during", [True, False]) +@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC) +async def test_subgraph_checkpoint_true( + checkpointer_name: str, checkpoint_during: bool +) -> None: + class InnerState(TypedDict): + my_key: Annotated[str, operator.add] + my_other_key: str + + def inner_1(state: InnerState): + return {"my_key": " got here", "my_other_key": state["my_key"]} + + def inner_2(state: InnerState): + return {"my_key": " and there"} + + inner = StateGraph(InnerState) + inner.add_node("inner_1", inner_1) + inner.add_node("inner_2", inner_2) + inner.add_edge("inner_1", "inner_2") + inner.set_entry_point("inner_1") + inner.set_finish_point("inner_2") + + class State(TypedDict): + my_key: str + + graph = StateGraph(State) + graph.add_node("inner", inner.compile(checkpointer=True)) + graph.add_edge(START, "inner") + graph.add_conditional_edges( + "inner", lambda s: "inner" if s["my_key"].count("there") < 2 else END + ) + + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = graph.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "2"}} + assert [ + c + async for c in app.astream( + {"my_key": ""}, + config, + subgraphs=True, + checkpoint_during=checkpoint_during, + ) + ] == [ + (("inner",), {"inner_1": {"my_key": " got here", "my_other_key": ""}}), + (("inner",), {"inner_2": {"my_key": " and there"}}), + ((), {"inner": {"my_key": " got here and there"}}), + ( + ("inner",), + { + "inner_1": { + "my_key": " got here", + "my_other_key": " got here and there got here and there", + } + }, + ), + (("inner",), {"inner_2": {"my_key": " and there"}}), + ( + (), + { + "inner": { + "my_key": " got here and there got here and there got here and there" + } + }, + ), + ] + + +@pytest.mark.parametrize("checkpoint_during", [True, False]) +@pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC) +async def test_subgraph_checkpoint_true_interrupt( + checkpointer_name: str, checkpoint_during: bool +) -> None: + # Define subgraph + class SubgraphState(TypedDict): + # note that none of these keys are shared with the parent graph state + bar: str + baz: str + + def subgraph_node_1(state: SubgraphState): + baz_value = interrupt("Provide baz value") + return {"baz": baz_value} + + def subgraph_node_2(state: SubgraphState): + return {"bar": state["bar"] + state["baz"]} + + subgraph_builder = StateGraph(SubgraphState) + subgraph_builder.add_node(subgraph_node_1) + subgraph_builder.add_node(subgraph_node_2) + subgraph_builder.add_edge(START, "subgraph_node_1") + subgraph_builder.add_edge("subgraph_node_1", "subgraph_node_2") + subgraph = subgraph_builder.compile(checkpointer=True) + + class ParentState(TypedDict): + foo: str + + def node_1(state: ParentState): + return {"foo": "hi! " + state["foo"]} + + async def node_2(state: ParentState): + response = await subgraph.ainvoke({"bar": state["foo"]}) + return {"foo": response["bar"]} + + builder = StateGraph(ParentState) + builder.add_node("node_1", node_1) + builder.add_node("node_2", node_2) + builder.add_edge(START, "node_1") + builder.add_edge("node_1", "node_2") + + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) + config = {"configurable": {"thread_id": "1"}} + + assert await graph.ainvoke( + {"foo": "foo"}, config, checkpoint_during=checkpoint_during + ) == {"foo": "hi! foo"} + assert (await graph.aget_state(config, subgraphs=True)).tasks[ + 0 + ].state.values == {"bar": "hi! foo"} + assert await graph.ainvoke( + Command(resume="baz"), config, checkpoint_during=checkpoint_during + ) == {"foo": "hi! foobaz"} + + @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_stream_subgraphs_during_execution(checkpointer_name: str) -> None: class InnerState(TypedDict): @@ -5541,8 +5666,11 @@ async def test_stream_buffering_single_node(checkpointer_name: str) -> None: ] +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_nested_graph_interrupts_parallel(checkpointer_name: str) -> None: +async def test_nested_graph_interrupts_parallel( + checkpointer_name: str, checkpoint_during: bool +) -> None: class InnerState(TypedDict): my_key: Annotated[str, operator.add] my_other_key: str @@ -5591,11 +5719,13 @@ async def test_nested_graph_interrupts_parallel(checkpointer_name: str) -> None: # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} - assert await app.ainvoke({"my_key": ""}, config, debug=True) == { + assert await app.ainvoke( + {"my_key": ""}, config, checkpoint_during=checkpoint_during + ) == { "my_key": " and parallel", } - assert await app.ainvoke(None, config, debug=True) == { + assert await app.ainvoke(None, config, checkpoint_during=checkpoint_during) == { "my_key": "got here and there and parallel and back again", } @@ -5605,7 +5735,13 @@ async def test_nested_graph_interrupts_parallel(checkpointer_name: str) -> None: # test stream updates w/ nested interrupt config = {"configurable": {"thread_id": "2"}} assert [ - c async for c in app.astream({"my_key": ""}, config, subgraphs=True) + c + async for c in app.astream( + {"my_key": ""}, + config, + subgraphs=True, + checkpoint_during=checkpoint_during, + ) ] == [ # we got to parallel node first ((), {"outer_1": {"my_key": " and parallel"}}), @@ -5615,7 +5751,12 @@ async def test_nested_graph_interrupts_parallel(checkpointer_name: str) -> None: ), ((), {"__interrupt__": ()}), ] - assert [c async for c in app.astream(None, config)] == [ + assert [ + c + async for c in app.astream( + None, config, checkpoint_during=checkpoint_during + ) + ] == [ {"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}}, {"inner": {"my_key": "got here and there"}}, {"outer_2": {"my_key": " and back again"}}, @@ -5624,12 +5765,23 @@ async def test_nested_graph_interrupts_parallel(checkpointer_name: str) -> None: # test stream values w/ nested interrupt config = {"configurable": {"thread_id": "3"}} assert [ - c async for c in app.astream({"my_key": ""}, config, stream_mode="values") + c + async for c in app.astream( + {"my_key": ""}, + config, + stream_mode="values", + checkpoint_during=checkpoint_during, + ) ] == [ {"my_key": ""}, {"my_key": " and parallel"}, ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + assert [ + c + async for c in app.astream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": ""}, {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, @@ -5639,16 +5791,32 @@ async def test_nested_graph_interrupts_parallel(checkpointer_name: str) -> None: app = graph.compile(checkpointer=checkpointer, interrupt_before=["outer_1"]) config = {"configurable": {"thread_id": "4"}} assert [ - c async for c in app.astream({"my_key": ""}, config, stream_mode="values") + c + async for c in app.astream( + {"my_key": ""}, + config, + stream_mode="values", + checkpoint_during=checkpoint_during, + ) ] == [ {"my_key": ""}, ] # while we're waiting for the node w/ interrupt inside to finish - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + assert [ + c + async for c in app.astream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": ""}, {"my_key": " and parallel"}, ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + assert [ + c + async for c in app.astream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": ""}, {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, @@ -5658,23 +5826,42 @@ async def test_nested_graph_interrupts_parallel(checkpointer_name: str) -> None: app = graph.compile(checkpointer=checkpointer, interrupt_after=["outer_1"]) config = {"configurable": {"thread_id": "5"}} assert [ - c async for c in app.astream({"my_key": ""}, config, stream_mode="values") + c + async for c in app.astream( + {"my_key": ""}, + config, + stream_mode="values", + checkpoint_during=checkpoint_during, + ) ] == [ {"my_key": ""}, {"my_key": " and parallel"}, ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + assert [ + c + async for c in app.astream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": ""}, {"my_key": "got here and there and parallel"}, ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + assert [ + c + async for c in app.astream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, ] +@pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_doubly_nested_graph_interrupts(checkpointer_name: str) -> None: +async def test_doubly_nested_graph_interrupts( + checkpointer_name: str, checkpoint_during: bool +) -> None: class State(TypedDict): my_key: str @@ -5727,11 +5914,13 @@ async def test_doubly_nested_graph_interrupts(checkpointer_name: str) -> None: # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} - assert await app.ainvoke({"my_key": "my value"}, config, debug=True) == { + assert await app.ainvoke( + {"my_key": "my value"}, config, checkpoint_during=checkpoint_during + ) == { "my_key": "hi my value", } - assert await app.ainvoke(None, config, debug=True) == { + assert await app.ainvoke(None, config, checkpoint_during=checkpoint_during) == { "my_key": "hi my value here and there and back again", } @@ -5740,12 +5929,22 @@ async def test_doubly_nested_graph_interrupts(checkpointer_name: str) -> None: config = { "configurable": {"thread_id": "2", CONFIG_KEY_NODE_FINISHED: nodes.append} } - assert [c async for c in app.astream({"my_key": "my value"}, config)] == [ + assert [ + c + async for c in app.astream( + {"my_key": "my value"}, config, checkpoint_during=checkpoint_during + ) + ] == [ {"parent_1": {"my_key": "hi my value"}}, {"__interrupt__": ()}, ] assert nodes == ["parent_1", "grandchild_1"] - assert [c async for c in app.astream(None, config)] == [ + assert [ + c + async for c in app.astream( + None, config, checkpoint_during=checkpoint_during + ) + ] == [ {"child": {"my_key": "hi my value here and there"}}, {"parent_2": {"my_key": "hi my value here and there and back again"}}, ] @@ -5763,13 +5962,21 @@ async def test_doubly_nested_graph_interrupts(checkpointer_name: str) -> None: assert [ c async for c in app.astream( - {"my_key": "my value"}, config, stream_mode="values" + {"my_key": "my value"}, + config, + stream_mode="values", + checkpoint_during=checkpoint_during, ) ] == [ {"my_key": "my value"}, {"my_key": "hi my value"}, ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + assert [ + c + async for c in app.astream( + None, config, stream_mode="values", checkpoint_during=checkpoint_during + ) + ] == [ {"my_key": "hi my value"}, {"my_key": "hi my value here and there"}, {"my_key": "hi my value here and there and back again"}, From cbbfaba1fd85bd708942f763079b380714de051e Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 8 Apr 2025 14:07:08 -0700 Subject: [PATCH 21/32] Add comments --- libs/langgraph/langgraph/pregel/loop.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 772564a50..e1de05602 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -296,6 +296,8 @@ class PregelLoop(LoopProtocol): """Put writes for a task, to be read by the next tick.""" if not writes: return + # always checkpoint writes containing Send, as they are fetched from the + # parent checkpoint, not the current one checkpoint_during = self.checkpoint_during or any(w[0] == TASKS for w in writes) # deduplicate writes to special channels, last write wins if all(w[0] in WRITES_IDX_MAP for w in writes): @@ -789,6 +791,7 @@ class PregelLoop(LoopProtocol): **self.checkpoint_config, CONF: { **self.checkpoint_config[CONF], + # this is guaranteed to be set by code above CONFIG_KEY_CHECKPOINT_ID: self.checkpoint_id_prev, CONFIG_KEY_CHECKPOINT_NS: self.config[CONF].get( CONFIG_KEY_CHECKPOINT_NS, "" From b76dc8ae0a209fcf906216d6fcd4ef1b796f2bbe Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 8 Apr 2025 14:15:13 -0700 Subject: [PATCH 22/32] Fix --- libs/langgraph/tests/test_pregel_async.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 20e2b96b5..81f3c2289 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -5533,8 +5533,8 @@ async def test_subgraph_checkpoint_true_interrupt( def node_1(state: ParentState): return {"foo": "hi! " + state["foo"]} - async def node_2(state: ParentState): - response = await subgraph.ainvoke({"bar": state["foo"]}) + async def node_2(state: ParentState, config: RunnableConfig): + response = await subgraph.ainvoke({"bar": state["foo"]}, config) return {"foo": response["bar"]} builder = StateGraph(ParentState) From 947a233fc5f2694780c2bd0855ff01be06d33121 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 8 Apr 2025 14:20:29 -0700 Subject: [PATCH 23/32] Fix --- libs/langgraph/tests/test_pregel_async.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 81f3c2289..dd146dbb7 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -5502,6 +5502,7 @@ async def test_subgraph_checkpoint_true( ] +@NEEDS_CONTEXTVARS @pytest.mark.parametrize("checkpoint_during", [True, False]) @pytest.mark.parametrize("checkpointer_name", REGULAR_CHECKPOINTERS_ASYNC) async def test_subgraph_checkpoint_true_interrupt( @@ -5534,7 +5535,7 @@ async def test_subgraph_checkpoint_true_interrupt( return {"foo": "hi! " + state["foo"]} async def node_2(state: ParentState, config: RunnableConfig): - response = await subgraph.ainvoke({"bar": state["foo"]}, config) + response = await subgraph.ainvoke({"bar": state["foo"]}) return {"foo": response["bar"]} builder = StateGraph(ParentState) From 27e4b0fcfee221df607f7768c36594c42d4bd054 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Tue, 8 Apr 2025 23:45:03 +0200 Subject: [PATCH 24/32] release(sdk-js): 0.0.64 --- libs/sdk-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json index 343a33412..2c6fea683 100644 --- a/libs/sdk-js/package.json +++ b/libs/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-sdk", - "version": "0.0.63", + "version": "0.0.64", "description": "Client library for interacting with the LangGraph API", "type": "module", "packageManager": "yarn@1.22.19", From 392805938ec7e84adb0a5f75f87fb62d8d7ea175 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 8 Apr 2025 15:04:46 -0700 Subject: [PATCH 25/32] 0.3.27 --- libs/langgraph/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index a15580b0b..722b2e018 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.3.26" +version = "0.3.27" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From 1b9093459c1550323c5ec183cf5f33600ece90a3 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:49:47 -0700 Subject: [PATCH 26/32] Remove pip from image Signed-off-by: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> --- libs/cli/langgraph_cli/config.py | 9 +++++++++ libs/cli/pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 5c512b58c..29807dae0 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -406,6 +406,13 @@ class Config(TypedDict, total=False): """ +PIP_CLEANUP_LINES = """# -- Removing pip from the final image ~<:===~~~ -- +RUN pip uninstall -y pip setuptools wheel && \ + rm -rf /usr/local/lib/python*/site-packages/pip* /usr/local/lib/python*/site-packages/setuptools* /usr/local/lib/python*/site-packages/wheel* && \ + find /usr/local/bin -name "pip*" -delete +# -- End of pip removal --""" + + def _parse_version(version_str: str) -> tuple[int, int]: """Parse a version string into a tuple of (major, minor).""" try: @@ -1141,6 +1148,8 @@ ADD {relpath} /deps/{name} "", ui_inst_str, "", + PIP_CLEANUP_LINES, # Add pip cleanup after all installations are complete + "", f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else "", ] diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml index e350076f9..2040ad217 100644 --- a/libs/cli/pyproject.toml +++ b/libs/cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-cli" -version = "0.1.89" +version = "0.1.90" description = "CLI for interacting with LangGraph API" authors = [] license = "MIT" From 3193f5d063729ea749f55fa6b0c1b839c6f7415b Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Wed, 9 Apr 2025 01:56:32 +0200 Subject: [PATCH 27/32] feat(sdk-js): add support for registering multiple events at once --- libs/sdk-js/package.json | 2 +- libs/sdk-js/src/auth/index.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json index 2c6fea683..29510712b 100644 --- a/libs/sdk-js/package.json +++ b/libs/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-sdk", - "version": "0.0.64", + "version": "0.0.65", "description": "Client library for interacting with the LangGraph API", "type": "module", "packageManager": "yarn@1.22.19", diff --git a/libs/sdk-js/src/auth/index.ts b/libs/sdk-js/src/auth/index.ts index c736f5cbb..d93dd889a 100644 --- a/libs/sdk-js/src/auth/index.ts +++ b/libs/sdk-js/src/auth/index.ts @@ -27,7 +27,11 @@ export class Auth< on(event: T, callback: OnCallback): this { this["~handlerCache"].callbacks ??= {}; - this["~handlerCache"].callbacks[event as string] = callback as AnyCallback; + const events = Array.isArray(event) ? event : [event]; + for (const event of events) { + this["~handlerCache"].callbacks[event as string] = + callback as AnyCallback; + } return this; } } From 72260e64d531a5e27d9b51e9e4b73e8ed26341f9 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Wed, 9 Apr 2025 01:57:50 +0200 Subject: [PATCH 28/32] Prevent casting --- libs/sdk-js/src/auth/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/sdk-js/src/auth/index.ts b/libs/sdk-js/src/auth/index.ts index d93dd889a..cf995438d 100644 --- a/libs/sdk-js/src/auth/index.ts +++ b/libs/sdk-js/src/auth/index.ts @@ -27,10 +27,9 @@ export class Auth< on(event: T, callback: OnCallback): this { this["~handlerCache"].callbacks ??= {}; - const events = Array.isArray(event) ? event : [event]; + const events: string[] = Array.isArray(event) ? event : [event]; for (const event of events) { - this["~handlerCache"].callbacks[event as string] = - callback as AnyCallback; + this["~handlerCache"].callbacks[event] = callback as AnyCallback; } return this; } From 5b58efc8d72a76aeb6c02e1f15b8376a40c11304 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:08:24 -0700 Subject: [PATCH 29/32] Update tests --- libs/cli/tests/unit_tests/cli/test_cli.py | 4 +- libs/cli/tests/unit_tests/test_config.py | 84 +++++++++++++++-------- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/libs/cli/tests/unit_tests/cli/test_cli.py b/libs/cli/tests/unit_tests/cli/test_cli.py index 53954da5e..4140ee9a5 100644 --- a/libs/cli/tests/unit_tests/cli/test_cli.py +++ b/libs/cli/tests/unit_tests/cli/test_cli.py @@ -2,13 +2,14 @@ import json import pathlib import shutil import tempfile +import textwrap from contextlib import contextmanager from pathlib import Path from click.testing import CliRunner from langgraph_cli.cli import cli, prepare_args_and_stdin -from langgraph_cli.config import Config, validate_config +from langgraph_cli.config import PIP_CLEANUP_LINES, Config, validate_config from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version from langgraph_cli.util import clean_empty_lines @@ -143,6 +144,7 @@ services: RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}' +{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")} WORKDIR /deps/cli develop: diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index dfa8cbbef..cb27b0d0c 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -2,11 +2,13 @@ import json import os import pathlib import tempfile +import textwrap import click import pytest from langgraph_cli.config import ( + PIP_CLEANUP_LINES, config_to_compose, config_to_docker, validate_config, @@ -208,7 +210,7 @@ def test_config_to_docker_simple(): ), "langchain/langgraph-api", ) - expected_docker_stdin = """\ + expected_docker_stdin = f"""\ FROM langchain/langgraph-api:3.11 # -- Installing local requirements -- COPY --from=__outer_requirements.txt requirements.txt /deps/__outer_graphs_reqs_a/graphs_reqs_a/requirements.txt @@ -242,8 +244,9 @@ RUN set -ex && \\ # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- -ENV LANGGRAPH_HTTP='{"app": "/deps/examples/my_app.py:app"}' -ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' +ENV LANGGRAPH_HTTP='{{"app": "/deps/examples/my_app.py:app"}}' +ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}' +{PIP_CLEANUP_LINES} WORKDIR /deps/__outer_unit_tests/unit_tests\ """ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin @@ -263,7 +266,8 @@ def test_config_to_docker_outside_path(): validate_config({"dependencies": [".", ".."], "graphs": graphs}), "langchain/langgraph-api", ) - expected_docker_stdin = """\ + expected_docker_stdin = ( + """\ FROM langchain/langgraph-api:3.11 # -- Adding non-package dependency unit_tests -- ADD . /deps/__outer_unit_tests/unit_tests @@ -291,8 +295,12 @@ RUN set -ex && \\ RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' +""" + + PIP_CLEANUP_LINES + + """ WORKDIR /deps/__outer_unit_tests/unit_tests\ """ + ) assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin assert additional_contexts == { "__outer_tests": str(pathlib.Path(__file__).parent.parent.absolute()), @@ -312,7 +320,8 @@ def test_config_to_docker_pipconfig(): ), "langchain/langgraph-api", ) - expected_docker_stdin = """\ + expected_docker_stdin = ( + """\ FROM langchain/langgraph-api:3.11 ADD pipconfig.txt /pipconfig.txt # -- Adding non-package dependency unit_tests -- @@ -330,8 +339,12 @@ RUN set -ex && \\ RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' +""" + + PIP_CLEANUP_LINES + + """ WORKDIR /deps/__outer_unit_tests/unit_tests\ """ + ) assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin assert additional_contexts == {} @@ -368,7 +381,7 @@ def test_config_to_docker_local_deps(): ), "langchain/langgraph-api-custom", ) - expected_docker_stdin = """\ + expected_docker_stdin = f"""\ FROM langchain/langgraph-api-custom:3.11 # -- Adding non-package dependency graphs -- ADD ./graphs /deps/__outer_graphs/src @@ -384,7 +397,8 @@ RUN set -ex && \\ # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- -ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'\ +ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_graphs/src/agent.py:graph"}}' +{PIP_CLEANUP_LINES}\ """ assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin assert additional_contexts == {} @@ -411,7 +425,8 @@ dependencies = ["langchain"]""" "langchain/langgraph-api", ) os.remove(pyproject_path) - expected_docker_stdin = """FROM langchain/langgraph-api:3.11 + expected_docker_stdin = ( + """FROM langchain/langgraph-api:3.11 # -- Adding local package . -- ADD . /deps/unit_tests # -- End of local package . -- @@ -419,7 +434,12 @@ ADD . /deps/unit_tests RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- ENV LANGSERVE_GRAPHS='{"agent": "/deps/unit_tests/graphs/agent.py:graph"}' -WORKDIR /deps/unit_tests""" +""" + + PIP_CLEANUP_LINES + + "\n" + + "WORKDIR /deps/unit_tests" + "" + ) assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin assert additional_contexts == {} @@ -439,7 +459,7 @@ def test_config_to_docker_end_to_end(): ), "langchain/langgraph-api", ) - expected_docker_stdin = """FROM langchain/langgraph-api:3.12 + expected_docker_stdin = f"""FROM langchain/langgraph-api:3.12 ARG meow ARG foo ADD pipconfig.txt /pipconfig.txt @@ -458,7 +478,8 @@ RUN set -ex && \\ # -- Installing all local dependencies -- RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- -ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'""" +ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_graphs/src/agent.py:graph"}}' +{PIP_CLEANUP_LINES}""" assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin assert additional_contexts == {} @@ -509,7 +530,7 @@ def test_config_to_docker_gen_ui_python(): "langchain/langgraph-api", ) - expected_docker_stdin = """FROM langchain/langgraph-api:3.11 + expected_docker_stdin = f"""FROM langchain/langgraph-api:3.11 RUN /storage/install-node.sh # -- Adding non-package dependency unit_tests -- ADD . /deps/__outer_unit_tests/unit_tests @@ -525,12 +546,13 @@ RUN set -ex && \\ # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- -ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' +ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}' # -- Installing UI dependencies -- -ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}' -ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}' +ENV LANGGRAPH_UI='{{"agent": "./graphs/agent.ui.jsx"}}' +ENV LANGGRAPH_UI_CONFIG='{{"shared": ["nuqs"]}}' RUN cd /deps/__outer_unit_tests/unit_tests && npm i && tsx /api/langgraph_api/js/build.mts # -- End of UI dependencies install -- +{PIP_CLEANUP_LINES} WORKDIR /deps/__outer_unit_tests/unit_tests""" assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin @@ -540,8 +562,8 @@ WORKDIR /deps/__outer_unit_tests/unit_tests""" # config_to_compose def test_config_to_compose_simple_config(): graphs = {"agent": "./agent.py:graph"} - expected_compose_stdin = """\ - + # Create a properly indented version of PIP_CLEANUP_LINES for compose files + expected_compose_stdin = f""" pull_policy: build build: context: . @@ -561,7 +583,8 @@ def test_config_to_compose_simple_config(): # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- - ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' + ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}' +{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")} WORKDIR /deps/__outer_unit_tests/unit_tests """ actual_compose_stdin = config_to_compose( @@ -569,12 +592,15 @@ def test_config_to_compose_simple_config(): validate_config({"dependencies": ["."], "graphs": graphs}), "langchain/langgraph-api", ) - assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin + assert ( + clean_empty_lines(actual_compose_stdin).strip() + == expected_compose_stdin.strip() + ) def test_config_to_compose_env_vars(): graphs = {"agent": "./agent.py:graph"} - expected_compose_stdin = """ OPENAI_API_KEY: "key" + expected_compose_stdin = f""" OPENAI_API_KEY: "key" pull_policy: build build: @@ -595,7 +621,8 @@ def test_config_to_compose_env_vars(): # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- - ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' + ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}' +{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")} WORKDIR /deps/__outer_unit_tests/unit_tests """ openai_api_key = "key" @@ -615,7 +642,7 @@ def test_config_to_compose_env_vars(): def test_config_to_compose_env_file(): graphs = {"agent": "./agent.py:graph"} - expected_compose_stdin = """\ + expected_compose_stdin = f"""\ env_file: .env pull_policy: build build: @@ -636,7 +663,8 @@ def test_config_to_compose_env_file(): # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- - ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' + ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}' +{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")} WORKDIR /deps/__outer_unit_tests/unit_tests """ actual_compose_stdin = config_to_compose( @@ -649,7 +677,7 @@ def test_config_to_compose_env_file(): def test_config_to_compose_watch(): graphs = {"agent": "./agent.py:graph"} - expected_compose_stdin = """\ + expected_compose_stdin = f"""\ pull_policy: build build: @@ -670,7 +698,8 @@ def test_config_to_compose_watch(): # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- - ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' + ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}' +{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")} WORKDIR /deps/__outer_unit_tests/unit_tests develop: @@ -692,7 +721,7 @@ def test_config_to_compose_watch(): def test_config_to_compose_end_to_end(): # test all of the above + langgraph API path graphs = {"agent": "./agent.py:graph"} - expected_compose_stdin = """\ + expected_compose_stdin = f"""\ env_file: .env pull_policy: build build: @@ -713,7 +742,8 @@ def test_config_to_compose_end_to_end(): # -- Installing all local dependencies -- RUN PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* # -- End of local dependencies install -- - ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' + ENV LANGSERVE_GRAPHS='{{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}}' +{textwrap.indent(textwrap.dedent(PIP_CLEANUP_LINES), " ")} WORKDIR /deps/__outer_unit_tests/unit_tests develop: From 288fe129331bfe6293cdd9d77287f9f6fe958557 Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:31:00 -0700 Subject: [PATCH 30/32] docs: Fix link (#4211) --- docs/docs/how-tos/ttl/configure_ttl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/how-tos/ttl/configure_ttl.md b/docs/docs/how-tos/ttl/configure_ttl.md index 9fdbf8eb0..de431946c 100644 --- a/docs/docs/how-tos/ttl/configure_ttl.md +++ b/docs/docs/how-tos/ttl/configure_ttl.md @@ -2,7 +2,7 @@ !!! tip "Prerequisites" - This guide assumes familiarity with the [LangGraph Platform](../../concepts/index.md#langgraph-platform), [Persistence](../../concepts/persistence.md), and [Cross-thread persistence](../../concepts/store.md) concepts. + This guide assumes familiarity with the [LangGraph Platform](../../concepts/index.md#langgraph-platform), [Persistence](../../concepts/persistence.md), and [Cross-thread persistence](../../concepts/persistence.md#memory-store) concepts. ???+ note "LangGraph platform only" From c6f5e561ec672f606ec8e203df408d071bd741d0 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 8 Apr 2025 17:52:24 -0700 Subject: [PATCH 31/32] Update poetry version used in ci --- .github/workflows/_integration_test.yml | 3 +-- .github/workflows/_lint.yml | 8 +------- .github/workflows/_test.yml | 8 +------- .github/workflows/_test_langgraph.yml | 2 +- .github/workflows/_test_release.yml | 2 +- .github/workflows/_test_scheduler_kafka.yml | 2 +- .github/workflows/baseline.yml | 2 +- .github/workflows/bench.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/deploy_docs.yml | 2 +- .github/workflows/link_check.yml | 12 ++++++------ .github/workflows/release.yml | 2 +- .github/workflows/run_notebooks.yml | 4 ++-- 13 files changed, 19 insertions(+), 32 deletions(-) diff --git a/.github/workflows/_integration_test.yml b/.github/workflows/_integration_test.yml index c2d7cb895..1cfdb5fde 100644 --- a/.github/workflows/_integration_test.yml +++ b/.github/workflows/_integration_test.yml @@ -4,7 +4,7 @@ on: workflow_call: env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" jobs: build: @@ -71,4 +71,3 @@ jobs: working-directory: libs/cli/js-examples run: | langgraph build -t langgraph-test-e - \ No newline at end of file diff --git a/.github/workflows/_lint.yml b/.github/workflows/_lint.yml index 6d63e11c6..97afe5ed6 100644 --- a/.github/workflows/_lint.yml +++ b/.github/workflows/_lint.yml @@ -9,7 +9,7 @@ on: description: "From which folder this pipeline executes" env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" # This env var allows us to get inline annotations when ruff has complaints. RUFF_OUTPUT_FORMAT: github @@ -50,12 +50,6 @@ jobs: working-directory: ${{ inputs.working-directory }} run: poetry check - - name: Check lock file - if: steps.changed-files.outputs.all - shell: bash - working-directory: ${{ inputs.working-directory }} - run: poetry check --lock - - name: Install dependencies if: steps.changed-files.outputs.all # Also installs dev/lint/test/typing dependencies, to ensure we have diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 64dfacd51..85b6ac353 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -9,7 +9,7 @@ on: description: "From which folder this pipeline executes" env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" jobs: build: @@ -39,12 +39,6 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_RO_TOKEN }} - - name: Check Lock - shell: bash - working-directory: ${{ inputs.working-directory }} - run: | - poetry check --lock - - name: Install dependencies shell: bash working-directory: ${{ inputs.working-directory }} diff --git a/.github/workflows/_test_langgraph.yml b/.github/workflows/_test_langgraph.yml index ba1cbd08e..6ff952e79 100644 --- a/.github/workflows/_test_langgraph.yml +++ b/.github/workflows/_test_langgraph.yml @@ -4,7 +4,7 @@ on: workflow_call: env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" jobs: build: diff --git a/.github/workflows/_test_release.yml b/.github/workflows/_test_release.yml index 46e065d33..21a8c9951 100644 --- a/.github/workflows/_test_release.yml +++ b/.github/workflows/_test_release.yml @@ -9,7 +9,7 @@ on: description: "From which folder this pipeline executes" env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" PYTHON_VERSION: "3.10" jobs: diff --git a/.github/workflows/_test_scheduler_kafka.yml b/.github/workflows/_test_scheduler_kafka.yml index 1f0edf420..8d0008871 100644 --- a/.github/workflows/_test_scheduler_kafka.yml +++ b/.github/workflows/_test_scheduler_kafka.yml @@ -4,7 +4,7 @@ on: workflow_call: env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" jobs: build: diff --git a/.github/workflows/baseline.yml b/.github/workflows/baseline.yml index 26a4f2ef0..4f3a33d73 100644 --- a/.github/workflows/baseline.yml +++ b/.github/workflows/baseline.yml @@ -8,7 +8,7 @@ on: - "libs/**" env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" jobs: benchmark: diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 0d0fc84b6..1222c4d4e 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -6,7 +6,7 @@ on: - "libs/**" env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" jobs: benchmark: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bed24785..6f359ad67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ concurrency: cancel-in-progress: true env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" jobs: changes: diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 490169946..ec17db77c 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" permissions: contents: read diff --git a/.github/workflows/link_check.yml b/.github/workflows/link_check.yml index a42b5fb73..f0d0c35b1 100644 --- a/.github/workflows/link_check.yml +++ b/.github/workflows/link_check.yml @@ -12,7 +12,7 @@ on: workflow_dispatch: env: - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" jobs: markdown-link-check: @@ -42,8 +42,8 @@ jobs: - name: Check README.md is in sync run: | - if ! diff -q README.md libs/langgraph/README.md >/dev/null; then - echo "README.md is out of sync with libs/langgraph/README.md" - diff -C 3 README.md libs/langgraph/README.md - exit 1 - fi + if ! diff -q README.md libs/langgraph/README.md >/dev/null; then + echo "README.md is out of sync with libs/langgraph/README.md" + diff -C 3 README.md libs/langgraph/README.md + exit 1 + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c93db70a3..8c1a07085 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ on: env: PYTHON_VERSION: "3.11" - POETRY_VERSION: "1.7.1" + POETRY_VERSION: "2.1.2" jobs: build: diff --git a/.github/workflows/run_notebooks.yml b/.github/workflows/run_notebooks.yml index 16abf71d7..8702f5f3c 100644 --- a/.github/workflows/run_notebooks.yml +++ b/.github/workflows/run_notebooks.yml @@ -9,7 +9,7 @@ on: type: string description: "JSON string of changed files" schedule: - - cron: '0 13 * * *' + - cron: "0 13 * * *" defaults: run: @@ -30,7 +30,7 @@ jobs: uses: "./.github/actions/poetry_setup" with: python-version: 3.11 - poetry-version: 1.7.1 + poetry-version: 2.1.2 cache-key: test-langgraph-notebooks - name: Install dependencies From d67a500cd976a0702d5b389ba4b6a774eb5105ea Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 8 Apr 2025 17:57:09 -0700 Subject: [PATCH 32/32] Fix --- .github/workflows/run_notebooks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_notebooks.yml b/.github/workflows/run_notebooks.yml index 8702f5f3c..07d52e93e 100644 --- a/.github/workflows/run_notebooks.yml +++ b/.github/workflows/run_notebooks.yml @@ -35,7 +35,7 @@ jobs: - name: Install dependencies run: | - poetry install --with test + poetry install --with test --no-root poetry run pip install jupyter - name: Start services