From 261cdf88a5aea13ea3c958af2e7c51d753b5a0b0 Mon Sep 17 00:00:00 2001 From: vbarda Date: Tue, 23 Jul 2024 16:43:26 -0400 Subject: [PATCH 001/287] langgraph: update get_state to handle nested subgraph state --- libs/langgraph/langgraph/checkpoint/base.py | 5 ++ libs/langgraph/langgraph/checkpoint/memory.py | 33 +++++++++ libs/langgraph/langgraph/checkpoint/sqlite.py | 43 ++++++++++++ libs/langgraph/langgraph/constants.py | 2 + libs/langgraph/langgraph/pregel/__init__.py | 67 +++++++++++++++---- libs/langgraph/langgraph/pregel/algo.py | 5 +- libs/langgraph/langgraph/pregel/types.py | 2 + libs/langgraph/tests/test_pregel.py | 44 ++++++------ 8 files changed, 166 insertions(+), 35 deletions(-) diff --git a/libs/langgraph/langgraph/checkpoint/base.py b/libs/langgraph/langgraph/checkpoint/base.py index b6694db89..fc53833c1 100644 --- a/libs/langgraph/langgraph/checkpoint/base.py +++ b/libs/langgraph/langgraph/checkpoint/base.py @@ -231,6 +231,11 @@ class BaseCheckpointSaver(ABC): """ raise NotImplementedError + def list_subgraph_checkpoints( + self, config: RunnableConfig + ) -> Iterator[CheckpointTuple]: + raise NotImplementedError + def put( self, config: RunnableConfig, diff --git a/libs/langgraph/langgraph/checkpoint/memory.py b/libs/langgraph/langgraph/checkpoint/memory.py index 72b8c93db..55246be4b 100644 --- a/libs/langgraph/langgraph/checkpoint/memory.py +++ b/libs/langgraph/langgraph/checkpoint/memory.py @@ -171,6 +171,39 @@ class MemorySaver(BaseCheckpointSaver): else None, ) + def list_subgraph_checkpoints( + self, config: RunnableConfig + ) -> Iterator[CheckpointTuple]: + thread_id_prefix = config["configurable"]["thread_id"] + matching_thread_ids = [ + key for key in self.storage.keys() if key.startswith(thread_id_prefix) + ] + for thread_id in matching_thread_ids: + ts = config["configurable"].get("thread_ts") + if not ts: + if checkpoints := self.storage[thread_id]: + ts = max(checkpoints.keys()) + + if saved := self.storage[thread_id].get(ts): + checkpoint, metadata, parent_ts = saved + writes = self.writes[(thread_id, ts)] + yield CheckpointTuple( + config={"configurable": {"thread_id": thread_id, "thread_ts": ts}}, + checkpoint=self.serde.loads(checkpoint), + metadata=self.serde.loads(metadata), + pending_writes=[ + (id, c, self.serde.loads(v)) for id, c, v in writes + ], + parent_config={ + "configurable": { + "thread_id": thread_id, + "thread_ts": parent_ts, + } + } + if parent_ts + else None, + ) + def put( self, config: RunnableConfig, diff --git a/libs/langgraph/langgraph/checkpoint/sqlite.py b/libs/langgraph/langgraph/checkpoint/sqlite.py index eee6e05c7..f6bf4b857 100644 --- a/libs/langgraph/langgraph/checkpoint/sqlite.py +++ b/libs/langgraph/langgraph/checkpoint/sqlite.py @@ -357,6 +357,49 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): ), ) + def list_subgraph_checkpoints( + self, config: RunnableConfig + ) -> Iterator[CheckpointTuple]: + with self.cursor(transaction=False) as cur: + if config["configurable"].get("thread_ts"): + cur.execute( + "SELECT thread_id, thread_ts, parent_ts, checkpoint, metadata FROM checkpoints WHERE thread_id LIKE ? || '%' AND thread_ts = ?", + ( + str(config["configurable"]["thread_id"]), + str(config["configurable"]["thread_ts"]), + ), + ) + else: + cur.execute( + """SELECT checkpoints.thread_id, checkpoints.thread_ts, checkpoints.parent_ts, checkpoints.checkpoint, checkpoints.metadata + FROM checkpoints + INNER JOIN ( + SELECT thread_id, MAX(thread_ts) as thread_ts + FROM checkpoints + WHERE thread_id LIKE ? || '%' + GROUP BY thread_id + ) latest_checkpoints + ON checkpoints.thread_id = latest_checkpoints.thread_id AND checkpoints.thread_ts = latest_checkpoints.thread_ts + ORDER BY checkpoints.thread_id, checkpoints.thread_ts DESC""", + (str(config["configurable"]["thread_id"]),), + ) + for thread_id, thread_ts, parent_ts, value, metadata in cur: + yield CheckpointTuple( + {"configurable": {"thread_id": thread_id, "thread_ts": thread_ts}}, + self.serde.loads(value), + self.serde.loads(metadata) if metadata is not None else {}, + ( + { + "configurable": { + "thread_id": thread_id, + "thread_ts": parent_ts, + } + } + if parent_ts + else None + ), + ) + def put( self, config: RunnableConfig, diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index f3aeb6a2e..edab6d6b7 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -18,6 +18,8 @@ RESERVED = { } TAG_HIDDEN = "langsmith:hidden" +THREAD_ID_SEPARATOR = "__" + START = "__start__" END = "__end__" diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index c0edcacc3..c04f3e9e8 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -59,6 +59,7 @@ from langgraph.channels.manager import ( ) from langgraph.checkpoint.base import ( BaseCheckpointSaver, + CheckpointTuple, copy_checkpoint, empty_checkpoint, ) @@ -68,6 +69,7 @@ from langgraph.constants import ( CONFIG_KEY_RESUMING, CONFIG_KEY_SEND, INTERRUPT, + THREAD_ID_SEPARATOR, ) from langgraph.errors import GraphRecursionError, InvalidUpdateError from langgraph.managed.base import ( @@ -350,12 +352,9 @@ class Pregel( if is_managed_value(v) } - def get_state(self, config: RunnableConfig) -> StateSnapshot: - """Get the current state of the graph.""" - if not self.checkpointer: - raise ValueError("No checkpointer set") - - saved = self.checkpointer.get_tuple(config) + def _prepare_state_snapshot( + self, saved: CheckpointTuple, config: RunnableConfig + ) -> StateSnapshot: checkpoint = saved.checkpoint if saved else empty_checkpoint() config = saved.config if saved else config with ChannelsManager( @@ -373,14 +372,58 @@ class Pregel( for_execution=False, ) return StateSnapshot( - read_channels(channels, self.stream_channels_asis), - tuple(name for name, _ in next_tasks), - saved.config if saved else config, - saved.metadata if saved else None, - saved.checkpoint["ts"] if saved else None, - saved.parent_config if saved else None, + values=read_channels(channels, self.stream_channels_asis), + next=tuple(name for name, _ in next_tasks), + config=saved.config if saved else config, + metadata=saved.metadata if saved else None, + created_at=saved.checkpoint["ts"] if saved else None, + parent_config=saved.parent_config if saved else None, ) + @staticmethod + def _assemble_state_snapshot_hierarchy( + root_thread_id: str, subgraph_state_snapshots: dict[str, StateSnapshot] + ) -> StateSnapshot: + thread_ids_to_visit = sorted( + subgraph_state_snapshots.keys(), + key=lambda x: len(x.split(THREAD_ID_SEPARATOR)), + ) + while thread_ids_to_visit: + thread_id = thread_ids_to_visit.pop() + state_snapshot = subgraph_state_snapshots[thread_id] + *path, subgraph_node = thread_id.split(THREAD_ID_SEPARATOR) + parent_thread_id = THREAD_ID_SEPARATOR.join(path) + if parent_thread_id and THREAD_ID_SEPARATOR in parent_thread_id: + parent_subgraph_snapshots = ( + subgraph_state_snapshots[parent_thread_id].subgraph_state_snapshots + or {} + ) + parent_subgraph_snapshots[subgraph_node] = state_snapshot + subgraph_state_snapshots[parent_thread_id] = subgraph_state_snapshots[ + parent_thread_id + ]._replace(subgraph_state_snapshots=parent_subgraph_snapshots) + + state_snapshot = subgraph_state_snapshots.pop(root_thread_id) + return state_snapshot + + def get_state(self, config: RunnableConfig) -> StateSnapshot: + """Get the current state of the graph.""" + if not self.checkpointer: + raise ValueError("No checkpointer set") + + subgraph_state_snapshots: dict[str, StateSnapshot] = { + checkpoint.config["configurable"][ + "thread_id" + ]: self._prepare_state_snapshot(checkpoint, config) + for checkpoint in self.checkpointer.list_subgraph_checkpoints(config) + } + + thread_id = config["configurable"]["thread_id"] + state_snapshot = self._assemble_state_snapshot_hierarchy( + thread_id, subgraph_state_snapshots + ) + return state_snapshot + async def aget_state(self, config: RunnableConfig) -> StateSnapshot: """Get the current state of the graph.""" if not self.checkpointer: diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 13ce7e183..00d9e7809 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -36,6 +36,7 @@ from langgraph.constants import ( RESERVED, TAG_HIDDEN, TASKS, + THREAD_ID_SEPARATOR, Send, ) from langgraph.errors import EmptyChannelError, InvalidUpdateError @@ -345,7 +346,9 @@ def prepare_next_tasks( if parent_thread_id := config.get("configurable", {}).get( "thread_id" ): - thread_id: Optional[str] = f"{parent_thread_id}-{name}" + thread_id: Optional[ + str + ] = f"{parent_thread_id}{THREAD_ID_SEPARATOR}{name}" else: thread_id = None writes = deque() diff --git a/libs/langgraph/langgraph/pregel/types.py b/libs/langgraph/langgraph/pregel/types.py index 19d3c3301..d0648cf3b 100644 --- a/libs/langgraph/langgraph/pregel/types.py +++ b/libs/langgraph/langgraph/pregel/types.py @@ -85,6 +85,8 @@ class StateSnapshot(NamedTuple): """Timestamp of snapshot creation""" parent_config: Optional[RunnableConfig] = None """Config used to fetch the parent snapshot, if any""" + subgraph_state_snapshots: Optional[dict[str, "StateSnapshot"]] = None + """State snapshots of subgraphs represented as a mapping from thread ID suffix to snapshot.""" All = Literal["*"] diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index db902eca0..b218fabda 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -568,53 +568,53 @@ def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> None: ) # start execution, stop at inbox - assert app.invoke(2, {"configurable": {"thread_id": 1}}) is None + assert app.invoke(2, {"configurable": {"thread_id": "1"}}) is None # inbox == 3 - checkpoint = memory.get({"configurable": {"thread_id": 1}}) + checkpoint = memory.get({"configurable": {"thread_id": "1"}}) assert checkpoint is not None assert checkpoint["channel_values"]["inbox"] == 3 # resume execution, finish - assert app.invoke(None, {"configurable": {"thread_id": 1}}) == 4 + assert app.invoke(None, {"configurable": {"thread_id": "1"}}) == 4 # start execution again, stop at inbox - assert app.invoke(20, {"configurable": {"thread_id": 1}}) is None + assert app.invoke(20, {"configurable": {"thread_id": "1"}}) is None # inbox == 21 - checkpoint = memory.get({"configurable": {"thread_id": 1}}) + checkpoint = memory.get({"configurable": {"thread_id": "1"}}) assert checkpoint is not None assert checkpoint["channel_values"]["inbox"] == 21 # send a new value in, interrupting the previous execution - assert app.invoke(3, {"configurable": {"thread_id": 1}}) is None - assert app.invoke(None, {"configurable": {"thread_id": 1}}) == 5 + assert app.invoke(3, {"configurable": {"thread_id": "1"}}) is None + assert app.invoke(None, {"configurable": {"thread_id": "1"}}) == 5 # start execution again, stopping at inbox - assert app.invoke(20, {"configurable": {"thread_id": 2}}) is None + assert app.invoke(20, {"configurable": {"thread_id": "2"}}) is None # inbox == 21 - snapshot = app.get_state({"configurable": {"thread_id": 2}}) + snapshot = app.get_state({"configurable": {"thread_id": "2"}}) assert snapshot.values["inbox"] == 21 assert snapshot.next == ("two",) # update the state, resume - app.update_state({"configurable": {"thread_id": 2}}, 25, as_node="one") - assert app.invoke(None, {"configurable": {"thread_id": 2}}) == 26 + app.update_state({"configurable": {"thread_id": "2"}}, 25, as_node="one") + assert app.invoke(None, {"configurable": {"thread_id": "2"}}) == 26 # no pending tasks - snapshot = app.get_state({"configurable": {"thread_id": 2}}) + snapshot = app.get_state({"configurable": {"thread_id": "2"}}) assert snapshot.next == () # list history - thread1 = {"configurable": {"thread_id": 1}} + thread1 = {"configurable": {"thread_id": "1"}} assert [c for c in app.get_state_history(thread1)] == [ StateSnapshot( values={"inbox": 4, "output": 5, "input": 3}, next=(), config={ "configurable": { - "thread_id": 1, + "thread_id": "1", "thread_ts": AnyStr(), } }, @@ -627,7 +627,7 @@ def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> None: next=("two",), config={ "configurable": { - "thread_id": 1, + "thread_id": "1", "thread_ts": AnyStr(), } }, @@ -640,7 +640,7 @@ def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> None: next=("one",), config={ "configurable": { - "thread_id": 1, + "thread_id": "1", "thread_ts": AnyStr(), } }, @@ -653,7 +653,7 @@ def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> None: next=("two",), config={ "configurable": { - "thread_id": 1, + "thread_id": "1", "thread_ts": AnyStr(), } }, @@ -666,7 +666,7 @@ def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> None: next=("one",), config={ "configurable": { - "thread_id": 1, + "thread_id": "1", "thread_ts": AnyStr(), } }, @@ -679,7 +679,7 @@ def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> None: next=(), config={ "configurable": { - "thread_id": 1, + "thread_id": "1", "thread_ts": AnyStr(), } }, @@ -692,7 +692,7 @@ def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> None: next=("two",), config={ "configurable": { - "thread_id": 1, + "thread_id": "1", "thread_ts": AnyStr(), } }, @@ -705,7 +705,7 @@ def test_invoke_two_processes_in_out_interrupt(mocker: MockerFixture) -> None: next=("one",), config={ "configurable": { - "thread_id": 1, + "thread_id": "1", "thread_ts": AnyStr(), } }, @@ -1080,7 +1080,7 @@ def test_pending_writes_resume(checkpointer: BaseCheckpointSaver) -> None: builder.add_edge(START, "two") graph = builder.compile(checkpointer=checkpointer) - thread1: RunnableConfig = {"configurable": {"thread_id": 1}} + thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} with pytest.raises(ConnectionError, match="I'm not good"): graph.invoke({"value": 1}, thread1) From 92ae8f48177ddc75e877881bbe82f62ad21868d7 Mon Sep 17 00:00:00 2001 From: vbarda Date: Tue, 23 Jul 2024 19:51:44 -0400 Subject: [PATCH 002/287] async methods --- .../langgraph/checkpoint/aiosqlite.py | 4 +- libs/langgraph/langgraph/checkpoint/memory.py | 13 ++++ libs/langgraph/langgraph/pregel/__init__.py | 68 ++++++++++++------- libs/langgraph/tests/test_pregel_async.py | 4 +- 4 files changed, 59 insertions(+), 30 deletions(-) diff --git a/libs/langgraph/langgraph/checkpoint/aiosqlite.py b/libs/langgraph/langgraph/checkpoint/aiosqlite.py index 21db080d1..401982fdf 100644 --- a/libs/langgraph/langgraph/checkpoint/aiosqlite.py +++ b/libs/langgraph/langgraph/checkpoint/aiosqlite.py @@ -352,7 +352,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): ) -> AsyncIterator[CheckpointTuple]: async with self.conn.cursor() as cur: if config["configurable"].get("thread_ts"): - cur.execute( + await cur.execute( "SELECT thread_id, thread_ts, parent_ts, checkpoint, metadata FROM checkpoints WHERE thread_id LIKE ? || '%' AND thread_ts = ?", ( str(config["configurable"]["thread_id"]), @@ -360,7 +360,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): ), ) else: - cur.execute( + await cur.execute( """SELECT checkpoints.thread_id, checkpoints.thread_ts, checkpoints.parent_ts, checkpoints.checkpoint, checkpoints.metadata FROM checkpoints INNER JOIN ( diff --git a/libs/langgraph/langgraph/checkpoint/memory.py b/libs/langgraph/langgraph/checkpoint/memory.py index 55246be4b..348aacdf0 100644 --- a/libs/langgraph/langgraph/checkpoint/memory.py +++ b/libs/langgraph/langgraph/checkpoint/memory.py @@ -311,6 +311,19 @@ class MemorySaver(BaseCheckpointSaver): else: break + async def alist_subgraph_checkpoints( + self, config: RunnableConfig + ) -> AsyncIterator[CheckpointTuple]: + loop = asyncio.get_running_loop() + iter = await loop.run_in_executor(None, self.list_subgraph_checkpoints, config) + while True: + # handling StopIteration exception inside coroutine won't work + # as expected, so using next() with default value to break the loop + if item := await loop.run_in_executor(None, next, iter, None): + yield item + else: + break + async def aput( self, config: RunnableConfig, diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index c04f3e9e8..e3241eae4 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -380,6 +380,34 @@ class Pregel( parent_config=saved.parent_config if saved else None, ) + async def _prepare_state_snapshot_async( + self, saved: CheckpointTuple, config: RunnableConfig + ) -> StateSnapshot: + checkpoint = saved.checkpoint if saved else empty_checkpoint() + config = saved.config if saved else config + async with AsyncChannelsManager( + self.channels, checkpoint, config + ) as channels, AsyncManagedValuesManager( + self.managed_values_dict, ensure_config(config), self + ) as managed: + next_tasks = prepare_next_tasks( + checkpoint, + self.nodes, + channels, + managed, + config, + -1, + for_execution=False, + ) + return StateSnapshot( + values=read_channels(channels, self.stream_channels_asis), + next=tuple(name for name, _ in next_tasks), + config=saved.config if saved else config, + metadata=saved.metadata if saved else None, + created_at=saved.checkpoint["ts"] if saved else None, + parent_config=saved.parent_config if saved else None, + ) + @staticmethod def _assemble_state_snapshot_hierarchy( root_thread_id: str, subgraph_state_snapshots: dict[str, StateSnapshot] @@ -403,7 +431,9 @@ class Pregel( parent_thread_id ]._replace(subgraph_state_snapshots=parent_subgraph_snapshots) - state_snapshot = subgraph_state_snapshots.pop(root_thread_id) + state_snapshot = subgraph_state_snapshots.pop(root_thread_id, None) + if state_snapshot is None: + raise ValueError(f"Missing snapshot for thread ID '{root_thread_id}'") return state_snapshot def get_state(self, config: RunnableConfig) -> StateSnapshot: @@ -429,32 +459,18 @@ class Pregel( if not self.checkpointer: raise ValueError("No checkpointer set") - saved = await self.checkpointer.aget_tuple(config) - checkpoint = saved.checkpoint if saved else empty_checkpoint() + subgraph_state_snapshots: dict[str, StateSnapshot] = { + checkpoint.config["configurable"][ + "thread_id" + ]: await self._prepare_state_snapshot_async(checkpoint, config) + async for checkpoint in self.checkpointer.alist_subgraph_checkpoints(config) + } - config = saved.config if saved else config - async with AsyncChannelsManager( - self.channels, checkpoint, config - ) as channels, AsyncManagedValuesManager( - self.managed_values_dict, ensure_config(config), self - ) as managed: - next_tasks = prepare_next_tasks( - checkpoint, - self.nodes, - channels, - managed, - config, - -1, - for_execution=False, - ) - return StateSnapshot( - read_channels(channels, self.stream_channels_asis), - tuple(name for name, _ in next_tasks), - saved.config if saved else config, - saved.metadata if saved else None, - saved.checkpoint["ts"] if saved else None, - saved.parent_config if saved else None, - ) + thread_id = config["configurable"]["thread_id"] + state_snapshot = self._assemble_state_snapshot_hierarchy( + thread_id, subgraph_state_snapshots + ) + return state_snapshot def get_state_history( self, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 76a182d69..7d88f286e 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1204,7 +1204,7 @@ async def test_pending_writes_resume(checkpointer: BaseCheckpointSaver) -> None: builder.add_edge(START, "two") graph = builder.compile(checkpointer=checkpointer) - thread1: RunnableConfig = {"configurable": {"thread_id": 1}} + thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} with pytest.raises(ValueError, match="I'm not good"): await graph.ainvoke({"value": 1}, thread1) @@ -6079,7 +6079,7 @@ async def test_nested_graph(snapshot: SnapshotAssertion) -> None: assert times_called == 1 -@pytest.mark.repeat(10) +# @pytest.mark.repeat(10) @pytest.mark.parametrize( "checkpointer_fct", [ From e615aabf14385152c021ce1686cdff4318bc0aff Mon Sep 17 00:00:00 2001 From: vbarda Date: Tue, 23 Jul 2024 20:14:53 -0400 Subject: [PATCH 003/287] cleanup names + make subgraph state optional --- libs/langgraph/langgraph/pregel/__init__.py | 49 +++++++++++++-------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index e3241eae4..511a5fd9c 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -410,65 +410,76 @@ class Pregel( @staticmethod def _assemble_state_snapshot_hierarchy( - root_thread_id: str, subgraph_state_snapshots: dict[str, StateSnapshot] + root_thread_id: str, thread_id_to_state_snapshots: dict[str, StateSnapshot] ) -> StateSnapshot: thread_ids_to_visit = sorted( - subgraph_state_snapshots.keys(), + thread_id_to_state_snapshots.keys(), key=lambda x: len(x.split(THREAD_ID_SEPARATOR)), ) while thread_ids_to_visit: thread_id = thread_ids_to_visit.pop() - state_snapshot = subgraph_state_snapshots[thread_id] + state_snapshot = thread_id_to_state_snapshots[thread_id] *path, subgraph_node = thread_id.split(THREAD_ID_SEPARATOR) parent_thread_id = THREAD_ID_SEPARATOR.join(path) - if parent_thread_id and THREAD_ID_SEPARATOR in parent_thread_id: - parent_subgraph_snapshots = ( - subgraph_state_snapshots[parent_thread_id].subgraph_state_snapshots - or {} - ) - parent_subgraph_snapshots[subgraph_node] = state_snapshot - subgraph_state_snapshots[parent_thread_id] = subgraph_state_snapshots[ + if parent_thread_id and (parent_state_snapshot := thread_id_to_state_snapshots.get(parent_thread_id)): + parent_subgraph_snapshots = { + **(parent_state_snapshot.subgraph_state_snapshots or {}), + subgraph_node: state_snapshot + } + thread_id_to_state_snapshots[parent_thread_id] = thread_id_to_state_snapshots[ parent_thread_id ]._replace(subgraph_state_snapshots=parent_subgraph_snapshots) - state_snapshot = subgraph_state_snapshots.pop(root_thread_id, None) + state_snapshot = thread_id_to_state_snapshots.pop(root_thread_id, None) if state_snapshot is None: raise ValueError(f"Missing snapshot for thread ID '{root_thread_id}'") return state_snapshot - def get_state(self, config: RunnableConfig) -> StateSnapshot: + def get_state(self, config: RunnableConfig, *, include_subgraph_state: bool = False) -> StateSnapshot: """Get the current state of the graph.""" if not self.checkpointer: raise ValueError("No checkpointer set") - subgraph_state_snapshots: dict[str, StateSnapshot] = { + if include_subgraph_state: + checkpoint_tuples = self.checkpointer.list_subgraph_checkpoints(config) + else: + checkpoint_tuples = iter([self.checkpointer.get_tuple(config)]) + + thread_id_to_state_snapshots: dict[str, StateSnapshot] = { checkpoint.config["configurable"][ "thread_id" ]: self._prepare_state_snapshot(checkpoint, config) - for checkpoint in self.checkpointer.list_subgraph_checkpoints(config) + for checkpoint in checkpoint_tuples } thread_id = config["configurable"]["thread_id"] state_snapshot = self._assemble_state_snapshot_hierarchy( - thread_id, subgraph_state_snapshots + thread_id, thread_id_to_state_snapshots ) return state_snapshot - async def aget_state(self, config: RunnableConfig) -> StateSnapshot: + async def aget_state(self, config: RunnableConfig, *, include_subgraph_state: bool = False) -> StateSnapshot: """Get the current state of the graph.""" if not self.checkpointer: raise ValueError("No checkpointer set") - subgraph_state_snapshots: dict[str, StateSnapshot] = { + if include_subgraph_state: + checkpoint_tuples = self.checkpointer.alist_subgraph_checkpoints(config) + else: + async def alist_checkpoints(): + yield await self.checkpointer.aget_tuple(config) + checkpoint_tuples = alist_checkpoints() + + thread_id_to_state_snapshots: dict[str, StateSnapshot] = { checkpoint.config["configurable"][ "thread_id" ]: await self._prepare_state_snapshot_async(checkpoint, config) - async for checkpoint in self.checkpointer.alist_subgraph_checkpoints(config) + async for checkpoint in checkpoint_tuples } thread_id = config["configurable"]["thread_id"] state_snapshot = self._assemble_state_snapshot_hierarchy( - thread_id, subgraph_state_snapshots + thread_id, thread_id_to_state_snapshots ) return state_snapshot From b43ef6440f2cc6a2f5801131d2aa783dc3c6244a Mon Sep 17 00:00:00 2001 From: vbarda Date: Tue, 23 Jul 2024 20:38:20 -0400 Subject: [PATCH 004/287] tests --- .../langgraph/checkpoint/aiosqlite.py | 1 + libs/langgraph/langgraph/checkpoint/base.py | 2 + libs/langgraph/langgraph/checkpoint/memory.py | 2 + libs/langgraph/langgraph/checkpoint/sqlite.py | 1 + libs/langgraph/langgraph/pregel/__init__.py | 24 +- libs/langgraph/tests/test_pregel.py | 354 +++++++++++++++++ libs/langgraph/tests/test_pregel_async.py | 365 +++++++++++++++++- 7 files changed, 742 insertions(+), 7 deletions(-) diff --git a/libs/langgraph/langgraph/checkpoint/aiosqlite.py b/libs/langgraph/langgraph/checkpoint/aiosqlite.py index 401982fdf..e0e982537 100644 --- a/libs/langgraph/langgraph/checkpoint/aiosqlite.py +++ b/libs/langgraph/langgraph/checkpoint/aiosqlite.py @@ -350,6 +350,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): async def alist_subgraph_checkpoints( self, config: RunnableConfig ) -> AsyncIterator[CheckpointTuple]: + # TODO: docstring async with self.conn.cursor() as cur: if config["configurable"].get("thread_ts"): await cur.execute( diff --git a/libs/langgraph/langgraph/checkpoint/base.py b/libs/langgraph/langgraph/checkpoint/base.py index f2b0b0f63..d2b0df3da 100644 --- a/libs/langgraph/langgraph/checkpoint/base.py +++ b/libs/langgraph/langgraph/checkpoint/base.py @@ -234,6 +234,7 @@ class BaseCheckpointSaver(ABC): def list_subgraph_checkpoints( self, config: RunnableConfig ) -> Iterator[CheckpointTuple]: + # TODO: docstring raise NotImplementedError def put( @@ -331,6 +332,7 @@ class BaseCheckpointSaver(ABC): async def alist_subgraph_checkpoints( self, config: RunnableConfig ) -> AsyncIterator[CheckpointTuple]: + # TODO: docstring raise NotImplementedError async def aput( diff --git a/libs/langgraph/langgraph/checkpoint/memory.py b/libs/langgraph/langgraph/checkpoint/memory.py index 348aacdf0..8160de238 100644 --- a/libs/langgraph/langgraph/checkpoint/memory.py +++ b/libs/langgraph/langgraph/checkpoint/memory.py @@ -174,6 +174,7 @@ class MemorySaver(BaseCheckpointSaver): def list_subgraph_checkpoints( self, config: RunnableConfig ) -> Iterator[CheckpointTuple]: + # TODO: docstring thread_id_prefix = config["configurable"]["thread_id"] matching_thread_ids = [ key for key in self.storage.keys() if key.startswith(thread_id_prefix) @@ -314,6 +315,7 @@ class MemorySaver(BaseCheckpointSaver): async def alist_subgraph_checkpoints( self, config: RunnableConfig ) -> AsyncIterator[CheckpointTuple]: + # TODO: docstring loop = asyncio.get_running_loop() iter = await loop.run_in_executor(None, self.list_subgraph_checkpoints, config) while True: diff --git a/libs/langgraph/langgraph/checkpoint/sqlite.py b/libs/langgraph/langgraph/checkpoint/sqlite.py index f6bf4b857..6578764ca 100644 --- a/libs/langgraph/langgraph/checkpoint/sqlite.py +++ b/libs/langgraph/langgraph/checkpoint/sqlite.py @@ -360,6 +360,7 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): def list_subgraph_checkpoints( self, config: RunnableConfig ) -> Iterator[CheckpointTuple]: + # TODO: docstring with self.cursor(transaction=False) as cur: if config["configurable"].get("thread_ts"): cur.execute( diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 511a5fd9c..f1ab7d5b6 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -421,21 +421,29 @@ class Pregel( state_snapshot = thread_id_to_state_snapshots[thread_id] *path, subgraph_node = thread_id.split(THREAD_ID_SEPARATOR) parent_thread_id = THREAD_ID_SEPARATOR.join(path) - if parent_thread_id and (parent_state_snapshot := thread_id_to_state_snapshots.get(parent_thread_id)): + if parent_thread_id and ( + parent_state_snapshot := thread_id_to_state_snapshots.get( + parent_thread_id + ) + ): parent_subgraph_snapshots = { **(parent_state_snapshot.subgraph_state_snapshots or {}), - subgraph_node: state_snapshot + subgraph_node: state_snapshot, } - thread_id_to_state_snapshots[parent_thread_id] = thread_id_to_state_snapshots[ + thread_id_to_state_snapshots[ parent_thread_id - ]._replace(subgraph_state_snapshots=parent_subgraph_snapshots) + ] = thread_id_to_state_snapshots[parent_thread_id]._replace( + subgraph_state_snapshots=parent_subgraph_snapshots + ) state_snapshot = thread_id_to_state_snapshots.pop(root_thread_id, None) if state_snapshot is None: raise ValueError(f"Missing snapshot for thread ID '{root_thread_id}'") return state_snapshot - def get_state(self, config: RunnableConfig, *, include_subgraph_state: bool = False) -> StateSnapshot: + def get_state( + self, config: RunnableConfig, *, include_subgraph_state: bool = False + ) -> StateSnapshot: """Get the current state of the graph.""" if not self.checkpointer: raise ValueError("No checkpointer set") @@ -458,7 +466,9 @@ class Pregel( ) return state_snapshot - async def aget_state(self, config: RunnableConfig, *, include_subgraph_state: bool = False) -> StateSnapshot: + async def aget_state( + self, config: RunnableConfig, *, include_subgraph_state: bool = False + ) -> StateSnapshot: """Get the current state of the graph.""" if not self.checkpointer: raise ValueError("No checkpointer set") @@ -466,8 +476,10 @@ class Pregel( if include_subgraph_state: checkpoint_tuples = self.checkpointer.alist_subgraph_checkpoints(config) else: + async def alist_checkpoints(): yield await self.checkpointer.aget_tuple(config) + checkpoint_tuples = alist_checkpoints() thread_id_to_state_snapshots: dict[str, StateSnapshot] = { diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 907fdb0f9..fea1e29b8 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -8674,6 +8674,360 @@ def test_doubly_nested_graph_interrupts(checkpointer: BaseCheckpointSaver) -> No checkpointer.__exit__(None, None, None) +@pytest.mark.parametrize( + "checkpointer_fct", + [ + lambda: MemorySaverAssertImmutable(put_sleep=0.2), + lambda: SqliteSaver.from_conn_string(":memory:"), + ], + ids=[ + "memory", + "sqlite", + ], +) +def test_nested_graph_state( + checkpointer_fct: Callable[[], BaseCheckpointSaver], +) -> None: + try: + checkpointer = checkpointer_fct() + + class InnerState(TypedDict): + my_key: str + my_other_key: str + + def inner_1(state: InnerState): + return { + "my_key": state["my_key"] + " here", + "my_other_key": state["my_key"], + } + + def inner_2(state: InnerState): + return { + "my_key": state["my_key"] + " and there", + "my_other_key": state["my_key"], + } + + 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 + + def outer_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + def outer_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("outer_1", outer_1) + graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) + graph.add_node("outer_2", outer_2) + graph.set_entry_point("outer_1") + graph.add_edge("outer_1", "inner") + graph.add_edge("inner", "outer_2") + graph.set_finish_point("outer_2") + + app = graph.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "1"}} + app.invoke({"my_key": "my value"}, config, debug=True) + # test state w/ nested subgraph state (right after interrupt) + assert app.get_state(config, include_subgraph_state=False) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots=None, + ) + assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": {"thread_id": "1__inner", "thread_ts": AnyStr()} + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1__inner", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots=None, + ) + }, + ) + app.invoke(None, config, debug=True) + # test state w/ nested subgraph state (after resuming from interrupt) + assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": { + "outer_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": {"thread_id": "1__inner", "thread_ts": AnyStr()} + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1__inner", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots=None, + ) + }, + ) + finally: + if hasattr(checkpointer, "__exit__"): + checkpointer.__exit__(None, None, None) + + +@pytest.mark.parametrize( + "checkpointer", + [ + MemorySaverAssertImmutable(), + SqliteSaver.from_conn_string(":memory:"), + ], + ids=[ + "memory", + "sqlite", + ], +) +def test_doubly_nested_graph_state(checkpointer: BaseCheckpointSaver) -> None: + try: + + class State(TypedDict): + my_key: str + + class ChildState(TypedDict): + my_key: str + + class GrandChildState(TypedDict): + my_key: str + + def grandchild_1(state: ChildState): + return {"my_key": state["my_key"] + " here"} + + def grandchild_2(state: ChildState): + return { + "my_key": state["my_key"] + " and there", + } + + grandchild = StateGraph(GrandChildState) + grandchild.add_node("grandchild_1", grandchild_1) + grandchild.add_node("grandchild_2", grandchild_2) + grandchild.add_edge("grandchild_1", "grandchild_2") + grandchild.set_entry_point("grandchild_1") + grandchild.set_finish_point("grandchild_2") + + child = StateGraph(ChildState) + child.add_node("child_1", grandchild.compile(interrupt_before=["grandchild_2"])) + child.set_entry_point("child_1") + child.set_finish_point("child_1") + + def parent_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + def parent_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("parent_1", parent_1) + graph.add_node("child", child.compile()) + graph.add_node("parent_2", parent_2) + graph.set_entry_point("parent_1") + graph.add_edge("parent_1", "child") + graph.add_edge("child", "parent_2") + graph.set_finish_point("parent_2") + + app = graph.compile(checkpointer=checkpointer) + + # test invoke w/ nested interrupt + config = {"configurable": {"thread_id": "1"}} + app.invoke({"my_key": "my value"}, config, debug=True) + assert app.get_state(config) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("child",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots=None, + ) + assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("child",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots={ + "child": StateSnapshot( + values={"my_key": "hi my value"}, + next=(), + config={ + "configurable": {"thread_id": "1__child", "thread_ts": AnyStr()} + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1__child", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots={ + "child_1": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": { + "thread_id": "1__child__child_1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "grandchild_1": {"my_key": "hi my value here"} + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1__child__child_1", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + }, + ) + app.invoke(None, config, debug=True) + assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": { + "parent_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots={ + "child": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": {"thread_id": "1__child", "thread_ts": AnyStr()} + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1__child", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots={ + "child_1": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1__child__child_1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "grandchild_2": { + "my_key": "hi my value here and there" + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1__child__child_1", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + }, + ) + + finally: + if hasattr(checkpointer, "__exit__"): + checkpointer.__exit__(None, None, None) + + def test_repeat_condition(snapshot: SnapshotAssertion) -> None: class AgentState(TypedDict): hello: str diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 7d88f286e..2e40dc0b1 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -6079,7 +6079,7 @@ async def test_nested_graph(snapshot: SnapshotAssertion) -> None: assert times_called == 1 -# @pytest.mark.repeat(10) +@pytest.mark.repeat(10) @pytest.mark.parametrize( "checkpointer_fct", [ @@ -7209,6 +7209,369 @@ async def test_doubly_nested_graph_interrupts( await checkpointer.__aexit__(None, None, None) +@pytest.mark.parametrize( + "checkpointer", + [ + MemorySaverAssertImmutable(), + AsyncSqliteSaver.from_conn_string(":memory:"), + ], + ids=[ + "memory", + "sqlite", + ], +) +async def test_nested_graph_state(checkpointer: BaseCheckpointSaver) -> None: + try: + + class InnerState(TypedDict): + my_key: str + my_other_key: str + + async def inner_1(state: InnerState): + return { + "my_key": state["my_key"] + " here", + "my_other_key": state["my_key"], + } + + async def inner_2(state: InnerState): + return { + "my_key": state["my_key"] + " and there", + "my_other_key": state["my_key"], + } + + 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 + + async def outer_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + async def outer_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("outer_1", outer_1) + graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) + graph.add_node("outer_2", outer_2) + graph.set_entry_point("outer_1") + graph.add_edge("outer_1", "inner") + graph.add_edge("inner", "outer_2") + graph.set_finish_point("outer_2") + + app = graph.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "1"}} + await app.ainvoke({"my_key": "my value"}, config, debug=True) + # test state w/ nested subgraph state (right after interrupt) + assert await app.aget_state( + config, include_subgraph_state=False + ) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots=None, + ) + assert await app.aget_state( + config, include_subgraph_state=True + ) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": {"thread_id": "1__inner", "thread_ts": AnyStr()} + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1__inner", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots=None, + ) + }, + ) + await app.ainvoke(None, config, debug=True) + # test state w/ nested subgraph state (after resuming from interrupt) + assert await app.aget_state( + config, include_subgraph_state=True + ) == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": { + "outer_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": {"thread_id": "1__inner", "thread_ts": AnyStr()} + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1__inner", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots=None, + ) + }, + ) + finally: + if hasattr(checkpointer, "__aexit__"): + await checkpointer.__aexit__(None, None, None) + + +@pytest.mark.parametrize( + "checkpointer", + [ + MemorySaverAssertImmutable(), + AsyncSqliteSaver.from_conn_string(":memory:"), + ], + ids=[ + "memory", + "sqlite", + ], +) +async def test_doubly_nested_graph_state( + checkpointer: BaseCheckpointSaver, +) -> None: + try: + + class State(TypedDict): + my_key: str + + class ChildState(TypedDict): + my_key: str + + class GrandChildState(TypedDict): + my_key: str + + async def grandchild_1(state: ChildState): + return {"my_key": state["my_key"] + " here"} + + async def grandchild_2(state: ChildState): + return { + "my_key": state["my_key"] + " and there", + } + + grandchild = StateGraph(GrandChildState) + grandchild.add_node("grandchild_1", grandchild_1) + grandchild.add_node("grandchild_2", grandchild_2) + grandchild.add_edge("grandchild_1", "grandchild_2") + grandchild.set_entry_point("grandchild_1") + grandchild.set_finish_point("grandchild_2") + + child = StateGraph(ChildState) + child.add_node("child_1", grandchild.compile(interrupt_before=["grandchild_2"])) + child.set_entry_point("child_1") + child.set_finish_point("child_1") + + async def parent_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + async def parent_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("parent_1", parent_1) + graph.add_node("child", child.compile()) + graph.add_node("parent_2", parent_2) + graph.set_entry_point("parent_1") + graph.add_edge("parent_1", "child") + graph.add_edge("child", "parent_2") + graph.set_finish_point("parent_2") + + app = graph.compile(checkpointer=checkpointer) + + # test invoke w/ nested interrupt + config = {"configurable": {"thread_id": "1"}} + await app.ainvoke({"my_key": "my value"}, config, debug=True) + assert await app.aget_state(config) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("child",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots=None, + ) + assert await app.aget_state( + config, include_subgraph_state=True + ) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("child",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots={ + "child": StateSnapshot( + values={"my_key": "hi my value"}, + next=(), + config={ + "configurable": {"thread_id": "1__child", "thread_ts": AnyStr()} + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1__child", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots={ + "child_1": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": { + "thread_id": "1__child__child_1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "grandchild_1": {"my_key": "hi my value here"} + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1__child__child_1", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + }, + ) + await app.ainvoke(None, config, debug=True) + assert await app.aget_state( + config, include_subgraph_state=True + ) == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": { + "parent_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + subgraph_state_snapshots={ + "child": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": {"thread_id": "1__child", "thread_ts": AnyStr()} + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1__child", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots={ + "child_1": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1__child__child_1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "grandchild_2": { + "my_key": "hi my value here and there" + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1__child__child_1", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + }, + ) + + finally: + if hasattr(checkpointer, "__aexit__"): + await checkpointer.__aexit__(None, None, None) + + async def test_checkpoint_metadata() -> None: """This test verifies that a run's configurable fields are merged with the previous checkpoint config for each step in the run. From ae696d4f3039cb8286bf01325e6970da5206fc63 Mon Sep 17 00:00:00 2001 From: vbarda Date: Tue, 23 Jul 2024 21:40:04 -0400 Subject: [PATCH 005/287] add sync history --- libs/langgraph/langgraph/pregel/__init__.py | 59 ++++-- libs/langgraph/tests/test_pregel.py | 215 ++++++++++++++++++++ 2 files changed, 253 insertions(+), 21 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index f1ab7d5b6..6287c713b 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -502,6 +502,7 @@ class Pregel( filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, + include_subgraph_state: bool = False, ) -> Iterator[StateSnapshot]: """Get the history of the state of the graph.""" if not self.checkpointer: @@ -514,28 +515,44 @@ class Pregel( for config, checkpoint, metadata, parent_config, _ in self.checkpointer.list( config, before=before, limit=limit, filter=filter ): - with ChannelsManager( - self.channels, checkpoint, config - ) as channels, ManagedValuesManager( - self.managed_values_dict, ensure_config(config), self - ) as managed: - next_tasks = prepare_next_tasks( - checkpoint, - self.nodes, - channels, - managed, - config, - -1, - for_execution=False, - ) - yield StateSnapshot( - read_channels(channels, self.stream_channels_asis), - tuple(name for name, _ in next_tasks), - config, - metadata, - checkpoint["ts"], - parent_config, + # is there a way to do this more efficiently? + if include_subgraph_state: + checkpoint_tuples = self.checkpointer.list_subgraph_checkpoints(config) + + thread_id_to_state_snapshots: dict[str, StateSnapshot] = { + checkpoint.config["configurable"][ + "thread_id" + ]: self._prepare_state_snapshot(checkpoint, config) + for checkpoint in checkpoint_tuples + } + thread_id = config["configurable"]["thread_id"] + state_snapshot = self._assemble_state_snapshot_hierarchy( + thread_id, thread_id_to_state_snapshots ) + yield state_snapshot + else: + with ChannelsManager( + self.channels, checkpoint, config + ) as channels, ManagedValuesManager( + self.managed_values_dict, ensure_config(config), self + ) as managed: + next_tasks = prepare_next_tasks( + checkpoint, + self.nodes, + channels, + managed, + config, + -1, + for_execution=False, + ) + yield StateSnapshot( + read_channels(channels, self.stream_channels_asis), + tuple(name for name, _ in next_tasks), + config, + metadata, + checkpoint["ts"], + parent_config, + ) async def aget_state_history( self, diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index fea1e29b8..0a86aee21 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -8786,6 +8786,76 @@ def test_nested_graph_state( ) }, ) + assert list(app.get_state_history(config, include_subgraph_state=True)) == [ + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": { + "thread_id": "1__inner", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1__inner", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("outer_1",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={}, + next=("__start__",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + subgraph_state_snapshots=None, + ), + ] app.invoke(None, config, debug=True) # test state w/ nested subgraph state (after resuming from interrupt) assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( @@ -8826,6 +8896,151 @@ def test_nested_graph_state( ) }, ) + assert list(app.get_state_history(config, include_subgraph_state=False)) == [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "outer_2": { + "my_key": "hi my value here and there and back again" + } + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=("outer_2",), + config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"inner": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + # TODO: this is likely very confusing for an end user, and we'll probably need to update this. + # right now this is happening due to us overwriting the + # subgraph snapshot after we finish the graph with while the thread_ts + # is the same as when we interrupted + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1__inner", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1__inner", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + subgraph_state_snapshots=None, + ), + ] finally: if hasattr(checkpointer, "__exit__"): checkpointer.__exit__(None, None, None) From a7d48465dafdc328e79f591edb956ad5e1a11bfd Mon Sep 17 00:00:00 2001 From: vbarda Date: Wed, 24 Jul 2024 12:44:35 -0400 Subject: [PATCH 006/287] use .list for looking up prefix-matched checkpoints --- .../langgraph/checkpoint/aiosqlite.py | 47 +--- libs/langgraph/langgraph/checkpoint/base.py | 13 +- libs/langgraph/langgraph/checkpoint/memory.py | 74 ++---- libs/langgraph/langgraph/checkpoint/sqlite.py | 52 +---- libs/langgraph/langgraph/pregel/__init__.py | 131 +++++++---- libs/langgraph/tests/test_pregel.py | 2 +- libs/langgraph/tests/test_pregel_async.py | 219 ++++++++++++++++++ 7 files changed, 333 insertions(+), 205 deletions(-) diff --git a/libs/langgraph/langgraph/checkpoint/aiosqlite.py b/libs/langgraph/langgraph/checkpoint/aiosqlite.py index e0e982537..5ac9a9197 100644 --- a/libs/langgraph/langgraph/checkpoint/aiosqlite.py +++ b/libs/langgraph/langgraph/checkpoint/aiosqlite.py @@ -306,6 +306,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, + as_prefix: bool = False, ) -> AsyncIterator[CheckpointTuple]: """List checkpoints from the database asynchronously. @@ -322,7 +323,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples. """ await self.setup() - where, param_values = search_where(config, filter, before) + where, param_values = search_where(config, filter, before, as_prefix=as_prefix) query = f"""SELECT thread_id, thread_ts, parent_ts, checkpoint, metadata FROM checkpoints {where} @@ -347,50 +348,6 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): ), ) - async def alist_subgraph_checkpoints( - self, config: RunnableConfig - ) -> AsyncIterator[CheckpointTuple]: - # TODO: docstring - async with self.conn.cursor() as cur: - if config["configurable"].get("thread_ts"): - await cur.execute( - "SELECT thread_id, thread_ts, parent_ts, checkpoint, metadata FROM checkpoints WHERE thread_id LIKE ? || '%' AND thread_ts = ?", - ( - str(config["configurable"]["thread_id"]), - str(config["configurable"]["thread_ts"]), - ), - ) - else: - await cur.execute( - """SELECT checkpoints.thread_id, checkpoints.thread_ts, checkpoints.parent_ts, checkpoints.checkpoint, checkpoints.metadata - FROM checkpoints - INNER JOIN ( - SELECT thread_id, MAX(thread_ts) as thread_ts - FROM checkpoints - WHERE thread_id LIKE ? || '%' - GROUP BY thread_id - ) latest_checkpoints - ON checkpoints.thread_id = latest_checkpoints.thread_id AND checkpoints.thread_ts = latest_checkpoints.thread_ts - ORDER BY checkpoints.thread_id, checkpoints.thread_ts DESC""", - (str(config["configurable"]["thread_id"]),), - ) - async for thread_id, thread_ts, parent_ts, value, metadata in cur: - yield CheckpointTuple( - {"configurable": {"thread_id": thread_id, "thread_ts": thread_ts}}, - self.serde.loads(value), - self.serde.loads(metadata) if metadata is not None else {}, - ( - { - "configurable": { - "thread_id": thread_id, - "thread_ts": parent_ts, - } - } - if parent_ts - else None - ), - ) - async def aput( self, config: RunnableConfig, diff --git a/libs/langgraph/langgraph/checkpoint/base.py b/libs/langgraph/langgraph/checkpoint/base.py index d2b0df3da..cca902050 100644 --- a/libs/langgraph/langgraph/checkpoint/base.py +++ b/libs/langgraph/langgraph/checkpoint/base.py @@ -214,6 +214,7 @@ class BaseCheckpointSaver(ABC): filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, + as_prefix: bool = False, ) -> Iterator[CheckpointTuple]: """List checkpoints that match the given criteria. @@ -231,12 +232,6 @@ class BaseCheckpointSaver(ABC): """ raise NotImplementedError - def list_subgraph_checkpoints( - self, config: RunnableConfig - ) -> Iterator[CheckpointTuple]: - # TODO: docstring - raise NotImplementedError - def put( self, config: RunnableConfig, @@ -329,12 +324,6 @@ class BaseCheckpointSaver(ABC): raise NotImplementedError yield - async def alist_subgraph_checkpoints( - self, config: RunnableConfig - ) -> AsyncIterator[CheckpointTuple]: - # TODO: docstring - raise NotImplementedError - async def aput( self, config: RunnableConfig, diff --git a/libs/langgraph/langgraph/checkpoint/memory.py b/libs/langgraph/langgraph/checkpoint/memory.py index 8160de238..1aa89f881 100644 --- a/libs/langgraph/langgraph/checkpoint/memory.py +++ b/libs/langgraph/langgraph/checkpoint/memory.py @@ -119,6 +119,7 @@ class MemorySaver(BaseCheckpointSaver): filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, + as_prefix: bool = False, ) -> Iterator[CheckpointTuple]: """List checkpoints from the in-memory storage. @@ -134,7 +135,19 @@ class MemorySaver(BaseCheckpointSaver): Yields: Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples. """ - thread_ids = (config["configurable"]["thread_id"],) if config else self.storage + if config: + config_thread_id = config["configurable"]["thread_id"] + if as_prefix: + thread_ids = ( + thread_id + for thread_id in self.storage + if thread_id.startswith(config_thread_id) + ) + else: + thread_ids = (config_thread_id,) + else: + thread_ids = self.storage.keys() + for thread_id in thread_ids: for ts, (checkpoint, metadata_b, parent_ts) in sorted( self.storage[thread_id].items(), key=lambda x: x[0], reverse=True @@ -171,40 +184,6 @@ class MemorySaver(BaseCheckpointSaver): else None, ) - def list_subgraph_checkpoints( - self, config: RunnableConfig - ) -> Iterator[CheckpointTuple]: - # TODO: docstring - thread_id_prefix = config["configurable"]["thread_id"] - matching_thread_ids = [ - key for key in self.storage.keys() if key.startswith(thread_id_prefix) - ] - for thread_id in matching_thread_ids: - ts = config["configurable"].get("thread_ts") - if not ts: - if checkpoints := self.storage[thread_id]: - ts = max(checkpoints.keys()) - - if saved := self.storage[thread_id].get(ts): - checkpoint, metadata, parent_ts = saved - writes = self.writes[(thread_id, ts)] - yield CheckpointTuple( - config={"configurable": {"thread_id": thread_id, "thread_ts": ts}}, - checkpoint=self.serde.loads(checkpoint), - metadata=self.serde.loads(metadata), - pending_writes=[ - (id, c, self.serde.loads(v)) for id, c, v in writes - ], - parent_config={ - "configurable": { - "thread_id": thread_id, - "thread_ts": parent_ts, - } - } - if parent_ts - else None, - ) - def put( self, config: RunnableConfig, @@ -288,6 +267,7 @@ class MemorySaver(BaseCheckpointSaver): filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, + as_prefix: bool = False, ) -> AsyncIterator[CheckpointTuple]: """Asynchronous version of list. @@ -302,7 +282,15 @@ class MemorySaver(BaseCheckpointSaver): """ loop = asyncio.get_running_loop() iter = await loop.run_in_executor( - None, partial(self.list, before=before, limit=limit, filter=filter), config + None, + partial( + self.list, + before=before, + limit=limit, + filter=filter, + as_prefix=as_prefix, + ), + config, ) while True: # handling StopIteration exception inside coroutine won't work @@ -312,20 +300,6 @@ class MemorySaver(BaseCheckpointSaver): else: break - async def alist_subgraph_checkpoints( - self, config: RunnableConfig - ) -> AsyncIterator[CheckpointTuple]: - # TODO: docstring - loop = asyncio.get_running_loop() - iter = await loop.run_in_executor(None, self.list_subgraph_checkpoints, config) - while True: - # handling StopIteration exception inside coroutine won't work - # as expected, so using next() with default value to break the loop - if item := await loop.run_in_executor(None, next, iter, None): - yield item - else: - break - async def aput( self, config: RunnableConfig, diff --git a/libs/langgraph/langgraph/checkpoint/sqlite.py b/libs/langgraph/langgraph/checkpoint/sqlite.py index 6578764ca..c1c0b2217 100644 --- a/libs/langgraph/langgraph/checkpoint/sqlite.py +++ b/libs/langgraph/langgraph/checkpoint/sqlite.py @@ -301,6 +301,7 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, + as_prefix: bool = False, ) -> Iterator[CheckpointTuple]: """List checkpoints from the database. @@ -331,7 +332,7 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): >>> print(checkpoints) [CheckpointTuple(...), ...] """ - where, param_values = search_where(config, filter, before) + where, param_values = search_where(config, filter, before, as_prefix=as_prefix) query = f"""SELECT thread_id, thread_ts, parent_ts, checkpoint, metadata FROM checkpoints {where} @@ -357,50 +358,6 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): ), ) - def list_subgraph_checkpoints( - self, config: RunnableConfig - ) -> Iterator[CheckpointTuple]: - # TODO: docstring - with self.cursor(transaction=False) as cur: - if config["configurable"].get("thread_ts"): - cur.execute( - "SELECT thread_id, thread_ts, parent_ts, checkpoint, metadata FROM checkpoints WHERE thread_id LIKE ? || '%' AND thread_ts = ?", - ( - str(config["configurable"]["thread_id"]), - str(config["configurable"]["thread_ts"]), - ), - ) - else: - cur.execute( - """SELECT checkpoints.thread_id, checkpoints.thread_ts, checkpoints.parent_ts, checkpoints.checkpoint, checkpoints.metadata - FROM checkpoints - INNER JOIN ( - SELECT thread_id, MAX(thread_ts) as thread_ts - FROM checkpoints - WHERE thread_id LIKE ? || '%' - GROUP BY thread_id - ) latest_checkpoints - ON checkpoints.thread_id = latest_checkpoints.thread_id AND checkpoints.thread_ts = latest_checkpoints.thread_ts - ORDER BY checkpoints.thread_id, checkpoints.thread_ts DESC""", - (str(config["configurable"]["thread_id"]),), - ) - for thread_id, thread_ts, parent_ts, value, metadata in cur: - yield CheckpointTuple( - {"configurable": {"thread_id": thread_id, "thread_ts": thread_ts}}, - self.serde.loads(value), - self.serde.loads(metadata) if metadata is not None else {}, - ( - { - "configurable": { - "thread_id": thread_id, - "thread_ts": parent_ts, - } - } - if parent_ts - else None - ), - ) - def put( self, config: RunnableConfig, @@ -592,6 +549,7 @@ def search_where( config: Optional[RunnableConfig], filter: Optional[Dict[str, Any]], before: Optional[RunnableConfig] = None, + as_prefix: bool = False, ) -> Tuple[str, Sequence[Any]]: """Return WHERE clause predicates for (a)search() given metadata filter and `before` config. @@ -606,7 +564,9 @@ def search_where( # construct predicate for config filter if config is not None: - wheres.append("thread_id = ?") + thread_filter = "thread_id LIKE ? || '%'" if as_prefix else "thread_id = ?" + wheres.append(thread_filter) + param_values.append(config["configurable"]["thread_id"]) # construct predicate for metadata filter diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 6287c713b..c0b3a511d 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -438,7 +438,7 @@ class Pregel( state_snapshot = thread_id_to_state_snapshots.pop(root_thread_id, None) if state_snapshot is None: - raise ValueError(f"Missing snapshot for thread ID '{root_thread_id}'") + raise ValueError(f"Missing checkpoint for thread ID '{root_thread_id}'") return state_snapshot def get_state( @@ -449,18 +449,34 @@ class Pregel( raise ValueError("No checkpointer set") if include_subgraph_state: - checkpoint_tuples = self.checkpointer.list_subgraph_checkpoints(config) + checkpoint_tuples = self.checkpointer.list(config, as_prefix=True) else: checkpoint_tuples = iter([self.checkpointer.get_tuple(config)]) - thread_id_to_state_snapshots: dict[str, StateSnapshot] = { - checkpoint.config["configurable"][ - "thread_id" - ]: self._prepare_state_snapshot(checkpoint, config) - for checkpoint in checkpoint_tuples - } - thread_id = config["configurable"]["thread_id"] + thread_ts = config["configurable"].get("thread_ts") + thread_id_to_thread_ts: dict[str, str] = {} + thread_id_to_state_snapshots: dict[str, StateSnapshot] = {} + for checkpoint_tuple in checkpoint_tuples: + checkpoint_thread_id = checkpoint_tuple.config["configurable"]["thread_id"] + checkpoint_thread_ts = checkpoint_tuple.config["configurable"]["thread_ts"] + if thread_ts and thread_ts != checkpoint_thread_ts: + continue + + existing_thread_ts = thread_id_to_thread_ts.get(checkpoint_thread_id) + # keep only most recent thread_ts + if existing_thread_ts is None or checkpoint_thread_ts > existing_thread_ts: + state_snapshot = self._prepare_state_snapshot(checkpoint_tuple, config) + thread_id_to_state_snapshots[checkpoint_thread_id] = state_snapshot + thread_id_to_thread_ts[checkpoint_thread_id] = checkpoint_thread_ts + + if not thread_id_to_state_snapshots: + error_msg = f"Could not find checkpoints for thread ID '{thread_id}'" + if thread_ts: + error_msg += f" and thread TS '{thread_ts}'" + + raise ValueError(error_msg) + state_snapshot = self._assemble_state_snapshot_hierarchy( thread_id, thread_id_to_state_snapshots ) @@ -474,7 +490,7 @@ class Pregel( raise ValueError("No checkpointer set") if include_subgraph_state: - checkpoint_tuples = self.checkpointer.alist_subgraph_checkpoints(config) + checkpoint_tuples = self.checkpointer.alist(config, as_prefix=True) else: async def alist_checkpoints(): @@ -482,14 +498,32 @@ class Pregel( checkpoint_tuples = alist_checkpoints() - thread_id_to_state_snapshots: dict[str, StateSnapshot] = { - checkpoint.config["configurable"][ - "thread_id" - ]: await self._prepare_state_snapshot_async(checkpoint, config) - async for checkpoint in checkpoint_tuples - } - thread_id = config["configurable"]["thread_id"] + thread_ts = config["configurable"].get("thread_ts") + thread_id_to_thread_ts: dict[str, str] = {} + thread_id_to_state_snapshots: dict[str, StateSnapshot] = {} + async for checkpoint_tuple in checkpoint_tuples: + checkpoint_thread_id = checkpoint_tuple.config["configurable"]["thread_id"] + checkpoint_thread_ts = checkpoint_tuple.config["configurable"]["thread_ts"] + if thread_ts and thread_ts != checkpoint_thread_ts: + continue + + existing_thread_ts = thread_id_to_thread_ts.get(checkpoint_thread_id) + # keep only most recent thread_ts + if existing_thread_ts is None or checkpoint_thread_ts > existing_thread_ts: + state_snapshot = await self._prepare_state_snapshot_async( + checkpoint_tuple, config + ) + thread_id_to_state_snapshots[checkpoint_thread_id] = state_snapshot + thread_id_to_thread_ts[checkpoint_thread_id] = checkpoint_thread_ts + + if not thread_id_to_state_snapshots: + error_msg = f"Could not find checkpoints for thread ID '{thread_id}'" + if thread_ts: + error_msg += f" and thread TS '{thread_ts}'" + + raise ValueError(error_msg) + state_snapshot = self._assemble_state_snapshot_hierarchy( thread_id, thread_id_to_state_snapshots ) @@ -515,20 +549,8 @@ class Pregel( for config, checkpoint, metadata, parent_config, _ in self.checkpointer.list( config, before=before, limit=limit, filter=filter ): - # is there a way to do this more efficiently? if include_subgraph_state: - checkpoint_tuples = self.checkpointer.list_subgraph_checkpoints(config) - - thread_id_to_state_snapshots: dict[str, StateSnapshot] = { - checkpoint.config["configurable"][ - "thread_id" - ]: self._prepare_state_snapshot(checkpoint, config) - for checkpoint in checkpoint_tuples - } - thread_id = config["configurable"]["thread_id"] - state_snapshot = self._assemble_state_snapshot_hierarchy( - thread_id, thread_id_to_state_snapshots - ) + state_snapshot = self.get_state(config, include_subgraph_state=True) yield state_snapshot else: with ChannelsManager( @@ -561,6 +583,7 @@ class Pregel( filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, + include_subgraph_state: bool = False, ) -> AsyncIterator[StateSnapshot]: """Get the history of the state of the graph.""" if not self.checkpointer: @@ -577,28 +600,34 @@ class Pregel( parent_config, _, ) in self.checkpointer.alist(config, before=before, limit=limit, filter=filter): - async with AsyncChannelsManager( - self.channels, checkpoint, config - ) as channels, AsyncManagedValuesManager( - self.managed_values_dict, ensure_config(config), self - ) as managed: - next_tasks = prepare_next_tasks( - checkpoint, - self.nodes, - channels, - managed, - config, - -1, - for_execution=False, - ) - yield StateSnapshot( - read_channels(channels, self.stream_channels_asis), - tuple(name for name, _ in next_tasks), - config, - metadata, - checkpoint["ts"], - parent_config, + if include_subgraph_state: + state_snapshot = await self.aget_state( + config, include_subgraph_state=True ) + yield state_snapshot + else: + async with AsyncChannelsManager( + self.channels, checkpoint, config + ) as channels, AsyncManagedValuesManager( + self.managed_values_dict, ensure_config(config), self + ) as managed: + next_tasks = prepare_next_tasks( + checkpoint, + self.nodes, + channels, + managed, + config, + -1, + for_execution=False, + ) + yield StateSnapshot( + read_channels(channels, self.stream_channels_asis), + tuple(name for name, _ in next_tasks), + config, + metadata, + checkpoint["ts"], + parent_config, + ) def update_state( self, diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 0a86aee21..34e4028ff 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -8896,7 +8896,7 @@ def test_nested_graph_state( ) }, ) - assert list(app.get_state_history(config, include_subgraph_state=False)) == [ + assert list(app.get_state_history(config, include_subgraph_state=True)) == [ StateSnapshot( values={"my_key": "hi my value here and there and back again"}, next=(), diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 2e40dc0b1..a9653b546 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -7322,6 +7322,78 @@ async def test_nested_graph_state(checkpointer: BaseCheckpointSaver) -> None: ) }, ) + assert [ + c async for c in app.aget_state_history(config, include_subgraph_state=True) + ] == [ + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": { + "thread_id": "1__inner", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1__inner", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("outer_1",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": {"thread_id": "1", "thread_ts": AnyStr()} + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={}, + next=("__start__",), + config={"configurable": {"thread_id": "1", "thread_ts": AnyStr()}}, + metadata={ + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + subgraph_state_snapshots=None, + ), + ] await app.ainvoke(None, config, debug=True) # test state w/ nested subgraph state (after resuming from interrupt) assert await app.aget_state( @@ -7364,6 +7436,153 @@ async def test_nested_graph_state(checkpointer: BaseCheckpointSaver) -> None: ) }, ) + assert [ + c async for c in app.aget_state_history(config, include_subgraph_state=True) + ] == [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "outer_2": { + "my_key": "hi my value here and there and back again" + } + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=("outer_2",), + config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"inner": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + # TODO: this is likely very confusing for an end user, and we'll probably need to update this. + # right now this is happening due to us overwriting the + # subgraph snapshot after we finish the graph with while the thread_ts + # is the same as when we interrupted + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1__inner", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1__inner", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "thread_ts": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + subgraph_state_snapshots=None, + ), + ] finally: if hasattr(checkpointer, "__aexit__"): await checkpointer.__aexit__(None, None, None) From eee91d21292052476988a5bbcf341e38d98f891c Mon Sep 17 00:00:00 2001 From: isaac hershenson Date: Thu, 8 Aug 2024 15:29:40 -0700 Subject: [PATCH 007/287] wip --- libs/langgraph/langgraph/pregel/__init__.py | 7 +++++++ libs/langgraph/langgraph/pregel/debug.py | 9 ++++++--- libs/langgraph/tests/any_int.py | 6 ++++++ libs/langgraph/tests/test_pregel.py | 21 +++++++++++++++++++++ libs/langgraph/tests/test_pregel_async.py | 21 +++++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 libs/langgraph/tests/any_int.py diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 8f9583964..e8d43d0c9 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -4,6 +4,7 @@ import asyncio import concurrent.futures import time from collections import deque +from datetime import datetime, timezone from functools import partial from inspect import signature from typing import ( @@ -913,6 +914,8 @@ class Pregel( interrupt_after=interrupt_after, manager=run_manager, ): + # start time of superstep + step_start_time = datetime.now(timezone.utc) # debug flag if self.debug: print_step_checkpoint( @@ -987,6 +990,7 @@ class Pregel( loop.step, [task], self.stream_channels_list, + step_start_time, ), ) else: @@ -1157,6 +1161,8 @@ class Pregel( interrupt_after=interrupt_after, manager=run_manager, ): + # start time of superstep + step_start_time = datetime.now(timezone.utc) # debug flag if self.debug: print_step_checkpoint( @@ -1233,6 +1239,7 @@ class Pregel( loop.step, [task], self.stream_channels_list, + step_start_time, ), ): yield chunk diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index 9e152cf87..d4123ba2b 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -90,9 +90,10 @@ def map_debug_task_results( step: int, tasks: list[PregelExecutableTask], stream_channels_list: Sequence[str], + step_start_time: datetime, ) -> Iterator[DebugOutputTaskResult]: - ts = datetime.now(timezone.utc).isoformat() - for name, _, _, writes, config, _, _, _ in tasks: + ts = datetime.now(timezone.utc) + for name, _, _, writes, config, _, _, task_id in tasks: if config is not None and TAG_HIDDEN in config.get("tags", []): continue @@ -102,11 +103,13 @@ def map_debug_task_results( yield { "type": "task_result", - "timestamp": ts, + "timestamp": ts.isoformat(), "step": step, "payload": { "id": str(uuid5(TASK_NAMESPACE, json.dumps((name, step, metadata)))), + "task_id": task_id, "name": name, + "node_exec_ms": int((ts - step_start_time).total_seconds() * 1000), "result": [w for w in writes if w[0] in stream_channels_list], }, } diff --git a/libs/langgraph/tests/any_int.py b/libs/langgraph/tests/any_int.py new file mode 100644 index 000000000..2fb2dba55 --- /dev/null +++ b/libs/langgraph/tests/any_int.py @@ -0,0 +1,6 @@ +class AnyInt(int): + def __init__(self) -> None: + super().__init__() + + def __eq__(self, other: object) -> bool: + return isinstance(other, int) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 71f2f8c59..388e34507 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -61,6 +61,7 @@ from langgraph.prebuilt.chat_agent_executor import ( from langgraph.prebuilt.tool_node import ToolNode from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot from langgraph.pregel.retry import RetryPolicy +from tests.any_int import AnyInt from tests.any_str import AnyStr from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -1010,6 +1011,8 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "payload": { "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", "name": "one", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("inbox", 3)], }, }, @@ -1020,6 +1023,8 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "payload": { "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", "name": "two", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("output", 13)], }, }, @@ -1041,6 +1046,8 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "payload": { "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", "name": "two", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("output", 4)], }, }, @@ -5881,6 +5888,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", "name": "rewrite_query", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("query", "query: what is weather in sf")], }, }, @@ -5935,6 +5944,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", "name": "retriever_two", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("docs", ["doc3", "doc4"])], }, }, @@ -5952,6 +5963,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", "name": "retriever_one", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("docs", ["doc1", "doc2"])], }, }, @@ -5991,6 +6004,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", "name": "qa", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("answer", "doc1,doc2,doc3,doc4")], }, }, @@ -6269,6 +6284,8 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "payload": { "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", "name": "prepare", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("my_key", " prepared")], }, }, @@ -6317,6 +6334,8 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "payload": { "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", "name": "tool_two_slow", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("my_key", " slow")], }, }, @@ -6365,6 +6384,8 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "payload": { "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", "name": "finish", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("my_key", " finished")], }, }, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index e5c305dd8..e27cc2b58 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -58,6 +58,7 @@ from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.prebuilt.tool_node import ToolNode from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot from langgraph.pregel.retry import RetryPolicy +from tests.any_int import AnyInt from tests.any_str import AnyStr from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -1117,6 +1118,8 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "payload": { "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", "name": "one", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("inbox", 3)], }, }, @@ -1127,6 +1130,8 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "payload": { "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", "name": "two", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("output", 13)], }, }, @@ -1148,6 +1153,8 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "payload": { "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", "name": "two", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("output", 4)], }, }, @@ -4490,6 +4497,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", "name": "rewrite_query", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("query", "query: what is weather in sf")], }, }, @@ -4544,6 +4553,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", "name": "retriever_two", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("docs", ["doc3", "doc4"])], }, }, @@ -4561,6 +4572,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", "name": "retriever_one", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("docs", ["doc1", "doc2"])], }, }, @@ -4600,6 +4613,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", "name": "qa", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("answer", "doc1,doc2,doc3,doc4")], }, }, @@ -4904,6 +4919,8 @@ async def test_branch_then() -> None: "payload": { "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", "name": "prepare", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("my_key", " prepared")], }, }, @@ -4952,6 +4969,8 @@ async def test_branch_then() -> None: "payload": { "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", "name": "tool_two_slow", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("my_key", " slow")], }, }, @@ -5000,6 +5019,8 @@ async def test_branch_then() -> None: "payload": { "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", "name": "finish", + "task_id": AnyStr(), + "node_exec_ms": AnyInt(), "result": [("my_key", " finished")], }, }, From 322cfc46d3acbbd8cc3efda0e40bb70ef524af93 Mon Sep 17 00:00:00 2001 From: vbarda Date: Mon, 12 Aug 2024 10:36:25 -0400 Subject: [PATCH 008/287] cleanup --- .../langgraph/checkpoint/base/__init__.py | 2 + .../langgraph/checkpoint/memory/__init__.py | 109 +++-- libs/langgraph/langgraph/pregel/__init__.py | 150 +++--- libs/langgraph/tests/test_pregel.py | 450 +++++++++++++++++ libs/langgraph/tests/test_pregel_async.py | 452 ++++++++++++++++++ 5 files changed, 1057 insertions(+), 106 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/base/__init__.py b/libs/checkpoint/langgraph/checkpoint/base/__init__.py index 86c8b0eec..4f0f08a84 100644 --- a/libs/checkpoint/langgraph/checkpoint/base/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/base/__init__.py @@ -259,6 +259,7 @@ class BaseCheckpointSaver(ABC): filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, + include_nested_checkpoints: bool = False, ) -> Iterator[CheckpointTuple]: """List checkpoints that match the given criteria. @@ -350,6 +351,7 @@ class BaseCheckpointSaver(ABC): filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, + include_nested_checkpoints: bool = False, ) -> AsyncIterator[CheckpointTuple]: """Asynchronously list checkpoints that match the given criteria. diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index d989b1fbc..5e60ebaf0 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -157,6 +157,7 @@ class MemorySaver( filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, + include_nested_checkpoints: bool = False, ) -> Iterator[CheckpointTuple]: """List checkpoints from the in-memory storage. @@ -177,53 +178,67 @@ class MemorySaver( config["configurable"].get("checkpoint_ns", "") if config else "" ) for thread_id in thread_ids: - for checkpoint_id, (checkpoint, metadata_b, parent_checkpoint_id) in sorted( - self.storage[thread_id][checkpoint_ns].items(), - key=lambda x: x[0], - reverse=True, - ): - # filter by checkpoint ID - if ( - before - and (before_checkpoint_id := get_checkpoint_id(before)) - and checkpoint_id >= before_checkpoint_id - ): - continue - - # filter by metadata - metadata = self.serde.loads_typed(metadata_b) - if filter and not all( - query_value == metadata[query_key] - for query_key, query_value in filter.items() - ): - continue - - # limit search results - if limit is not None and limit <= 0: - break - elif limit is not None: - limit -= 1 - - yield CheckpointTuple( - config={ - "configurable": { - "thread_id": thread_id, - "checkpoint_ns": checkpoint_ns, - "checkpoint_id": checkpoint_id, - } - }, - checkpoint=self.serde.loads_typed(checkpoint), - metadata=metadata, - parent_config={ - "configurable": { - "thread_id": thread_id, - "checkpoint_ns": checkpoint_ns, - "checkpoint_id": parent_checkpoint_id, - } - } - if parent_checkpoint_id - else None, + checkpoint_ns_iter = ( + ( + key + for key in self.storage[thread_id].keys() + if key.startswith(checkpoint_ns) ) + if include_nested_checkpoints + else [checkpoint_ns] + ) + for checkpoint_ns in checkpoint_ns_iter: + for checkpoint_id, ( + checkpoint, + metadata_b, + parent_checkpoint_id, + ) in sorted( + self.storage[thread_id][checkpoint_ns].items(), + key=lambda x: x[0], + reverse=True, + ): + # filter by checkpoint ID + if ( + before + and (before_checkpoint_id := get_checkpoint_id(before)) + and checkpoint_id >= before_checkpoint_id + ): + continue + + # filter by metadata + metadata = self.serde.loads_typed(metadata_b) + if filter and not all( + query_value == metadata[query_key] + for query_key, query_value in filter.items() + ): + continue + + # limit search results + if limit is not None and limit <= 0: + break + elif limit is not None: + limit -= 1 + + yield CheckpointTuple( + config={ + "configurable": { + "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, + "checkpoint_id": checkpoint_id, + } + }, + checkpoint=self.serde.loads_typed(checkpoint), + metadata=metadata, + parent_config={ + "configurable": { + "thread_id": thread_id, + "checkpoint_ns": checkpoint_ns, + "checkpoint_id": parent_checkpoint_id, + } + } + if parent_checkpoint_id + else None, + ) def put( self, @@ -315,6 +330,7 @@ class MemorySaver( filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, + include_nested_checkpoints: bool = False, ) -> AsyncIterator[CheckpointTuple]: """Asynchronous version of list. @@ -335,6 +351,7 @@ class MemorySaver( before=before, limit=limit, filter=filter, + include_nested_checkpoints=include_nested_checkpoints, ), config, ) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 2a31ec281..34c12604c 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -65,12 +65,12 @@ from langgraph.checkpoint.base import ( empty_checkpoint, ) from langgraph.constants import ( + CHECKPOINT_NAMESPACE_SEPARATOR, CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_READ, CONFIG_KEY_RESUMING, CONFIG_KEY_SEND, INTERRUPT, - THREAD_ID_SEPARATOR, ) from langgraph.errors import GraphRecursionError, InvalidUpdateError from langgraph.managed.base import ( @@ -422,35 +422,36 @@ class Pregel( @staticmethod def _assemble_state_snapshot_hierarchy( - root_thread_id: str, thread_id_to_state_snapshots: dict[str, StateSnapshot] + root_checkpoint_ns: str, + checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot], ) -> StateSnapshot: - thread_ids_to_visit = sorted( - thread_id_to_state_snapshots.keys(), - key=lambda x: len(x.split(THREAD_ID_SEPARATOR)), + checkpoint_ns_list_to_visit = sorted( + checkpoint_ns_to_state_snapshots.keys(), + key=lambda x: len(x.split(CHECKPOINT_NAMESPACE_SEPARATOR)), ) - while thread_ids_to_visit: - thread_id = thread_ids_to_visit.pop() - state_snapshot = thread_id_to_state_snapshots[thread_id] - *path, subgraph_node = thread_id.split(THREAD_ID_SEPARATOR) - parent_thread_id = THREAD_ID_SEPARATOR.join(path) - if parent_thread_id and ( - parent_state_snapshot := thread_id_to_state_snapshots.get( - parent_thread_id + while checkpoint_ns_list_to_visit: + checkpoint_ns = checkpoint_ns_list_to_visit.pop() + state_snapshot = checkpoint_ns_to_state_snapshots[checkpoint_ns] + *path, subgraph_node = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) + parent_checkpoint_ns = CHECKPOINT_NAMESPACE_SEPARATOR.join(path) + if subgraph_node and ( + parent_state_snapshot := checkpoint_ns_to_state_snapshots.get( + parent_checkpoint_ns ) ): parent_subgraph_snapshots = { **(parent_state_snapshot.subgraph_state_snapshots or {}), subgraph_node: state_snapshot, } - thread_id_to_state_snapshots[ - parent_thread_id - ] = thread_id_to_state_snapshots[parent_thread_id]._replace( + checkpoint_ns_to_state_snapshots[ + parent_checkpoint_ns + ] = checkpoint_ns_to_state_snapshots[parent_checkpoint_ns]._replace( subgraph_state_snapshots=parent_subgraph_snapshots ) - state_snapshot = thread_id_to_state_snapshots.pop(root_thread_id, None) + state_snapshot = checkpoint_ns_to_state_snapshots.pop(root_checkpoint_ns, None) if state_snapshot is None: - raise ValueError(f"Missing checkpoint for thread ID '{root_thread_id}'") + raise ValueError(f"Missing checkpoint for thread ID '{root_checkpoint_ns}'") return state_snapshot def get_state( @@ -461,36 +462,51 @@ class Pregel( raise ValueError("No checkpointer set") if include_subgraph_state: - checkpoint_tuples = self.checkpointer.list(config, as_prefix=True) + checkpoint_tuples = self.checkpointer.list( + config, include_nested_checkpoints=True + ) else: checkpoint_tuples = iter([self.checkpointer.get_tuple(config)]) - thread_id = config["configurable"]["thread_id"] - thread_ts = config["configurable"].get("thread_ts") - thread_id_to_thread_ts: dict[str, str] = {} - thread_id_to_state_snapshots: dict[str, StateSnapshot] = {} + checkpoint_ns = config["configurable"].get("checkpoint_ns", "") + checkpoint_id = config["configurable"].get("checkpoint_id") + checkpoint_ns_to_checkpoint_id: dict[str, str] = {} + checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} for checkpoint_tuple in checkpoint_tuples: - checkpoint_thread_id = checkpoint_tuple.config["configurable"]["thread_id"] - checkpoint_thread_ts = checkpoint_tuple.config["configurable"]["thread_ts"] - if thread_ts and thread_ts != checkpoint_thread_ts: + saved_checkpoint_ns = checkpoint_tuple.config["configurable"][ + "checkpoint_ns" + ] + saved_checkpoint_id = checkpoint_tuple.config["configurable"][ + "checkpoint_id" + ] + if checkpoint_id and checkpoint_id != saved_checkpoint_id: continue - existing_thread_ts = thread_id_to_thread_ts.get(checkpoint_thread_id) - # keep only most recent thread_ts - if existing_thread_ts is None or checkpoint_thread_ts > existing_thread_ts: + existing_checkpoint_id = checkpoint_ns_to_checkpoint_id.get( + saved_checkpoint_ns + ) + # keep only most recent checkpoint_id + if ( + existing_checkpoint_id is None + or saved_checkpoint_id > existing_checkpoint_id + ): state_snapshot = self._prepare_state_snapshot(checkpoint_tuple, config) - thread_id_to_state_snapshots[checkpoint_thread_id] = state_snapshot - thread_id_to_thread_ts[checkpoint_thread_id] = checkpoint_thread_ts + checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot + checkpoint_ns_to_checkpoint_id[ + saved_checkpoint_ns + ] = saved_checkpoint_id - if not thread_id_to_state_snapshots: - error_msg = f"Could not find checkpoints for thread ID '{thread_id}'" - if thread_ts: - error_msg += f" and thread TS '{thread_ts}'" + if not checkpoint_ns_to_state_snapshots: + error_msg = ( + f"Could not find checkpoints for checkpoint NS '{checkpoint_ns}'" + ) + if checkpoint_id: + error_msg += f" and checkpoint ID '{checkpoint_id}'" raise ValueError(error_msg) state_snapshot = self._assemble_state_snapshot_hierarchy( - thread_id, thread_id_to_state_snapshots + checkpoint_ns, checkpoint_ns_to_state_snapshots ) return state_snapshot @@ -502,7 +518,9 @@ class Pregel( raise ValueError("No checkpointer set") if include_subgraph_state: - checkpoint_tuples = self.checkpointer.alist(config, as_prefix=True) + checkpoint_tuples = self.checkpointer.alist( + config, include_nested_checkpoints=True + ) else: async def alist_checkpoints(): @@ -510,34 +528,45 @@ class Pregel( checkpoint_tuples = alist_checkpoints() - thread_id = config["configurable"]["thread_id"] - thread_ts = config["configurable"].get("thread_ts") - thread_id_to_thread_ts: dict[str, str] = {} - thread_id_to_state_snapshots: dict[str, StateSnapshot] = {} + checkpoint_ns = config["configurable"].get("checkpoint_ns", "") + checkpoint_id = config["configurable"].get("checkpoint_id") + checkpoint_ns_to_checkpoint_id: dict[str, str] = {} + checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} async for checkpoint_tuple in checkpoint_tuples: - checkpoint_thread_id = checkpoint_tuple.config["configurable"]["thread_id"] - checkpoint_thread_ts = checkpoint_tuple.config["configurable"]["thread_ts"] - if thread_ts and thread_ts != checkpoint_thread_ts: + saved_checkpoint_ns = checkpoint_tuple.config["configurable"][ + "checkpoint_ns" + ] + saved_checkpoint_id = checkpoint_tuple.config["configurable"][ + "checkpoint_id" + ] + if checkpoint_id and checkpoint_id != saved_checkpoint_id: continue - existing_thread_ts = thread_id_to_thread_ts.get(checkpoint_thread_id) - # keep only most recent thread_ts - if existing_thread_ts is None or checkpoint_thread_ts > existing_thread_ts: - state_snapshot = await self._prepare_state_snapshot_async( - checkpoint_tuple, config - ) - thread_id_to_state_snapshots[checkpoint_thread_id] = state_snapshot - thread_id_to_thread_ts[checkpoint_thread_id] = checkpoint_thread_ts + existing_checkpoint_id = checkpoint_ns_to_checkpoint_id.get( + saved_checkpoint_ns + ) + # keep only most recent checkpoint_id + if ( + existing_checkpoint_id is None + or saved_checkpoint_id > existing_checkpoint_id + ): + state_snapshot = self._prepare_state_snapshot(checkpoint_tuple, config) + checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot + checkpoint_ns_to_checkpoint_id[ + saved_checkpoint_ns + ] = saved_checkpoint_id - if not thread_id_to_state_snapshots: - error_msg = f"Could not find checkpoints for thread ID '{thread_id}'" - if thread_ts: - error_msg += f" and thread TS '{thread_ts}'" + if not checkpoint_ns_to_state_snapshots: + error_msg = ( + f"Could not find checkpoints for checkpoint NS '{checkpoint_ns}'" + ) + if checkpoint_id: + error_msg += f" and checkpoint ID '{checkpoint_id}'" raise ValueError(error_msg) state_snapshot = self._assemble_state_snapshot_hierarchy( - thread_id, thread_id_to_state_snapshots + checkpoint_ns, checkpoint_ns_to_state_snapshots ) return state_snapshot @@ -584,7 +613,7 @@ class Pregel( -1, for_execution=False, ) - + yield StateSnapshot( read_channels(channels, self.stream_channels_asis), tuple(t.name for t in next_tasks), @@ -620,10 +649,11 @@ class Pregel( ) in self.checkpointer.alist(config, before=before, limit=limit, filter=filter): if include_subgraph_state: state_snapshot = await self.aget_state( - config, include_subgraph_state=True) + config, include_subgraph_state=True + ) yield state_snapshot else: - async with AsyncChannelsManager( + async with AsyncChannelsManager( { k: LastValue(None) if isinstance(c, Context) else c for k, c in self.channels.items() diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index af7f0920a..1530fcda4 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -9093,6 +9093,456 @@ def test_doubly_nested_graph_interrupts( ] +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_nested_graph_state( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + + class InnerState(TypedDict): + my_key: str + my_other_key: str + + def inner_1(state: InnerState): + return { + "my_key": state["my_key"] + " here", + "my_other_key": state["my_key"], + } + + def inner_2(state: InnerState): + return { + "my_key": state["my_key"] + " and there", + "my_other_key": state["my_key"], + } + + 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 + + def outer_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + def outer_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("outer_1", outer_1) + graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) + graph.add_node("outer_2", outer_2) + graph.set_entry_point("outer_1") + graph.add_edge("outer_1", "inner") + graph.add_edge("inner", "outer_2") + graph.set_finish_point("outer_2") + + app = graph.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "1"}} + app.invoke({"my_key": "my value"}, config, debug=True) + # test state w/ nested subgraph state (right after interrupt) + assert app.get_state(config, include_subgraph_state=False) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + assert list(app.get_state_history(config, include_subgraph_state=True)) == [ + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + subgraph_state_snapshots=None, + ), + ] + app.invoke(None, config, debug=True) + # test state w/ nested subgraph state (after resuming from interrupt) + assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "outer_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + assert list(app.get_state_history(config, include_subgraph_state=True)) == [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "outer_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=("outer_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"inner": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + # TODO: this is likely very confusing for an end user, and we'll probably need to update this. + # right now this is happening due to us overwriting the + # subgraph snapshot after we finish the graph with while the checkpoint_id + # is the same as when we interrupted + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + subgraph_state_snapshots=None, + ), + ] + + def test_repeat_condition(snapshot: SnapshotAssertion) -> None: class AgentState(TypedDict): hello: str diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 67b62941b..064c48448 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -7600,6 +7600,458 @@ async def test_doubly_nested_graph_interrupts( ] +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], +) +async def test_nested_graph_state( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + + class InnerState(TypedDict): + my_key: str + my_other_key: str + + def inner_1(state: InnerState): + return { + "my_key": state["my_key"] + " here", + "my_other_key": state["my_key"], + } + + def inner_2(state: InnerState): + return { + "my_key": state["my_key"] + " and there", + "my_other_key": state["my_key"], + } + + 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 + + def outer_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + def outer_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("outer_1", outer_1) + graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) + graph.add_node("outer_2", outer_2) + graph.set_entry_point("outer_1") + graph.add_edge("outer_1", "inner") + graph.add_edge("inner", "outer_2") + graph.set_finish_point("outer_2") + + app = graph.compile(checkpointer=checkpointer) + + config = {"configurable": {"thread_id": "1"}} + await app.ainvoke({"my_key": "my value"}, config, debug=True) + # test state w/ nested subgraph state (right after interrupt) + assert await app.aget_state(config, include_subgraph_state=False) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + assert list(app.get_state_history(config, include_subgraph_state=True)) == [ + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + subgraph_state_snapshots=None, + ), + ] + await app.ainvoke(None, config, debug=True) + # test state w/ nested subgraph state (after resuming from interrupt) + assert await app.aget_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "outer_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + assert [ + s async for s in app.aget_state_history(config, include_subgraph_state=True) + ] == [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "outer_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=("outer_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"inner": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + # TODO: this is likely very confusing for an end user, and we'll probably need to update this. + # right now this is happening due to us overwriting the + # subgraph snapshot after we finish the graph with while the checkpoint_id + # is the same as when we interrupted + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + subgraph_state_snapshots=None, + ), + ] + + async def test_checkpoint_metadata() -> None: """This test verifies that a run's configurable fields are merged with the previous checkpoint config for each step in the run. From 9948125745de3acafa1cbdef9e8f90e1fe484126 Mon Sep 17 00:00:00 2001 From: vbarda Date: Mon, 12 Aug 2024 14:00:38 -0400 Subject: [PATCH 009/287] add checkpointer=INHERIT_CHECKPOINTER --- libs/langgraph/langgraph/graph/graph.py | 5 +- libs/langgraph/langgraph/graph/state.py | 4 +- libs/langgraph/langgraph/pregel/__init__.py | 23 +- libs/langgraph/tests/test_pregel.py | 280 +++++++++++++++++++- libs/langgraph/tests/test_pregel_async.py | 278 ++++++++++++++++++- 5 files changed, 568 insertions(+), 22 deletions(-) diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index dc927446d..c17373820 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -24,7 +24,6 @@ from langchain_core.runnables.graph import Graph as DrawableGraph from langchain_core.runnables.graph import Node as DrawableNode from langgraph.channels.ephemeral_value import EphemeralValue -from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.constants import ( CHECKPOINT_NAMESPACE_SEPARATOR, END, @@ -33,7 +32,7 @@ from langgraph.constants import ( Send, ) from langgraph.errors import InvalidUpdateError -from langgraph.pregel import Channel, Pregel +from langgraph.pregel import Channel, CheckpointerType, Pregel from langgraph.pregel.read import PregelNode from langgraph.pregel.types import All from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry @@ -369,7 +368,7 @@ class Graph: def compile( self, - checkpointer: Optional[BaseCheckpointSaver] = None, + checkpointer: Optional[CheckpointerType] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: bool = False, diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 5810aaf29..e9971a5f1 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -29,7 +29,6 @@ from langgraph.channels.dynamic_barrier_value import DynamicBarrierValue, WaitFo from langgraph.channels.ephemeral_value import EphemeralValue from langgraph.channels.last_value import LastValue from langgraph.channels.named_barrier_value import NamedBarrierValue -from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.constants import CHECKPOINT_NAMESPACE_SEPARATOR, TAG_HIDDEN from langgraph.errors import InvalidUpdateError from langgraph.graph.graph import ( @@ -41,6 +40,7 @@ from langgraph.graph.graph import ( Send, ) from langgraph.managed.base import ManagedValue, is_managed_value +from langgraph.pregel import CheckpointerType from langgraph.pregel.read import ChannelRead, PregelNode from langgraph.pregel.types import All, RetryPolicy from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry @@ -373,7 +373,7 @@ class StateGraph(Graph): def compile( self, - checkpointer: Optional[BaseCheckpointSaver] = None, + checkpointer: Optional[CheckpointerType] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: bool = False, diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 34c12604c..dd105e0ba 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -13,6 +13,7 @@ from typing import ( Callable, Dict, Iterator, + Literal, Mapping, Optional, Sequence, @@ -115,6 +116,10 @@ WriteValue = Union[ ] +INHERIT_CHECKPOINTER = "inherit_checkpointer" +CheckpointerType = Union[BaseCheckpointSaver, Literal["inherit_checkpointer"]] + + class Channel: @overload @classmethod @@ -218,7 +223,7 @@ class Pregel( debug: bool = Field(default_factory=get_debug) """Whether to print debug information during execution. Defaults to False.""" - checkpointer: Optional[BaseCheckpointSaver] = None + checkpointer: Optional[CheckpointerType] = None """Checkpointer used to save and load graph state. Defaults to None.""" retry_policy: Optional[RetryPolicy] = None @@ -275,6 +280,7 @@ class Pregel( + ( self.checkpointer.config_specs if self.checkpointer is not None + and self.checkpointer != INHERIT_CHECKPOINTER else [] ) + ( @@ -950,10 +956,23 @@ class Pregel( if config and config.get("configurable", {}).get(CONFIG_KEY_READ) is not None: # if being called as a node in another graph, always use values mode stream_mode = ["values"] + + if self.checkpointer is None: + raise ValueError( + "Missing checkpointer for subgraph. " + "Please compile the subgraph graph with checkpointer=INHERIT_CHECKPOINTER (from langgraph.pregel import INHERIT_CHECKPOINTER)." + ) + + if self.checkpointer != INHERIT_CHECKPOINTER: + raise ValueError( + "Custom checkpointers for subgraphs are not allowed. " + "Please compile the subgraph graph with checkpointer=INHERIT_CHECKPOINTER (from langgraph.pregel import INHERIT_CHECKPOINTER)." + ) + if ( config is not None and config.get("configurable", {}).get(CONFIG_KEY_CHECKPOINTER) - and (interrupt_after or interrupt_before) + and self.checkpointer == INHERIT_CHECKPOINTER ): checkpointer: Optional[BaseCheckpointSaver] = config["configurable"][ CONFIG_KEY_CHECKPOINTER diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 1530fcda4..0f4fbb5b2 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -60,7 +60,13 @@ from langgraph.prebuilt.chat_agent_executor import ( create_tool_calling_executor, ) from langgraph.prebuilt.tool_node import ToolNode -from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot +from langgraph.pregel import ( + INHERIT_CHECKPOINTER, + Channel, + GraphRecursionError, + Pregel, + StateSnapshot, +) from langgraph.pregel.retry import RetryPolicy from tests.any_str import AnyStr from tests.memory_assert import ( @@ -7737,7 +7743,10 @@ def test_nested_graph_interrupts( graph = StateGraph(State) graph.add_node("outer_1", outer_1) - graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) + graph.add_node( + "inner", + inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + ) graph.add_node("outer_2", outer_2) graph.set_entry_point("outer_1") graph.add_edge("outer_1", "inner") @@ -8913,7 +8922,10 @@ def test_nested_graph_interrupts_parallel( return {"my_key": " and back again"} graph = StateGraph(State) - graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) + graph.add_node( + "inner", + inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + ) graph.add_node("outer_1", outer_1) graph.add_node("outer_2", outer_2) @@ -8997,7 +9009,6 @@ def test_nested_graph_interrupts_parallel( ] -@pytest.mark.skip @pytest.mark.parametrize( "checkpointer_name", ["memory", "sqlite", "postgres", "postgres_pipe"], @@ -9005,7 +9016,7 @@ def test_nested_graph_interrupts_parallel( def test_doubly_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: - checkpointer = request.getfixturevalue(checkpointer_name) + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) class State(TypedDict): my_key: str @@ -9032,7 +9043,12 @@ def test_doubly_nested_graph_interrupts( grandchild.set_finish_point("grandchild_2") child = StateGraph(ChildState) - child.add_node("child_1", grandchild.compile(interrupt_before=["grandchild_2"])) + child.add_node( + "child_1", + grandchild.compile( + interrupt_before=["grandchild_2"], checkpointer=INHERIT_CHECKPOINTER + ), + ) child.set_entry_point("child_1") child.set_finish_point("child_1") @@ -9044,7 +9060,7 @@ def test_doubly_nested_graph_interrupts( graph = StateGraph(State) graph.add_node("parent_1", parent_1) - graph.add_node("child", child.compile()) + graph.add_node("child", child.compile(checkpointer=INHERIT_CHECKPOINTER)) graph.add_node("parent_2", parent_2) graph.set_entry_point("parent_1") graph.add_edge("parent_1", "child") @@ -9136,7 +9152,10 @@ def test_nested_graph_state( graph = StateGraph(State) graph.add_node("outer_1", outer_1) - graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) + graph.add_node( + "inner", + inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + ) graph.add_node("outer_2", outer_2) graph.set_entry_point("outer_1") graph.add_edge("outer_1", "inner") @@ -9543,6 +9562,251 @@ def test_nested_graph_state( ] +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_doubly_nested_graph_state( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + + class State(TypedDict): + my_key: str + + class ChildState(TypedDict): + my_key: str + + class GrandChildState(TypedDict): + my_key: str + + def grandchild_1(state: ChildState): + return {"my_key": state["my_key"] + " here"} + + def grandchild_2(state: ChildState): + return { + "my_key": state["my_key"] + " and there", + } + + grandchild = StateGraph(GrandChildState) + grandchild.add_node("grandchild_1", grandchild_1) + grandchild.add_node("grandchild_2", grandchild_2) + grandchild.add_edge("grandchild_1", "grandchild_2") + grandchild.set_entry_point("grandchild_1") + grandchild.set_finish_point("grandchild_2") + + child = StateGraph(ChildState) + child.add_node( + "child_1", + grandchild.compile( + interrupt_before=["grandchild_2"], checkpointer=INHERIT_CHECKPOINTER + ), + ) + child.set_entry_point("child_1") + child.set_finish_point("child_1") + + def parent_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + def parent_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("parent_1", parent_1) + graph.add_node("child", child.compile(checkpointer=INHERIT_CHECKPOINTER)) + graph.add_node("parent_2", parent_2) + graph.set_entry_point("parent_1") + graph.add_edge("parent_1", "child") + graph.add_edge("child", "parent_2") + graph.set_finish_point("parent_2") + + app = graph.compile(checkpointer=checkpointer) + + # test invoke w/ nested interrupt + config = {"configurable": {"thread_id": "1"}} + app.invoke({"my_key": "my value"}, config, debug=True) + assert app.get_state(config) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "child": StateSnapshot( + values={"my_key": "hi my value"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "child_1": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + }, + ) + app.invoke(None, config, debug=True) + assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "parent_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "child": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "child_1": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "grandchild_2": {"my_key": "hi my value here and there"} + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + }, + ) + + def test_repeat_condition(snapshot: SnapshotAssertion) -> None: class AgentState(TypedDict): hello: str diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 064c48448..ee5efaa37 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -56,7 +56,13 @@ from langgraph.prebuilt.chat_agent_executor import ( ) from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.prebuilt.tool_node import ToolNode -from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot +from langgraph.pregel import ( + INHERIT_CHECKPOINTER, + Channel, + GraphRecursionError, + Pregel, + StateSnapshot, +) from langgraph.pregel.retry import RetryPolicy from tests.any_str import AnyStr from tests.memory_assert import ( @@ -6232,7 +6238,10 @@ async def test_nested_graph_interrupts( graph = StateGraph(State) graph.add_node("outer_1", outer_1) - graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) + graph.add_node( + "inner", + inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + ) graph.add_node("outer_2", outer_2) graph.set_entry_point("outer_1") graph.add_edge("outer_1", "inner") @@ -7415,7 +7424,10 @@ async def test_nested_graph_interrupts_parallel( return {"my_key": " and back again"} graph = StateGraph(State) - graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) + graph.add_node( + "inner", + inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + ) graph.add_node("outer_1", outer_1) graph.add_node("outer_2", outer_2) @@ -7501,7 +7513,6 @@ async def test_nested_graph_interrupts_parallel( ] -@pytest.mark.skip @pytest.mark.parametrize( "checkpointer_name", ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], @@ -7536,7 +7547,12 @@ async def test_doubly_nested_graph_interrupts( grandchild.set_finish_point("grandchild_2") child = StateGraph(ChildState) - child.add_node("child_1", grandchild.compile(interrupt_before=["grandchild_2"])) + child.add_node( + "child_1", + grandchild.compile( + interrupt_before=["grandchild_2"], checkpointer=INHERIT_CHECKPOINTER + ), + ) child.set_entry_point("child_1") child.set_finish_point("child_1") @@ -7548,7 +7564,7 @@ async def test_doubly_nested_graph_interrupts( graph = StateGraph(State) graph.add_node("parent_1", parent_1) - graph.add_node("child", child.compile()) + graph.add_node("child", child.compile(checkpointer=INHERIT_CHECKPOINTER)) graph.add_node("parent_2", parent_2) graph.set_entry_point("parent_1") graph.add_edge("parent_1", "child") @@ -7643,7 +7659,10 @@ async def test_nested_graph_state( graph = StateGraph(State) graph.add_node("outer_1", outer_1) - graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) + graph.add_node( + "inner", + inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + ) graph.add_node("outer_2", outer_2) graph.set_entry_point("outer_1") graph.add_edge("outer_1", "inner") @@ -8052,6 +8071,251 @@ async def test_nested_graph_state( ] +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], +) +async def test_doubly_nested_graph_state( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + + class State(TypedDict): + my_key: str + + class ChildState(TypedDict): + my_key: str + + class GrandChildState(TypedDict): + my_key: str + + def grandchild_1(state: ChildState): + return {"my_key": state["my_key"] + " here"} + + def grandchild_2(state: ChildState): + return { + "my_key": state["my_key"] + " and there", + } + + grandchild = StateGraph(GrandChildState) + grandchild.add_node("grandchild_1", grandchild_1) + grandchild.add_node("grandchild_2", grandchild_2) + grandchild.add_edge("grandchild_1", "grandchild_2") + grandchild.set_entry_point("grandchild_1") + grandchild.set_finish_point("grandchild_2") + + child = StateGraph(ChildState) + child.add_node( + "child_1", + grandchild.compile( + interrupt_before=["grandchild_2"], checkpointer=INHERIT_CHECKPOINTER + ), + ) + child.set_entry_point("child_1") + child.set_finish_point("child_1") + + def parent_1(state: State): + return {"my_key": "hi " + state["my_key"]} + + def parent_2(state: State): + return {"my_key": state["my_key"] + " and back again"} + + graph = StateGraph(State) + graph.add_node("parent_1", parent_1) + graph.add_node("child", child.compile(checkpointer=INHERIT_CHECKPOINTER)) + graph.add_node("parent_2", parent_2) + graph.set_entry_point("parent_1") + graph.add_edge("parent_1", "child") + graph.add_edge("child", "parent_2") + graph.set_finish_point("parent_2") + + app = graph.compile(checkpointer=checkpointer) + + # test invoke w/ nested interrupt + config = {"configurable": {"thread_id": "1"}} + await app.ainvoke({"my_key": "my value"}, config, debug=True) + assert await app.aget_state(config) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + assert await app.aget_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "child": StateSnapshot( + values={"my_key": "hi my value"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "child_1": StateSnapshot( + values={"my_key": "hi my value here"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + }, + ) + await app.ainvoke(None, config, debug=True) + assert await app.aget_state(config, include_subgraph_state=True) == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "parent_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "child": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "child_1": StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "grandchild_2": {"my_key": "hi my value here and there"} + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + }, + ) + + async def test_checkpoint_metadata() -> None: """This test verifies that a run's configurable fields are merged with the previous checkpoint config for each step in the run. From fb05bdc2bfda23759cc894b0332d933c775423a9 Mon Sep 17 00:00:00 2001 From: vbarda Date: Mon, 12 Aug 2024 14:45:36 -0400 Subject: [PATCH 010/287] return all checkpoints from .list --- .../langgraph/checkpoint/postgres/base.py | 3 - .../langgraph/checkpoint/sqlite/utils.py | 3 - .../langgraph/checkpoint/base/__init__.py | 2 - .../langgraph/checkpoint/memory/__init__.py | 14 +- libs/langgraph/langgraph/pregel/__init__.py | 201 +++++++----------- libs/langgraph/tests/test_pregel_async.py | 22 +- 6 files changed, 90 insertions(+), 155 deletions(-) diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py index e9ca12305..038bb6677 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py @@ -253,9 +253,6 @@ class BasePostgresSaver(BaseCheckpointSaver): if config: wheres.append("thread_id = %s ") param_values.append(config["configurable"]["thread_id"]) - checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - wheres.append("checkpoint_ns = %s") - param_values.append(checkpoint_ns) # construct predicate for metadata filter if filter: diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py index 6e1baf5ae..56034ea34 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py @@ -70,9 +70,6 @@ def search_where( if config is not None: wheres.append("thread_id = ?") param_values.append(config["configurable"]["thread_id"]) - checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - wheres.append("checkpoint_ns = ?") - param_values.append(checkpoint_ns) # construct predicate for metadata filter if filter: diff --git a/libs/checkpoint/langgraph/checkpoint/base/__init__.py b/libs/checkpoint/langgraph/checkpoint/base/__init__.py index 4f0f08a84..86c8b0eec 100644 --- a/libs/checkpoint/langgraph/checkpoint/base/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/base/__init__.py @@ -259,7 +259,6 @@ class BaseCheckpointSaver(ABC): filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, - include_nested_checkpoints: bool = False, ) -> Iterator[CheckpointTuple]: """List checkpoints that match the given criteria. @@ -351,7 +350,6 @@ class BaseCheckpointSaver(ABC): filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, - include_nested_checkpoints: bool = False, ) -> AsyncIterator[CheckpointTuple]: """Asynchronously list checkpoints that match the given criteria. diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 5e60ebaf0..3bb7f8803 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -157,7 +157,6 @@ class MemorySaver( filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, - include_nested_checkpoints: bool = False, ) -> Iterator[CheckpointTuple]: """List checkpoints from the in-memory storage. @@ -178,16 +177,7 @@ class MemorySaver( config["configurable"].get("checkpoint_ns", "") if config else "" ) for thread_id in thread_ids: - checkpoint_ns_iter = ( - ( - key - for key in self.storage[thread_id].keys() - if key.startswith(checkpoint_ns) - ) - if include_nested_checkpoints - else [checkpoint_ns] - ) - for checkpoint_ns in checkpoint_ns_iter: + for checkpoint_ns in self.storage[thread_id].keys(): for checkpoint_id, ( checkpoint, metadata_b, @@ -330,7 +320,6 @@ class MemorySaver( filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, - include_nested_checkpoints: bool = False, ) -> AsyncIterator[CheckpointTuple]: """Asynchronous version of list. @@ -351,7 +340,6 @@ class MemorySaver( before=before, limit=limit, filter=filter, - include_nested_checkpoints=include_nested_checkpoints, ), config, ) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index dd105e0ba..9c591979f 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -360,70 +360,64 @@ class Pregel( if is_managed_value(v) } - def _prepare_state_snapshot( - self, saved: CheckpointTuple, config: RunnableConfig - ) -> StateSnapshot: - checkpoint = saved.checkpoint if saved else empty_checkpoint() - config = saved.config if saved else config + def _prepare_state_snapshot(self, saved: CheckpointTuple) -> StateSnapshot: with ChannelsManager( { k: LastValue(None) if isinstance(c, Context) else c for k, c in self.channels.items() }, - checkpoint, - config, + saved.checkpoint, + saved.config, ) as channels, ManagedValuesManager( - self.managed_values_dict, ensure_config(config) + self.managed_values_dict, ensure_config(saved.config) ) as managed: next_tasks = prepare_next_tasks( - checkpoint, + saved.checkpoint, self.nodes, channels, managed, - config, + saved.config, -1, for_execution=False, ) return StateSnapshot( values=read_channels(channels, self.stream_channels_asis), next=tuple(t.name for t in next_tasks), - config=saved.config if saved else config, - metadata=saved.metadata if saved else None, - created_at=saved.checkpoint["ts"] if saved else None, - parent_config=saved.parent_config if saved else None, + config=saved.config, + metadata=saved.metadata, + created_at=saved.checkpoint["ts"], + parent_config=saved.parent_config, ) async def _prepare_state_snapshot_async( - self, saved: CheckpointTuple, config: RunnableConfig + self, saved: CheckpointTuple ) -> StateSnapshot: - checkpoint = saved.checkpoint if saved else empty_checkpoint() - config = saved.config if saved else config async with AsyncChannelsManager( { k: LastValue(None) if isinstance(c, Context) else c for k, c in self.channels.items() }, - checkpoint, - config, + saved.checkpoint, + saved.config, ) as channels, AsyncManagedValuesManager( - self.managed_values_dict, ensure_config(config) + self.managed_values_dict, ensure_config(saved.config) ) as managed: next_tasks = prepare_next_tasks( - checkpoint, + saved.checkpoint, self.nodes, channels, managed, - config, + saved.config, -1, for_execution=False, ) return StateSnapshot( values=read_channels(channels, self.stream_channels_asis), next=tuple(t.name for t in next_tasks), - config=saved.config if saved else config, - metadata=saved.metadata if saved else None, - created_at=saved.checkpoint["ts"] if saved else None, - parent_config=saved.parent_config if saved else None, + config=saved.config, + metadata=saved.metadata, + created_at=saved.checkpoint["ts"], + parent_config=saved.parent_config, ) @staticmethod @@ -457,7 +451,9 @@ class Pregel( state_snapshot = checkpoint_ns_to_state_snapshots.pop(root_checkpoint_ns, None) if state_snapshot is None: - raise ValueError(f"Missing checkpoint for thread ID '{root_checkpoint_ns}'") + raise ValueError( + f"Missing checkpoint for checkpoint NS '{root_checkpoint_ns}'" + ) return state_snapshot def get_state( @@ -468,9 +464,7 @@ class Pregel( raise ValueError("No checkpointer set") if include_subgraph_state: - checkpoint_tuples = self.checkpointer.list( - config, include_nested_checkpoints=True - ) + checkpoint_tuples = self.checkpointer.list(config) else: checkpoint_tuples = iter([self.checkpointer.get_tuple(config)]) @@ -496,20 +490,16 @@ class Pregel( existing_checkpoint_id is None or saved_checkpoint_id > existing_checkpoint_id ): - state_snapshot = self._prepare_state_snapshot(checkpoint_tuple, config) + state_snapshot = self._prepare_state_snapshot(checkpoint_tuple) checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot checkpoint_ns_to_checkpoint_id[ saved_checkpoint_ns ] = saved_checkpoint_id if not checkpoint_ns_to_state_snapshots: - error_msg = ( - f"Could not find checkpoints for checkpoint NS '{checkpoint_ns}'" + return StateSnapshot( + values={}, next=(), config=config, checkpoint=empty_checkpoint() ) - if checkpoint_id: - error_msg += f" and checkpoint ID '{checkpoint_id}'" - - raise ValueError(error_msg) state_snapshot = self._assemble_state_snapshot_hierarchy( checkpoint_ns, checkpoint_ns_to_state_snapshots @@ -524,9 +514,7 @@ class Pregel( raise ValueError("No checkpointer set") if include_subgraph_state: - checkpoint_tuples = self.checkpointer.alist( - config, include_nested_checkpoints=True - ) + checkpoint_tuples = self.checkpointer.alist(config) else: async def alist_checkpoints(): @@ -556,20 +544,18 @@ class Pregel( existing_checkpoint_id is None or saved_checkpoint_id > existing_checkpoint_id ): - state_snapshot = self._prepare_state_snapshot(checkpoint_tuple, config) + state_snapshot = await self._prepare_state_snapshot_async( + checkpoint_tuple + ) checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot checkpoint_ns_to_checkpoint_id[ saved_checkpoint_ns ] = saved_checkpoint_id if not checkpoint_ns_to_state_snapshots: - error_msg = ( - f"Could not find checkpoints for checkpoint NS '{checkpoint_ns}'" + return StateSnapshot( + values={}, next=(), config=config, checkpoint=empty_checkpoint() ) - if checkpoint_id: - error_msg += f" and checkpoint ID '{checkpoint_id}'" - - raise ValueError(error_msg) state_snapshot = self._assemble_state_snapshot_hierarchy( checkpoint_ns, checkpoint_ns_to_state_snapshots @@ -593,41 +579,25 @@ class Pregel( and signature(self.checkpointer.list).parameters.get("filter") is None ): raise ValueError("Checkpointer does not support filtering") - for config, checkpoint, metadata, parent_config, _ in self.checkpointer.list( + + checkpoint_ns = config["configurable"].get("checkpoint_ns", "") + for checkpoint_tuple in self.checkpointer.list( config, before=before, limit=limit, filter=filter ): + if ( + checkpoint_tuple.config["configurable"]["checkpoint_ns"] + != checkpoint_ns + ): + # only list root checkpoints here + continue + if include_subgraph_state: - state_snapshot = self.get_state(config, include_subgraph_state=True) + state_snapshot = self.get_state( + checkpoint_tuple.config, include_subgraph_state=True + ) yield state_snapshot else: - with ChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() - }, - checkpoint, - config, - ) as channels, ManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: - next_tasks = prepare_next_tasks( - checkpoint, - self.nodes, - channels, - managed, - config, - -1, - for_execution=False, - ) - - yield StateSnapshot( - read_channels(channels, self.stream_channels_asis), - tuple(t.name for t in next_tasks), - config, - metadata, - checkpoint["ts"], - parent_config, - ) + yield self._prepare_state_snapshot(checkpoint_tuple) async def aget_state_history( self, @@ -646,46 +616,25 @@ class Pregel( and signature(self.checkpointer.list).parameters.get("filter") is None ): raise ValueError("Checkpointer does not support filtering") - async for ( - config, - checkpoint, - metadata, - parent_config, - _, - ) in self.checkpointer.alist(config, before=before, limit=limit, filter=filter): + + checkpoint_ns = config["configurable"].get("checkpoint_ns", "") + async for checkpoint_tuple in self.checkpointer.alist( + config, before=before, limit=limit, filter=filter + ): + if ( + checkpoint_tuple.config["configurable"]["checkpoint_ns"] + != checkpoint_ns + ): + # only list root checkpoints here + continue + if include_subgraph_state: state_snapshot = await self.aget_state( - config, include_subgraph_state=True + checkpoint_tuple.config, include_subgraph_state=True ) yield state_snapshot else: - async with AsyncChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() - }, - checkpoint, - config, - ) as channels, AsyncManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: - next_tasks = prepare_next_tasks( - checkpoint, - self.nodes, - channels, - managed, - config, - -1, - for_execution=False, - ) - yield StateSnapshot( - read_channels(channels, self.stream_channels_asis), - tuple(t.name for t in next_tasks), - config, - metadata, - checkpoint["ts"], - parent_config, - ) + yield await self._prepare_state_snapshot_async(checkpoint_tuple) def update_state( self, @@ -953,32 +902,36 @@ class Pregel( stream_mode = stream_mode if stream_mode is not None else self.stream_mode if not isinstance(stream_mode, list): stream_mode = [stream_mode] + + if config and config.get("configurable", {}).get(CONFIG_KEY_CHECKPOINTER): + parent_checkpointer = config["configurable"][CONFIG_KEY_CHECKPOINTER] + else: + parent_checkpointer = None + if config and config.get("configurable", {}).get(CONFIG_KEY_READ) is not None: # if being called as a node in another graph, always use values mode stream_mode = ["values"] - if self.checkpointer is None: + if parent_checkpointer is not None and self.checkpointer is None: raise ValueError( "Missing checkpointer for subgraph. " "Please compile the subgraph graph with checkpointer=INHERIT_CHECKPOINTER (from langgraph.pregel import INHERIT_CHECKPOINTER)." ) - if self.checkpointer != INHERIT_CHECKPOINTER: + if ( + parent_checkpointer is not None + and self.checkpointer != INHERIT_CHECKPOINTER + ): raise ValueError( "Custom checkpointers for subgraphs are not allowed. " "Please compile the subgraph graph with checkpointer=INHERIT_CHECKPOINTER (from langgraph.pregel import INHERIT_CHECKPOINTER)." ) - if ( - config is not None - and config.get("configurable", {}).get(CONFIG_KEY_CHECKPOINTER) - and self.checkpointer == INHERIT_CHECKPOINTER - ): - checkpointer: Optional[BaseCheckpointSaver] = config["configurable"][ - CONFIG_KEY_CHECKPOINTER - ] - else: - checkpointer = self.checkpointer + checkpointer = ( + parent_checkpointer + if parent_checkpointer is not None + else self.checkpointer + ) return ( debug, stream_mode, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index ee5efaa37..b94a73494 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -7629,13 +7629,13 @@ async def test_nested_graph_state( my_key: str my_other_key: str - def inner_1(state: InnerState): + async def inner_1(state: InnerState): return { "my_key": state["my_key"] + " here", "my_other_key": state["my_key"], } - def inner_2(state: InnerState): + async def inner_2(state: InnerState): return { "my_key": state["my_key"] + " and there", "my_other_key": state["my_key"], @@ -7651,10 +7651,10 @@ async def test_nested_graph_state( class State(TypedDict): my_key: str - def outer_1(state: State): + async def outer_1(state: State): return {"my_key": "hi " + state["my_key"]} - def outer_2(state: State): + async def outer_2(state: State): return {"my_key": state["my_key"] + " and back again"} graph = StateGraph(State) @@ -7699,7 +7699,7 @@ async def test_nested_graph_state( }, subgraph_state_snapshots=None, ) - assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + assert await app.aget_state(config, include_subgraph_state=True) == StateSnapshot( values={"my_key": "hi my value"}, next=("inner",), config={ @@ -7755,7 +7755,9 @@ async def test_nested_graph_state( ) }, ) - assert list(app.get_state_history(config, include_subgraph_state=True)) == [ + assert [ + s async for s in app.aget_state_history(config, include_subgraph_state=True) + ] == [ StateSnapshot( values={"my_key": "hi my value"}, next=("inner",), @@ -8089,10 +8091,10 @@ async def test_doubly_nested_graph_state( class GrandChildState(TypedDict): my_key: str - def grandchild_1(state: ChildState): + async def grandchild_1(state: ChildState): return {"my_key": state["my_key"] + " here"} - def grandchild_2(state: ChildState): + async def grandchild_2(state: ChildState): return { "my_key": state["my_key"] + " and there", } @@ -8114,10 +8116,10 @@ async def test_doubly_nested_graph_state( child.set_entry_point("child_1") child.set_finish_point("child_1") - def parent_1(state: State): + async def parent_1(state: State): return {"my_key": "hi " + state["my_key"]} - def parent_2(state: State): + async def parent_2(state: State): return {"my_key": state["my_key"] + " and back again"} graph = StateGraph(State) From 503304458721134eb927a3dc057d0061e76c8e37 Mon Sep 17 00:00:00 2001 From: vbarda Date: Mon, 12 Aug 2024 16:12:47 -0400 Subject: [PATCH 011/287] update checkpointer tests --- libs/checkpoint-postgres/tests/test_async.py | 26 +++++-------------- libs/checkpoint-postgres/tests/test_sync.py | 25 +++++------------- .../checkpoint-sqlite/tests/test_aiosqlite.py | 26 +++++-------------- libs/checkpoint-sqlite/tests/test_sqlite.py | 25 +++++------------- .../langgraph/checkpoint/memory/__init__.py | 5 +--- libs/checkpoint/tests/test_memory.py | 23 +++++++--------- 6 files changed, 34 insertions(+), 96 deletions(-) diff --git a/libs/checkpoint-postgres/tests/test_async.py b/libs/checkpoint-postgres/tests/test_async.py index e94cf32ae..6f9f7d78b 100644 --- a/libs/checkpoint-postgres/tests/test_async.py +++ b/libs/checkpoint-postgres/tests/test_async.py @@ -87,29 +87,15 @@ class TestAsyncPostgresSaver: search_results_4 = [c async for c in saver.alist(None, filter=query_4)] assert len(search_results_4) == 0 - # search by config (defaults to root graph checkpoints) + # search by config (defaults to checkpoints across all namespaces) search_results_5 = [ c async for c in saver.alist({"configurable": {"thread_id": "thread-2"}}) ] - assert len(search_results_5) == 1 - assert search_results_5[0].config["configurable"]["checkpoint_ns"] == "" - - # search by config and checkpoint_ns - search_results_6 = [ - c - async for c in saver.alist( - { - "configurable": { - "thread_id": "thread-2", - "checkpoint_ns": "inner", - } - } - ) - ] - assert len(search_results_6) == 1 - assert ( - search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner" - ) + assert len(search_results_5) == 2 + assert { + search_results_5[0].config["configurable"]["checkpoint_ns"], + search_results_5[1].config["configurable"]["checkpoint_ns"], + } == {"", "inner"} # TODO: test before and limit params diff --git a/libs/checkpoint-postgres/tests/test_sync.py b/libs/checkpoint-postgres/tests/test_sync.py index dfae82907..a2fbcbd88 100644 --- a/libs/checkpoint-postgres/tests/test_sync.py +++ b/libs/checkpoint-postgres/tests/test_sync.py @@ -88,27 +88,14 @@ class TestPostgresSaver: search_results_4 = list(saver.list(None, filter=query_4)) assert len(search_results_4) == 0 - # search by config (defaults to root graph checkpoints) + # search by config (defaults to checkpoints across all namespaces) search_results_5 = list( saver.list({"configurable": {"thread_id": "thread-2"}}) ) - assert len(search_results_5) == 1 - assert search_results_5[0].config["configurable"]["checkpoint_ns"] == "" - - # search by config and checkpoint_ns - search_results_6 = list( - saver.list( - { - "configurable": { - "thread_id": "thread-2", - "checkpoint_ns": "inner", - } - } - ) - ) - assert len(search_results_6) == 1 - assert ( - search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner" - ) + assert len(search_results_5) == 2 + assert { + search_results_5[0].config["configurable"]["checkpoint_ns"], + search_results_5[1].config["configurable"]["checkpoint_ns"], + } == {"", "inner"} # TODO: test before and limit params diff --git a/libs/checkpoint-sqlite/tests/test_aiosqlite.py b/libs/checkpoint-sqlite/tests/test_aiosqlite.py index 59f830dae..038030172 100644 --- a/libs/checkpoint-sqlite/tests/test_aiosqlite.py +++ b/libs/checkpoint-sqlite/tests/test_aiosqlite.py @@ -84,29 +84,15 @@ class TestAsyncSqliteSaver: search_results_4 = [c async for c in saver.alist(None, filter=query_4)] assert len(search_results_4) == 0 - # search by config (defaults to root graph checkpoints) + # search by config (defaults to checkpoints across all namespaces) search_results_5 = [ c async for c in saver.alist({"configurable": {"thread_id": "thread-2"}}) ] - assert len(search_results_5) == 1 - assert search_results_5[0].config["configurable"]["checkpoint_ns"] == "" - - # search by config and checkpoint_ns - search_results_6 = [ - c - async for c in saver.alist( - { - "configurable": { - "thread_id": "thread-2", - "checkpoint_ns": "inner", - } - } - ) - ] - assert len(search_results_6) == 1 - assert ( - search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner" - ) + assert len(search_results_5) == 2 + assert { + search_results_5[0].config["configurable"]["checkpoint_ns"], + search_results_5[1].config["configurable"]["checkpoint_ns"], + } == {"", "inner"} # TODO: test before and limit params diff --git a/libs/checkpoint-sqlite/tests/test_sqlite.py b/libs/checkpoint-sqlite/tests/test_sqlite.py index 2147cca87..99b7a3728 100644 --- a/libs/checkpoint-sqlite/tests/test_sqlite.py +++ b/libs/checkpoint-sqlite/tests/test_sqlite.py @@ -87,28 +87,15 @@ class TestSqliteSaver: search_results_4 = list(saver.list(None, filter=query_4)) assert len(search_results_4) == 0 - # search by config (defaults to root graph checkpoints) + # search by config (defaults to checkpoints across all namespaces) search_results_5 = list( saver.list({"configurable": {"thread_id": "thread-2"}}) ) - assert len(search_results_5) == 1 - assert search_results_5[0].config["configurable"]["checkpoint_ns"] == "" - - # search by config and checkpoint_ns - search_results_6 = list( - saver.list( - { - "configurable": { - "thread_id": "thread-2", - "checkpoint_ns": "inner", - } - } - ) - ) - assert len(search_results_6) == 1 - assert ( - search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner" - ) + assert len(search_results_5) == 2 + assert { + search_results_5[0].config["configurable"]["checkpoint_ns"], + search_results_5[1].config["configurable"]["checkpoint_ns"], + } == {"", "inner"} # TODO: test before and limit params diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 3bb7f8803..76901e2e0 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -173,9 +173,6 @@ class MemorySaver( Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples. """ thread_ids = (config["configurable"]["thread_id"],) if config else self.storage - checkpoint_ns = ( - config["configurable"].get("checkpoint_ns", "") if config else "" - ) for thread_id in thread_ids: for checkpoint_ns in self.storage[thread_id].keys(): for checkpoint_id, ( @@ -198,7 +195,7 @@ class MemorySaver( # filter by metadata metadata = self.serde.loads_typed(metadata_b) if filter and not all( - query_value == metadata[query_key] + query_value == metadata.get(query_key) for query_key, query_value in filter.items() ): continue diff --git a/libs/checkpoint/tests/test_memory.py b/libs/checkpoint/tests/test_memory.py index a0bc8d738..34c13b2d0 100644 --- a/libs/checkpoint/tests/test_memory.py +++ b/libs/checkpoint/tests/test_memory.py @@ -82,26 +82,20 @@ class TestMemorySaver: assert search_results_2[0].metadata == self.metadata_2 search_results_3 = list(self.memory_saver.list(None, filter=query_3)) - assert len(search_results_3) == 2 + assert len(search_results_3) == 3 search_results_4 = list(self.memory_saver.list(None, filter=query_4)) assert len(search_results_4) == 0 - # search by config (defaults to root graph checkpoints) + # search by config (defaults to checkpoints across all namespaces) search_results_5 = list( self.memory_saver.list({"configurable": {"thread_id": "thread-2"}}) ) - assert len(search_results_5) == 1 - assert search_results_5[0].config["configurable"]["checkpoint_ns"] == "" - - # search by config and checkpoint_ns - search_results_6 = list( - self.memory_saver.list( - {"configurable": {"thread_id": "thread-2", "checkpoint_ns": "inner"}} - ) - ) - assert len(search_results_6) == 1 - assert search_results_6[0].config["configurable"]["checkpoint_ns"] == "inner" + assert len(search_results_5) == 2 + assert { + search_results_5[0].config["configurable"]["checkpoint_ns"], + search_results_5[1].config["configurable"]["checkpoint_ns"], + } == {"", "inner"} # TODO: test before and limit params @@ -110,6 +104,7 @@ class TestMemorySaver: # save checkpoints self.memory_saver.put(self.config_1, self.chkpnt_1, self.metadata_1, {}) self.memory_saver.put(self.config_2, self.chkpnt_2, self.metadata_2, {}) + self.memory_saver.put(self.config_3, self.chkpnt_3, self.metadata_3, {}) # call method / assertions query_1: CheckpointMetadata = {"source": "input"} # search by 1 key @@ -135,7 +130,7 @@ class TestMemorySaver: search_results_3 = [ c async for c in self.memory_saver.alist(None, filter=query_3) ] - assert len(search_results_3) == 2 + assert len(search_results_3) == 3 search_results_4 = [ c async for c in self.memory_saver.alist(None, filter=query_4) From 6d4cdc94560b66dc424628115e49da1d45faed79 Mon Sep 17 00:00:00 2001 From: vbarda Date: Mon, 12 Aug 2024 16:29:15 -0400 Subject: [PATCH 012/287] opt-in --- libs/langgraph/langgraph/pregel/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 9c591979f..bf491c6e3 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -912,9 +912,9 @@ class Pregel( # if being called as a node in another graph, always use values mode stream_mode = ["values"] - if parent_checkpointer is not None and self.checkpointer is None: + if (interrupt_before or interrupt_after) and parent_checkpointer is not None and self.checkpointer is None: raise ValueError( - "Missing checkpointer for subgraph. " + "Missing checkpointer for a subgraph with interrupts. " "Please compile the subgraph graph with checkpointer=INHERIT_CHECKPOINTER (from langgraph.pregel import INHERIT_CHECKPOINTER)." ) From abe9b7c08eb887932a0f553f87040043ac2c4b13 Mon Sep 17 00:00:00 2001 From: vbarda Date: Mon, 12 Aug 2024 16:32:22 -0400 Subject: [PATCH 013/287] lint --- libs/langgraph/langgraph/pregel/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index bf491c6e3..d2b996d08 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -912,7 +912,11 @@ class Pregel( # if being called as a node in another graph, always use values mode stream_mode = ["values"] - if (interrupt_before or interrupt_after) and parent_checkpointer is not None and self.checkpointer is None: + if ( + (interrupt_before or interrupt_after) + and parent_checkpointer is not None + and self.checkpointer is None + ): raise ValueError( "Missing checkpointer for a subgraph with interrupts. " "Please compile the subgraph graph with checkpointer=INHERIT_CHECKPOINTER (from langgraph.pregel import INHERIT_CHECKPOINTER)." From f65d9b2b7d05b4cc0d09a41c346b177ac81042f8 Mon Sep 17 00:00:00 2001 From: vbarda Date: Mon, 12 Aug 2024 19:50:07 -0400 Subject: [PATCH 014/287] pass subgraph nodes/channels --- libs/langgraph/langgraph/pregel/__init__.py | 85 ++++++++++++++++++--- libs/langgraph/tests/test_pregel.py | 10 +-- libs/langgraph/tests/test_pregel_async.py | 10 +-- 3 files changed, 85 insertions(+), 20 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index d2b996d08..13aacf0e5 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -360,11 +360,41 @@ class Pregel( if is_managed_value(v) } - def _prepare_state_snapshot(self, saved: CheckpointTuple) -> StateSnapshot: + def _get_nodes_and_channels( + self, checkpoint_ns: str + ) -> tuple[Mapping[str, PregelNode], Mapping[str, BaseChannel]]: + if checkpoint_ns == "": + return self.nodes, self.channels + + path = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) + nodes = self.nodes + channels = self.channels + for subgraph_node_name in path: + if subgraph_node_name not in nodes: + raise ValueError(f"Couldn't find node '{subgraph_node_name}'.") + + subgraph_node = nodes[subgraph_node_name].get_node() + + if not isinstance(subgraph_node, RunnableSequence): + break + + first_step = subgraph_node.steps[0] + if isinstance(first_step, Pregel): + nodes = first_step.nodes + channels = first_step.channels + + return nodes, channels + + def _prepare_state_snapshot( + self, + saved: CheckpointTuple, + nodes: Mapping[str, PregelNode], + channels: Mapping[str, BaseChannel], + ) -> StateSnapshot: with ChannelsManager( { k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() + for k, c in channels.items() }, saved.checkpoint, saved.config, @@ -373,7 +403,7 @@ class Pregel( ) as managed: next_tasks = prepare_next_tasks( saved.checkpoint, - self.nodes, + nodes, channels, managed, saved.config, @@ -390,12 +420,15 @@ class Pregel( ) async def _prepare_state_snapshot_async( - self, saved: CheckpointTuple + self, + saved: CheckpointTuple, + nodes: Mapping[str, PregelNode], + channels: Mapping[str, BaseChannel], ) -> StateSnapshot: async with AsyncChannelsManager( { k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() + for k, c in channels.items() }, saved.checkpoint, saved.config, @@ -404,7 +437,7 @@ class Pregel( ) as managed: next_tasks = prepare_next_tasks( saved.checkpoint, - self.nodes, + nodes, channels, managed, saved.config, @@ -472,6 +505,9 @@ class Pregel( checkpoint_id = config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} + checkpoint_ns_to_nodes_and_channels: dict[ + str, tuple[Mapping[str, PregelNode], Mapping[str, BaseChannel]] + ] = {} for checkpoint_tuple in checkpoint_tuples: saved_checkpoint_ns = checkpoint_tuple.config["configurable"][ "checkpoint_ns" @@ -490,7 +526,17 @@ class Pregel( existing_checkpoint_id is None or saved_checkpoint_id > existing_checkpoint_id ): - state_snapshot = self._prepare_state_snapshot(checkpoint_tuple) + if saved_checkpoint_ns not in checkpoint_ns_to_nodes_and_channels: + checkpoint_ns_to_nodes_and_channels[ + saved_checkpoint_ns + ] = self._get_nodes_and_channels(saved_checkpoint_ns) + + nodes, channels = checkpoint_ns_to_nodes_and_channels[ + saved_checkpoint_ns + ] + state_snapshot = self._prepare_state_snapshot( + checkpoint_tuple, nodes, channels + ) checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot checkpoint_ns_to_checkpoint_id[ saved_checkpoint_ns @@ -526,6 +572,9 @@ class Pregel( checkpoint_id = config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} + checkpoint_ns_to_nodes_and_channels: dict[ + str, tuple[Mapping[str, PregelNode], Mapping[str, BaseChannel]] + ] = {} async for checkpoint_tuple in checkpoint_tuples: saved_checkpoint_ns = checkpoint_tuple.config["configurable"][ "checkpoint_ns" @@ -544,8 +593,16 @@ class Pregel( existing_checkpoint_id is None or saved_checkpoint_id > existing_checkpoint_id ): + if saved_checkpoint_ns not in checkpoint_ns_to_nodes_and_channels: + checkpoint_ns_to_nodes_and_channels[ + saved_checkpoint_ns + ] = self._get_nodes_and_channels(saved_checkpoint_ns) + + nodes, channels = checkpoint_ns_to_nodes_and_channels[ + saved_checkpoint_ns + ] state_snapshot = await self._prepare_state_snapshot_async( - checkpoint_tuple + checkpoint_tuple, nodes, channels ) checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot checkpoint_ns_to_checkpoint_id[ @@ -597,7 +654,10 @@ class Pregel( ) yield state_snapshot else: - yield self._prepare_state_snapshot(checkpoint_tuple) + nodes, channels = self._get_nodes_and_channels( + checkpoint_tuple.config["configurable"]["checkpoint_ns"] + ) + yield self._prepare_state_snapshot(checkpoint_tuple, nodes, channels) async def aget_state_history( self, @@ -634,7 +694,12 @@ class Pregel( ) yield state_snapshot else: - yield await self._prepare_state_snapshot_async(checkpoint_tuple) + nodes, channels = self._get_nodes_and_channels( + checkpoint_tuple.config["configurable"]["checkpoint_ns"] + ) + yield await self._prepare_state_snapshot_async( + checkpoint_tuple, nodes, channels + ) def update_state( self, diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 0f4fbb5b2..8780bc042 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -8604,7 +8604,7 @@ def test_nested_graph_interrupts( assert child_state_history == [ StateSnapshot( values={"my_key": "hi my value here"}, - next=(), + next=("inner_2",), config={ "configurable": { "thread_id": "6", @@ -9218,7 +9218,7 @@ def test_nested_graph_state( subgraph_state_snapshots={ "inner": StateSnapshot( values={"my_key": "hi my value here"}, - next=(), + next=("inner_2",), config={ "configurable": { "thread_id": "1", @@ -9275,7 +9275,7 @@ def test_nested_graph_state( subgraph_state_snapshots={ "inner": StateSnapshot( values={"my_key": "hi my value here"}, - next=(), + next=("inner_2",), config={ "configurable": { "thread_id": "1", @@ -9676,7 +9676,7 @@ def test_doubly_nested_graph_state( subgraph_state_snapshots={ "child": StateSnapshot( values={"my_key": "hi my value"}, - next=(), + next=("child_1",), config={ "configurable": { "thread_id": "1", @@ -9696,7 +9696,7 @@ def test_doubly_nested_graph_state( subgraph_state_snapshots={ "child_1": StateSnapshot( values={"my_key": "hi my value here"}, - next=(), + next=("grandchild_2",), config={ "configurable": { "thread_id": "1", diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index b94a73494..af624f639 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -7106,7 +7106,7 @@ async def test_nested_graph_interrupts( assert child_state_history == [ StateSnapshot( values={"my_key": "hi my value here"}, - next=(), + next=("inner_2",), config={ "configurable": { "thread_id": "6", @@ -7725,7 +7725,7 @@ async def test_nested_graph_state( subgraph_state_snapshots={ "inner": StateSnapshot( values={"my_key": "hi my value here"}, - next=(), + next=("inner_2",), config={ "configurable": { "thread_id": "1", @@ -7784,7 +7784,7 @@ async def test_nested_graph_state( subgraph_state_snapshots={ "inner": StateSnapshot( values={"my_key": "hi my value here"}, - next=(), + next=("inner_2",), config={ "configurable": { "thread_id": "1", @@ -8187,7 +8187,7 @@ async def test_doubly_nested_graph_state( subgraph_state_snapshots={ "child": StateSnapshot( values={"my_key": "hi my value"}, - next=(), + next=("child_1",), config={ "configurable": { "thread_id": "1", @@ -8207,7 +8207,7 @@ async def test_doubly_nested_graph_state( subgraph_state_snapshots={ "child_1": StateSnapshot( values={"my_key": "hi my value here"}, - next=(), + next=("grandchild_2",), config={ "configurable": { "thread_id": "1", From 0135c6f743742af1887dca841939c3f5c69c4d4e Mon Sep 17 00:00:00 2001 From: vbarda Date: Tue, 13 Aug 2024 10:07:52 -0400 Subject: [PATCH 015/287] correct check for using parent checkpointer --- libs/langgraph/langgraph/pregel/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 13aacf0e5..0b7dd76c9 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -989,6 +989,7 @@ class Pregel( if ( parent_checkpointer is not None + and self.checkpointer is not None and self.checkpointer != INHERIT_CHECKPOINTER ): raise ValueError( @@ -996,11 +997,14 @@ class Pregel( "Please compile the subgraph graph with checkpointer=INHERIT_CHECKPOINTER (from langgraph.pregel import INHERIT_CHECKPOINTER)." ) - checkpointer = ( - parent_checkpointer - if parent_checkpointer is not None - else self.checkpointer - ) + if ( + parent_checkpointer is not None + and self.checkpointer == INHERIT_CHECKPOINTER + ): + checkpointer = parent_checkpointer + else: + checkpointer = self.checkpointer + return ( debug, stream_mode, From 32952747116d769ecbdb51dc2d39fc7ceef91c55 Mon Sep 17 00:00:00 2001 From: vbarda Date: Tue, 13 Aug 2024 10:23:53 -0400 Subject: [PATCH 016/287] fix empty snapshot --- libs/langgraph/langgraph/pregel/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 0b7dd76c9..58e34dd59 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -499,7 +499,8 @@ class Pregel( if include_subgraph_state: checkpoint_tuples = self.checkpointer.list(config) else: - checkpoint_tuples = iter([self.checkpointer.get_tuple(config)]) + checkpoint_tuple = self.checkpointer.get_tuple(config) + checkpoint_tuples = iter([checkpoint_tuple] if checkpoint_tuple else []) checkpoint_ns = config["configurable"].get("checkpoint_ns", "") checkpoint_id = config["configurable"].get("checkpoint_id") @@ -544,7 +545,7 @@ class Pregel( if not checkpoint_ns_to_state_snapshots: return StateSnapshot( - values={}, next=(), config=config, checkpoint=empty_checkpoint() + values={}, next=(), config=config, metadata=None, created_at=None ) state_snapshot = self._assemble_state_snapshot_hierarchy( @@ -564,7 +565,9 @@ class Pregel( else: async def alist_checkpoints(): - yield await self.checkpointer.aget_tuple(config) + checkpoint_tuple = await self.checkpointer.aget_tuple(config) + if checkpoint_tuple: + yield checkpoint_tuple checkpoint_tuples = alist_checkpoints() @@ -611,7 +614,7 @@ class Pregel( if not checkpoint_ns_to_state_snapshots: return StateSnapshot( - values={}, next=(), config=config, checkpoint=empty_checkpoint() + values={}, next=(), config=config, metadata=None, created_at=None ) state_snapshot = self._assemble_state_snapshot_hierarchy( From 58887a5a3b93cc7c1b8da8122d7f54bb227b97f3 Mon Sep 17 00:00:00 2001 From: vbarda Date: Tue, 13 Aug 2024 16:23:30 -0400 Subject: [PATCH 017/287] checkpoints/interrupts for subgraphs triggered by sends --- .../langgraph/checkpoint/serde/jsonplus.py | 2 +- .../langgraph/checkpoint/serde/types.py | 1 + libs/langgraph/langgraph/constants.py | 12 +- libs/langgraph/langgraph/graph/graph.py | 14 +- libs/langgraph/langgraph/graph/state.py | 18 +- libs/langgraph/langgraph/pregel/__init__.py | 16 +- libs/langgraph/langgraph/pregel/algo.py | 8 +- libs/langgraph/tests/test_pregel.py | 360 +++++++++++++++++ libs/langgraph/tests/test_pregel_async.py | 362 ++++++++++++++++++ 9 files changed, 774 insertions(+), 19 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py index 2b7bce5d4..08feeabfa 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py @@ -66,7 +66,7 @@ class JsonPlusSerializer(SerializerProtocol): return self._encode_constructor_args(obj.__class__, args=[obj.value]) elif isinstance(obj, SendProtocol): return self._encode_constructor_args( - obj.__class__, kwargs={"node": obj.node, "arg": obj.arg} + obj.__class__, kwargs={"node": obj.node, "arg": obj.arg, "id": obj.id} ) elif isinstance(obj, (bytes, bytearray)): return self._encode_constructor_args( diff --git a/libs/checkpoint/langgraph/checkpoint/serde/types.py b/libs/checkpoint/langgraph/checkpoint/serde/types.py index de61ef78b..cc5c1fa8b 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/types.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/types.py @@ -55,6 +55,7 @@ class SendProtocol(Protocol): # Mirrors langgraph.constants.Send node: str arg: Any + id: str def __hash__(self) -> int: ... diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index ae447d62e..89a2effa7 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -1,4 +1,5 @@ -from typing import Any +from typing import Any, Optional +from uuid import uuid4 INPUT = "__input__" CONFIG_KEY_SEND = "__pregel_send" @@ -22,6 +23,7 @@ START = "__start__" END = "__end__" CHECKPOINT_NAMESPACE_SEPARATOR = "|" +SEND_CHECKPOINT_NAMESPACE_SEPARATOR = ":" class Send: @@ -40,6 +42,7 @@ class Send: Attributes: node (str): The name of the target node to send the message to. arg (Any): The state or message to send to the target node. + id (str): ID associated with the Send. Examples: >>> from typing import Annotated @@ -67,23 +70,26 @@ class Send: node: str arg: Any + id: Optional[str] - def __init__(self, /, node: str, arg: Any) -> None: + def __init__(self, /, node: str, arg: Any, id: Optional[str] = None) -> None: """ Initialize a new instance of the Send class. Args: node (str): The name of the target node to send the message to. arg (Any): The state or message to send to the target node. + id (str): ID associated with the Send. """ self.node = node self.arg = arg + self.id = id or str(uuid4()) def __hash__(self) -> int: return hash((self.node, self.arg)) def __repr__(self) -> str: - return f"Send(node={self.node!r}, arg={self.arg!r})" + return f"Send(node={self.node!r}, arg={self.arg!r}, id={self.id!r})" def __eq__(self, value: object) -> bool: return ( diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index c17373820..6fc4eb8cd 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -27,6 +27,7 @@ from langgraph.channels.ephemeral_value import EphemeralValue from langgraph.constants import ( CHECKPOINT_NAMESPACE_SEPARATOR, END, + SEND_CHECKPOINT_NAMESPACE_SEPARATOR, START, TAG_HIDDEN, Send, @@ -159,10 +160,15 @@ class Graph: *, metadata: Optional[dict[str, Any]] = None, ) -> None: - if isinstance(node, str) and CHECKPOINT_NAMESPACE_SEPARATOR in node: - raise ValueError( - f"'{CHECKPOINT_NAMESPACE_SEPARATOR}' is a reserved character and is not allowed in the node names." - ) + if isinstance(node, str): + for character in ( + CHECKPOINT_NAMESPACE_SEPARATOR, + SEND_CHECKPOINT_NAMESPACE_SEPARATOR, + ): + if character in node: + raise ValueError( + f"'{character}' is a reserved character and is not allowed in the node names." + ) if self.compiled: logger.warning( diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index e9971a5f1..8e7298616 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -29,7 +29,11 @@ from langgraph.channels.dynamic_barrier_value import DynamicBarrierValue, WaitFo from langgraph.channels.ephemeral_value import EphemeralValue from langgraph.channels.last_value import LastValue from langgraph.channels.named_barrier_value import NamedBarrierValue -from langgraph.constants import CHECKPOINT_NAMESPACE_SEPARATOR, TAG_HIDDEN +from langgraph.constants import ( + CHECKPOINT_NAMESPACE_SEPARATOR, + SEND_CHECKPOINT_NAMESPACE_SEPARATOR, + TAG_HIDDEN, +) from langgraph.errors import InvalidUpdateError from langgraph.graph.graph import ( END, @@ -312,10 +316,14 @@ class StateGraph(Graph): if node == END or node == START: raise ValueError(f"Node `{node}` is reserved.") - if CHECKPOINT_NAMESPACE_SEPARATOR in node: - raise ValueError( - f"'{CHECKPOINT_NAMESPACE_SEPARATOR}' is a reserved character and is not allowed in the node names." - ) + for character in ( + CHECKPOINT_NAMESPACE_SEPARATOR, + SEND_CHECKPOINT_NAMESPACE_SEPARATOR, + ): + if character in node: + raise ValueError( + f"'{character}' is a reserved character and is not allowed in the node names." + ) try: if isfunction(action) and ( diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 58e34dd59..96f720be5 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -72,6 +72,7 @@ from langgraph.constants import ( CONFIG_KEY_RESUMING, CONFIG_KEY_SEND, INTERRUPT, + SEND_CHECKPOINT_NAMESPACE_SEPARATOR, ) from langgraph.errors import GraphRecursionError, InvalidUpdateError from langgraph.managed.base import ( @@ -370,6 +371,15 @@ class Pregel( nodes = self.nodes channels = self.channels for subgraph_node_name in path: + # if we have this separator it means we have a node that was triggered by Send + if SEND_CHECKPOINT_NAMESPACE_SEPARATOR in subgraph_node_name: + name_parts = subgraph_node_name.split( + SEND_CHECKPOINT_NAMESPACE_SEPARATOR + ) + if len(name_parts) != 2: + raise ValueError(f"Malformed node name '{subgraph_node_name}'") + + subgraph_node_name = name_parts[0] if subgraph_node_name not in nodes: raise ValueError(f"Couldn't find node '{subgraph_node_name}'.") @@ -1190,8 +1200,7 @@ class Pregel( ) if not done: break # timed out - for fut in done: - task = futures.pop(fut) + for fut, task in zip(done, [futures.pop(fut) for fut in done]): if fut.exception() is not None: # we got an exception, break out of while loop # exception will be handled in panic_or_proceed @@ -1435,8 +1444,7 @@ class Pregel( ) if not done: break # timed out - for fut in done: - task = futures.pop(fut) + for fut, task in zip(done, [futures.pop(fut) for fut in done]): if fut.exception() is not None: # we got an exception, break out of while loop # exception will be handled in panic_or_proceed diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 2ddbcd873..ec8c18650 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -280,9 +280,9 @@ def prepare_next_tasks( "langgraph_task_idx": len(tasks), } checkpoint_ns = ( - f"{parent_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{packet.node}" + f"{parent_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{packet.node}:{packet.id}" if parent_ns - else packet.node + else f"{packet.node}:{packet.id}" ) task_id = str( uuid5(UUID(checkpoint["id"]), json.dumps((checkpoint_ns, metadata))) @@ -318,6 +318,10 @@ def prepare_next_tasks( PregelTaskWrites(packet.node, writes, triggers), config, ), + CONFIG_KEY_CHECKPOINTER: checkpointer, + CONFIG_KEY_RESUMING: is_resuming, + "checkpoint_id": checkpoint["id"], + "checkpoint_ns": checkpoint_ns, # in Send we can't checkpoint nested graphs # as they could be running in parallel }, diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 8780bc042..881487c93 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -9807,6 +9807,366 @@ def test_doubly_nested_graph_state( ) +@pytest.mark.repeat(10) +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_send_to_nested_graphs( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + + class OverallState(TypedDict): + subjects: list[str] + jokes: Annotated[list[str], operator.add] + + def continue_to_jokes(state: OverallState): + return [Send("generate_joke", {"subject": s}) for s in state["subjects"]] + + class JokeState(TypedDict): + subject: str + + def edit(state: JokeState): + subject = state["subject"] + return {"subject": f"{subject} - hohoho"} + + # subgraph + subgraph = StateGraph(input=JokeState, output=OverallState) + subgraph.add_node("edit", edit) + subgraph.add_node( + "generate", lambda state: {"jokes": [f"Joke about {state['subject']}"]} + ) + subgraph.set_entry_point("edit") + subgraph.add_edge("edit", "generate") + subgraph.set_finish_point("generate") + + # parent graph + builder = StateGraph(OverallState) + builder.add_node( + "generate_joke", + subgraph.compile( + checkpointer=INHERIT_CHECKPOINTER, interrupt_before=["generate"] + ), + ) + builder.add_conditional_edges(START, continue_to_jokes) + builder.add_edge("generate_joke", END) + + graph = builder.compile(checkpointer=checkpointer) + config = {"configurable": {"thread_id": "1"}} + + # invoke and pause at nested interrupt + assert graph.invoke({"subjects": ["cats", "dogs"]}, config=config) == { + "subjects": ["cats", "dogs"], + "jokes": [], + } + actual_snapshot = graph.get_state(config, include_subgraph_state=True) + subgraph_nodes = list(actual_snapshot.subgraph_state_snapshots.keys()) + assert len(subgraph_nodes) == 2 + for subgraph_node in subgraph_nodes: + assert subgraph_node.split(":")[0] == "generate_joke" + + expected_snapshot = StateSnapshot( + values={"subjects": ["cats", "dogs"], "jokes": []}, + next=("generate_joke", "generate_joke"), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + subgraph_nodes[0]: StateSnapshot( + values={"jokes": []}, + next=("generate",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": {"edit": None}, "step": 1}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + subgraph_nodes[1]: StateSnapshot( + values={"jokes": []}, + next=("generate",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": {"edit": None}, "step": 1}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + }, + ) + assert actual_snapshot == expected_snapshot + + # continue past interrupt + assert graph.invoke(None, config=config) == { + "subjects": ["cats", "dogs"], + "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + } + + actual_snapshot = graph.get_state(config, include_subgraph_state=True) + subgraph_nodes, _ = zip( + *( + sorted( + actual_snapshot.subgraph_state_snapshots.items(), + key=lambda x: x[1].values["jokes"][0], + ) + ) + ) + assert len(subgraph_nodes) == 2 + for subgraph_node in subgraph_nodes: + assert subgraph_node.split(":")[0] == "generate_joke" + expected_snapshot = StateSnapshot( + values={ + "subjects": ["cats", "dogs"], + "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + }, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "generate_joke": [ + {"jokes": ["Joke about cats - hohoho"]}, + {"jokes": ["Joke about dogs - hohoho"]}, + ] + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + subgraph_nodes[0]: StateSnapshot( + values={"jokes": ["Joke about cats - hohoho"]}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"generate": {"jokes": ["Joke about cats - hohoho"]}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + subgraph_nodes[1]: StateSnapshot( + values={"jokes": ["Joke about dogs - hohoho"]}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"generate": {"jokes": ["Joke about dogs - hohoho"]}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + }, + ) + assert actual_snapshot == expected_snapshot + + # test full history + actual_history = list(graph.get_state_history(config, include_subgraph_state=True)) + expected_history = [ + StateSnapshot( + values={ + "subjects": ["cats", "dogs"], + "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + }, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "generate_joke": [ + {"jokes": ["Joke about cats - hohoho"]}, + {"jokes": ["Joke about dogs - hohoho"]}, + ] + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={"subjects": ["cats", "dogs"], "jokes": []}, + next=("generate_joke", "generate_joke"), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + subgraph_nodes[0]: StateSnapshot( + values={"jokes": ["Joke about cats - hohoho"]}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"generate": {"jokes": ["Joke about cats - hohoho"]}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + subgraph_nodes[1]: StateSnapshot( + values={"jokes": ["Joke about dogs - hohoho"]}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"generate": {"jokes": ["Joke about dogs - hohoho"]}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + }, + ), + StateSnapshot( + values={"jokes": []}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"subjects": ["cats", "dogs"]}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + subgraph_state_snapshots=None, + ), + ] + assert actual_history == expected_history + + def test_repeat_condition(snapshot: SnapshotAssertion) -> None: class AgentState(TypedDict): hello: str diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index af624f639..43eab4ea6 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -8318,6 +8318,368 @@ async def test_doubly_nested_graph_state( ) +@pytest.mark.repeat(10) +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], +) +async def test_send_to_nested_graphs( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + + class OverallState(TypedDict): + subjects: list[str] + jokes: Annotated[list[str], operator.add] + + async def continue_to_jokes(state: OverallState): + return [Send("generate_joke", {"subject": s}) for s in state["subjects"]] + + class JokeState(TypedDict): + subject: str + + async def edit(state: JokeState): + subject = state["subject"] + return {"subject": f"{subject} - hohoho"} + + # subgraph + subgraph = StateGraph(input=JokeState, output=OverallState) + subgraph.add_node("edit", edit) + subgraph.add_node( + "generate", lambda state: {"jokes": [f"Joke about {state['subject']}"]} + ) + subgraph.set_entry_point("edit") + subgraph.add_edge("edit", "generate") + subgraph.set_finish_point("generate") + + # parent graph + builder = StateGraph(OverallState) + builder.add_node( + "generate_joke", + subgraph.compile( + checkpointer=INHERIT_CHECKPOINTER, interrupt_before=["generate"] + ), + ) + builder.add_conditional_edges(START, continue_to_jokes) + builder.add_edge("generate_joke", END) + + graph = builder.compile(checkpointer=checkpointer) + config = {"configurable": {"thread_id": "1"}} + + # invoke and pause at nested interrupt + assert await graph.ainvoke({"subjects": ["cats", "dogs"]}, config=config) == { + "subjects": ["cats", "dogs"], + "jokes": [], + } + actual_snapshot = await graph.aget_state(config, include_subgraph_state=True) + subgraph_nodes = list(actual_snapshot.subgraph_state_snapshots.keys()) + assert len(subgraph_nodes) == 2 + for subgraph_node in subgraph_nodes: + assert subgraph_node.split(":")[0] == "generate_joke" + + expected_snapshot = StateSnapshot( + values={"subjects": ["cats", "dogs"], "jokes": []}, + next=("generate_joke", "generate_joke"), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + subgraph_nodes[0]: StateSnapshot( + values={"jokes": []}, + next=("generate",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": {"edit": None}, "step": 1}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + subgraph_nodes[1]: StateSnapshot( + values={"jokes": []}, + next=("generate",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": {"edit": None}, "step": 1}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + }, + ) + assert actual_snapshot == expected_snapshot + + # continue past interrupt + assert await graph.ainvoke(None, config=config) == { + "subjects": ["cats", "dogs"], + "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + } + + actual_snapshot = await graph.aget_state(config, include_subgraph_state=True) + subgraph_nodes, _ = zip( + *( + sorted( + actual_snapshot.subgraph_state_snapshots.items(), + key=lambda x: x[1].values["jokes"][0], + ) + ) + ) + assert len(subgraph_nodes) == 2 + for subgraph_node in subgraph_nodes: + assert subgraph_node.split(":")[0] == "generate_joke" + expected_snapshot = StateSnapshot( + values={ + "subjects": ["cats", "dogs"], + "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + }, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "generate_joke": [ + {"jokes": ["Joke about cats - hohoho"]}, + {"jokes": ["Joke about dogs - hohoho"]}, + ] + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + subgraph_nodes[0]: StateSnapshot( + values={"jokes": ["Joke about cats - hohoho"]}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"generate": {"jokes": ["Joke about cats - hohoho"]}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + subgraph_nodes[1]: StateSnapshot( + values={"jokes": ["Joke about dogs - hohoho"]}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"generate": {"jokes": ["Joke about dogs - hohoho"]}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + }, + ) + assert actual_snapshot == expected_snapshot + + # test full history + actual_history = [ + c async for c in graph.aget_state_history(config, include_subgraph_state=True) + ] + expected_history = [ + StateSnapshot( + values={ + "subjects": ["cats", "dogs"], + "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + }, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "generate_joke": [ + {"jokes": ["Joke about cats - hohoho"]}, + {"jokes": ["Joke about dogs - hohoho"]}, + ] + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + StateSnapshot( + values={"subjects": ["cats", "dogs"], "jokes": []}, + next=("generate_joke", "generate_joke"), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + subgraph_nodes[0]: StateSnapshot( + values={"jokes": ["Joke about cats - hohoho"]}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"generate": {"jokes": ["Joke about cats - hohoho"]}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[0], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + subgraph_nodes[1]: StateSnapshot( + values={"jokes": ["Joke about dogs - hohoho"]}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"generate": {"jokes": ["Joke about dogs - hohoho"]}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": subgraph_nodes[1], + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ), + }, + ), + StateSnapshot( + values={"jokes": []}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"subjects": ["cats", "dogs"]}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + subgraph_state_snapshots=None, + ), + ] + assert actual_history == expected_history + + async def test_checkpoint_metadata() -> None: """This test verifies that a run's configurable fields are merged with the previous checkpoint config for each step in the run. From d9618880a3525a79635f8b2f5e98f76af26ed28c Mon Sep 17 00:00:00 2001 From: vbarda Date: Tue, 13 Aug 2024 19:46:13 -0400 Subject: [PATCH 018/287] update logic for latest snapshot's subgraph snapshots --- libs/langgraph/langgraph/pregel/__init__.py | 18 +- libs/langgraph/tests/test_pregel.py | 376 +++++++++----------- libs/langgraph/tests/test_pregel_async.py | 375 +++++++++---------- 3 files changed, 351 insertions(+), 418 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 96f720be5..81f08d98f 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -506,14 +506,15 @@ class Pregel( if not self.checkpointer: raise ValueError("No checkpointer set") + checkpoint_tuple = self.checkpointer.get_tuple(config) if include_subgraph_state: checkpoint_tuples = self.checkpointer.list(config) else: - checkpoint_tuple = self.checkpointer.get_tuple(config) checkpoint_tuples = iter([checkpoint_tuple] if checkpoint_tuple else []) - checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - checkpoint_id = config["configurable"].get("checkpoint_id") + checkpoint_config = checkpoint_tuple.config if checkpoint_tuple else config + checkpoint_ns = checkpoint_config["configurable"].get("checkpoint_ns", "") + checkpoint_id = checkpoint_config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} checkpoint_ns_to_nodes_and_channels: dict[ @@ -526,7 +527,7 @@ class Pregel( saved_checkpoint_id = checkpoint_tuple.config["configurable"][ "checkpoint_id" ] - if checkpoint_id and checkpoint_id != saved_checkpoint_id: + if checkpoint_id != saved_checkpoint_id: continue existing_checkpoint_id = checkpoint_ns_to_checkpoint_id.get( @@ -570,19 +571,20 @@ class Pregel( if not self.checkpointer: raise ValueError("No checkpointer set") + checkpoint_tuple = await self.checkpointer.aget_tuple(config) if include_subgraph_state: checkpoint_tuples = self.checkpointer.alist(config) else: async def alist_checkpoints(): - checkpoint_tuple = await self.checkpointer.aget_tuple(config) if checkpoint_tuple: yield checkpoint_tuple checkpoint_tuples = alist_checkpoints() - checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - checkpoint_id = config["configurable"].get("checkpoint_id") + checkpoint_config = checkpoint_tuple.config if checkpoint_tuple else config + checkpoint_ns = checkpoint_config["configurable"].get("checkpoint_ns", "") + checkpoint_id = checkpoint_config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} checkpoint_ns_to_nodes_and_channels: dict[ @@ -595,7 +597,7 @@ class Pregel( saved_checkpoint_id = checkpoint_tuple.config["configurable"][ "checkpoint_id" ] - if checkpoint_id and checkpoint_id != saved_checkpoint_id: + if checkpoint_id != saved_checkpoint_id: continue existing_checkpoint_id = checkpoint_ns_to_checkpoint_id.get( diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 881487c93..b64d97b57 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -9373,39 +9373,77 @@ def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={"my_key": "hi my value here and there"}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ) + # test loading inner snapshot + child_snapshot = app.get_state( + {"configurable": {"thread_id": "1", "checkpoint_ns": "inner"}} + ) + assert child_snapshot == StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + # test looking up parent state by checkpoint ID + assert app.get_state( + { + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], + } + }, + include_subgraph_state=True, + ) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={"inner": child_snapshot}, + ) + # test full history at the end assert list(app.get_state_history(config, include_subgraph_state=True)) == [ StateSnapshot( values={"my_key": "hi my value here and there and back again"}, @@ -9482,10 +9520,6 @@ def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - # TODO: this is likely very confusing for an end user, and we'll probably need to update this. - # right now this is happening due to us overwriting the - # subgraph snapshot after we finish the graph with while the checkpoint_id - # is the same as when we interrupted subgraph_state_snapshots={ "inner": StateSnapshot( values={"my_key": "hi my value here and there"}, @@ -9749,61 +9783,99 @@ def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "child": StateSnapshot( - values={"my_key": "hi my value here and there"}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"child_1": {"my_key": "hi my value here and there"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots={ - "child_1": StateSnapshot( - values={"my_key": "hi my value here and there"}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "grandchild_2": {"my_key": "hi my value here and there"} - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, - ) + ) + # test getting grandchild snapshot + grandchild_snapshot = app.get_state( + {"configurable": {"thread_id": "1", "checkpoint_ns": "child|child_1"}} + ) + assert grandchild_snapshot == StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } }, + metadata={ + "source": "loop", + "writes": {"grandchild_2": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + # test getting child snapshot + child_snapshot = app.get_state( + {"configurable": {"thread_id": "1", "checkpoint_ns": "child"}}, + include_subgraph_state=True, + ) + assert child_snapshot == StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={"child_1": grandchild_snapshot}, + ) + # test getting parent snapshot for a checkpoint ID + assert app.get_state( + { + "configurable": { + "thread_id": "1", + "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], + } + }, + include_subgraph_state=True, + ) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={"child": child_snapshot}, ) @@ -9939,17 +10011,6 @@ def test_send_to_nested_graphs( } actual_snapshot = graph.get_state(config, include_subgraph_state=True) - subgraph_nodes, _ = zip( - *( - sorted( - actual_snapshot.subgraph_state_snapshots.items(), - key=lambda x: x[1].values["jokes"][0], - ) - ) - ) - assert len(subgraph_nodes) == 2 - for subgraph_node in subgraph_nodes: - assert subgraph_node.split(":")[0] == "generate_joke" expected_snapshot = StateSnapshot( values={ "subjects": ["cats", "dogs"], @@ -9981,63 +10042,19 @@ def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - subgraph_nodes[0]: StateSnapshot( - values={"jokes": ["Joke about cats - hohoho"]}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"generate": {"jokes": ["Joke about cats - hohoho"]}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - subgraph_nodes[1]: StateSnapshot( - values={"jokes": ["Joke about dogs - hohoho"]}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"generate": {"jokes": ["Joke about dogs - hohoho"]}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - }, ) assert actual_snapshot == expected_snapshot # test full history actual_history = list(graph.get_state_history(config, include_subgraph_state=True)) + + # get subgraph node state for expected history + subgraph_state_snapshots = { + subgraph_node: graph.get_state( + {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} + ) + for subgraph_node in subgraph_nodes + } expected_history = [ StateSnapshot( values={ @@ -10091,58 +10108,7 @@ def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - subgraph_nodes[0]: StateSnapshot( - values={"jokes": ["Joke about cats - hohoho"]}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"generate": {"jokes": ["Joke about cats - hohoho"]}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - subgraph_nodes[1]: StateSnapshot( - values={"jokes": ["Joke about dogs - hohoho"]}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"generate": {"jokes": ["Joke about dogs - hohoho"]}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - }, + subgraph_state_snapshots=subgraph_state_snapshots, ), StateSnapshot( values={"jokes": []}, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 43eab4ea6..7b2e3222b 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -7882,39 +7882,77 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={"my_key": "hi my value here and there"}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ) + # test loading inner snapshot + child_snapshot = await app.aget_state( + {"configurable": {"thread_id": "1", "checkpoint_ns": "inner"}} + ) + assert child_snapshot == StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + # test looking up parent state by checkpoint ID + assert await app.aget_state( + { + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], + } + }, + include_subgraph_state=True, + ) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={"inner": child_snapshot}, + ) + # test full history at the end assert [ s async for s in app.aget_state_history(config, include_subgraph_state=True) ] == [ @@ -7993,10 +8031,6 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - # TODO: this is likely very confusing for an end user, and we'll probably need to update this. - # right now this is happening due to us overwriting the - # subgraph snapshot after we finish the graph with while the checkpoint_id - # is the same as when we interrupted subgraph_state_snapshots={ "inner": StateSnapshot( values={"my_key": "hi my value here and there"}, @@ -8260,61 +8294,99 @@ async def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "child": StateSnapshot( - values={"my_key": "hi my value here and there"}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"child_1": {"my_key": "hi my value here and there"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots={ - "child_1": StateSnapshot( - values={"my_key": "hi my value here and there"}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "grandchild_2": {"my_key": "hi my value here and there"} - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, - ) + ) + # test getting grandchild snapshot + grandchild_snapshot = await app.aget_state( + {"configurable": {"thread_id": "1", "checkpoint_ns": "child|child_1"}} + ) + assert grandchild_snapshot == StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } }, + metadata={ + "source": "loop", + "writes": {"grandchild_2": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + # test getting child snapshot + child_snapshot = await app.aget_state( + {"configurable": {"thread_id": "1", "checkpoint_ns": "child"}}, + include_subgraph_state=True, + ) + assert child_snapshot == StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={"child_1": grandchild_snapshot}, + ) + # test getting parent snapshot for a checkpoint ID + assert await app.aget_state( + { + "configurable": { + "thread_id": "1", + "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], + } + }, + include_subgraph_state=True, + ) == StateSnapshot( + values={"my_key": "hi my value"}, + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={"child": child_snapshot}, ) @@ -8450,17 +8522,6 @@ async def test_send_to_nested_graphs( } actual_snapshot = await graph.aget_state(config, include_subgraph_state=True) - subgraph_nodes, _ = zip( - *( - sorted( - actual_snapshot.subgraph_state_snapshots.items(), - key=lambda x: x[1].values["jokes"][0], - ) - ) - ) - assert len(subgraph_nodes) == 2 - for subgraph_node in subgraph_nodes: - assert subgraph_node.split(":")[0] == "generate_joke" expected_snapshot = StateSnapshot( values={ "subjects": ["cats", "dogs"], @@ -8492,58 +8553,6 @@ async def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - subgraph_nodes[0]: StateSnapshot( - values={"jokes": ["Joke about cats - hohoho"]}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"generate": {"jokes": ["Joke about cats - hohoho"]}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - subgraph_nodes[1]: StateSnapshot( - values={"jokes": ["Joke about dogs - hohoho"]}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"generate": {"jokes": ["Joke about dogs - hohoho"]}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - }, ) assert actual_snapshot == expected_snapshot @@ -8551,6 +8560,13 @@ async def test_send_to_nested_graphs( actual_history = [ c async for c in graph.aget_state_history(config, include_subgraph_state=True) ] + # get subgraph node state for expected history + subgraph_state_snapshots = { + subgraph_node: await graph.aget_state( + {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} + ) + for subgraph_node in subgraph_nodes + } expected_history = [ StateSnapshot( values={ @@ -8604,58 +8620,7 @@ async def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - subgraph_nodes[0]: StateSnapshot( - values={"jokes": ["Joke about cats - hohoho"]}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"generate": {"jokes": ["Joke about cats - hohoho"]}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - subgraph_nodes[1]: StateSnapshot( - values={"jokes": ["Joke about dogs - hohoho"]}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"generate": {"jokes": ["Joke about dogs - hohoho"]}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - }, + subgraph_state_snapshots=subgraph_state_snapshots, ), StateSnapshot( values={"jokes": []}, From 3b56cdf5248bceb1c44b2bc67702fa5e7fc83afc Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 13 Aug 2024 17:35:31 -0700 Subject: [PATCH 019/287] Add Runs.join_stream endpoint --- libs/sdk-py/langgraph_sdk/client.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index 13d5560db..9163522b5 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -1449,6 +1449,28 @@ class RunsClient: """ # noqa: E501 return await self.http.get(f"/threads/{thread_id}/runs/{run_id}/join") + def join_stream(self, thread_id: str, run_id: str) -> AsyncIterator[StreamPart]: + """Stream output from a run in real-time, until the run is done. + Output is not buffered, so any output produced before this call will + not be received here. + + Args: + thread_id: The thread ID to join. + run_id: The run ID to join. + + Returns: + None + + Example Usage: + + await client.runs.join( + thread_id="thread_id_to_join", + run_id="run_id_to_join" + ) + + """ # noqa: E501 + return self.http.stream(f"/threads/{thread_id}/runs/{run_id}/stream", "GET") + async def delete(self, thread_id: str, run_id: str) -> None: """Delete a run. From 409b915a3f38196fc95edfb1d12495f36a0da12e Mon Sep 17 00:00:00 2001 From: vbarda Date: Wed, 14 Aug 2024 11:54:23 -0400 Subject: [PATCH 020/287] code review --- libs/langgraph/langgraph/constants.py | 2 +- libs/langgraph/langgraph/pregel/__init__.py | 333 +++++++++++--------- 2 files changed, 181 insertions(+), 154 deletions(-) diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index 89a2effa7..bc74619ff 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -86,7 +86,7 @@ class Send: self.id = id or str(uuid4()) def __hash__(self) -> int: - return hash((self.node, self.arg)) + return hash((self.node, self.arg, self.id)) def __repr__(self) -> str: return f"Send(node={self.node!r}, arg={self.arg!r}, id={self.id!r})" diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 81f08d98f..ffa53da85 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -195,6 +195,145 @@ class Channel: ) +def _get_nodes_and_channels( + nodes: Mapping[str, PregelNode], + channels: Mapping[str, BaseChannel], + checkpoint_ns: str, +) -> tuple[Mapping[str, PregelNode], Mapping[str, BaseChannel]]: + if checkpoint_ns == "": + return nodes, channels + + path = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) + for subgraph_node_name in path: + # if we have this separator it means we have a node that was triggered by Send + if SEND_CHECKPOINT_NAMESPACE_SEPARATOR in subgraph_node_name: + name_parts = subgraph_node_name.split(SEND_CHECKPOINT_NAMESPACE_SEPARATOR) + if len(name_parts) != 2: + raise ValueError(f"Malformed node name '{subgraph_node_name}'") + + subgraph_node_name = name_parts[0] + if subgraph_node_name not in nodes: + raise ValueError(f"Couldn't find node '{subgraph_node_name}'.") + + subgraph_node = nodes[subgraph_node_name].get_node() + + if not isinstance(subgraph_node, RunnableSequence): + break + + first_step = subgraph_node.steps[0] + if isinstance(first_step, Pregel): + nodes = first_step.nodes + channels = first_step.channels + + return nodes, channels + + +def _assemble_state_snapshot_hierarchy( + root_checkpoint_ns: str, + checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot], +) -> StateSnapshot: + checkpoint_ns_list_to_visit = sorted( + checkpoint_ns_to_state_snapshots.keys(), + key=lambda x: len(x.split(CHECKPOINT_NAMESPACE_SEPARATOR)), + ) + while checkpoint_ns_list_to_visit: + checkpoint_ns = checkpoint_ns_list_to_visit.pop() + state_snapshot = checkpoint_ns_to_state_snapshots[checkpoint_ns] + *path, subgraph_node = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) + parent_checkpoint_ns = CHECKPOINT_NAMESPACE_SEPARATOR.join(path) + if subgraph_node and ( + parent_state_snapshot := checkpoint_ns_to_state_snapshots.get( + parent_checkpoint_ns + ) + ): + parent_subgraph_snapshots = { + **(parent_state_snapshot.subgraph_state_snapshots or {}), + subgraph_node: state_snapshot, + } + checkpoint_ns_to_state_snapshots[ + parent_checkpoint_ns + ] = checkpoint_ns_to_state_snapshots[parent_checkpoint_ns]._replace( + subgraph_state_snapshots=parent_subgraph_snapshots + ) + + state_snapshot = checkpoint_ns_to_state_snapshots.pop(root_checkpoint_ns, None) + if state_snapshot is None: + raise ValueError(f"Missing checkpoint for checkpoint NS '{root_checkpoint_ns}'") + return state_snapshot + + +def _prepare_state_snapshot( + saved: CheckpointTuple, + nodes: Mapping[str, PregelNode], + channels: Mapping[str, BaseChannel], + managed_values_dict: dict[str, ManagedValueSpec], + select_channels: str | list[str], +) -> StateSnapshot: + with ChannelsManager( + { + k: LastValue(None) if isinstance(c, Context) else c + for k, c in channels.items() + }, + saved.checkpoint, + saved.config, + ) as channels, ManagedValuesManager( + managed_values_dict, ensure_config(saved.config) + ) as managed: + next_tasks = prepare_next_tasks( + saved.checkpoint, + nodes, + channels, + managed, + saved.config, + -1, + for_execution=False, + ) + return StateSnapshot( + values=read_channels(channels, select_channels), + next=tuple(t.name for t in next_tasks), + config=saved.config, + metadata=saved.metadata, + created_at=saved.checkpoint["ts"], + parent_config=saved.parent_config, + ) + + +async def _prepare_state_snapshot_async( + saved: CheckpointTuple, + nodes: Mapping[str, PregelNode], + channels: Mapping[str, BaseChannel], + managed_values_dict: dict[str, ManagedValueSpec], + select_channels: str | list[str], +) -> StateSnapshot: + async with AsyncChannelsManager( + { + k: LastValue(None) if isinstance(c, Context) else c + for k, c in channels.items() + }, + saved.checkpoint, + saved.config, + ) as channels, AsyncManagedValuesManager( + managed_values_dict, ensure_config(saved.config) + ) as managed: + next_tasks = prepare_next_tasks( + saved.checkpoint, + nodes, + channels, + managed, + saved.config, + -1, + for_execution=False, + ) + return StateSnapshot( + values=read_channels(channels, select_channels), + next=tuple(t.name for t in next_tasks), + config=saved.config, + metadata=saved.metadata, + created_at=saved.checkpoint["ts"], + parent_config=saved.parent_config, + ) + + class Pregel( RunnableSerializable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]] ): @@ -361,144 +500,6 @@ class Pregel( if is_managed_value(v) } - def _get_nodes_and_channels( - self, checkpoint_ns: str - ) -> tuple[Mapping[str, PregelNode], Mapping[str, BaseChannel]]: - if checkpoint_ns == "": - return self.nodes, self.channels - - path = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) - nodes = self.nodes - channels = self.channels - for subgraph_node_name in path: - # if we have this separator it means we have a node that was triggered by Send - if SEND_CHECKPOINT_NAMESPACE_SEPARATOR in subgraph_node_name: - name_parts = subgraph_node_name.split( - SEND_CHECKPOINT_NAMESPACE_SEPARATOR - ) - if len(name_parts) != 2: - raise ValueError(f"Malformed node name '{subgraph_node_name}'") - - subgraph_node_name = name_parts[0] - if subgraph_node_name not in nodes: - raise ValueError(f"Couldn't find node '{subgraph_node_name}'.") - - subgraph_node = nodes[subgraph_node_name].get_node() - - if not isinstance(subgraph_node, RunnableSequence): - break - - first_step = subgraph_node.steps[0] - if isinstance(first_step, Pregel): - nodes = first_step.nodes - channels = first_step.channels - - return nodes, channels - - def _prepare_state_snapshot( - self, - saved: CheckpointTuple, - nodes: Mapping[str, PregelNode], - channels: Mapping[str, BaseChannel], - ) -> StateSnapshot: - with ChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in channels.items() - }, - saved.checkpoint, - saved.config, - ) as channels, ManagedValuesManager( - self.managed_values_dict, ensure_config(saved.config) - ) as managed: - next_tasks = prepare_next_tasks( - saved.checkpoint, - nodes, - channels, - managed, - saved.config, - -1, - for_execution=False, - ) - return StateSnapshot( - values=read_channels(channels, self.stream_channels_asis), - next=tuple(t.name for t in next_tasks), - config=saved.config, - metadata=saved.metadata, - created_at=saved.checkpoint["ts"], - parent_config=saved.parent_config, - ) - - async def _prepare_state_snapshot_async( - self, - saved: CheckpointTuple, - nodes: Mapping[str, PregelNode], - channels: Mapping[str, BaseChannel], - ) -> StateSnapshot: - async with AsyncChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in channels.items() - }, - saved.checkpoint, - saved.config, - ) as channels, AsyncManagedValuesManager( - self.managed_values_dict, ensure_config(saved.config) - ) as managed: - next_tasks = prepare_next_tasks( - saved.checkpoint, - nodes, - channels, - managed, - saved.config, - -1, - for_execution=False, - ) - return StateSnapshot( - values=read_channels(channels, self.stream_channels_asis), - next=tuple(t.name for t in next_tasks), - config=saved.config, - metadata=saved.metadata, - created_at=saved.checkpoint["ts"], - parent_config=saved.parent_config, - ) - - @staticmethod - def _assemble_state_snapshot_hierarchy( - root_checkpoint_ns: str, - checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot], - ) -> StateSnapshot: - checkpoint_ns_list_to_visit = sorted( - checkpoint_ns_to_state_snapshots.keys(), - key=lambda x: len(x.split(CHECKPOINT_NAMESPACE_SEPARATOR)), - ) - while checkpoint_ns_list_to_visit: - checkpoint_ns = checkpoint_ns_list_to_visit.pop() - state_snapshot = checkpoint_ns_to_state_snapshots[checkpoint_ns] - *path, subgraph_node = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) - parent_checkpoint_ns = CHECKPOINT_NAMESPACE_SEPARATOR.join(path) - if subgraph_node and ( - parent_state_snapshot := checkpoint_ns_to_state_snapshots.get( - parent_checkpoint_ns - ) - ): - parent_subgraph_snapshots = { - **(parent_state_snapshot.subgraph_state_snapshots or {}), - subgraph_node: state_snapshot, - } - checkpoint_ns_to_state_snapshots[ - parent_checkpoint_ns - ] = checkpoint_ns_to_state_snapshots[parent_checkpoint_ns]._replace( - subgraph_state_snapshots=parent_subgraph_snapshots - ) - - state_snapshot = checkpoint_ns_to_state_snapshots.pop(root_checkpoint_ns, None) - if state_snapshot is None: - raise ValueError( - f"Missing checkpoint for checkpoint NS '{root_checkpoint_ns}'" - ) - return state_snapshot - def get_state( self, config: RunnableConfig, *, include_subgraph_state: bool = False ) -> StateSnapshot: @@ -541,13 +542,19 @@ class Pregel( if saved_checkpoint_ns not in checkpoint_ns_to_nodes_and_channels: checkpoint_ns_to_nodes_and_channels[ saved_checkpoint_ns - ] = self._get_nodes_and_channels(saved_checkpoint_ns) + ] = _get_nodes_and_channels( + self.nodes, self.channels, saved_checkpoint_ns + ) nodes, channels = checkpoint_ns_to_nodes_and_channels[ saved_checkpoint_ns ] - state_snapshot = self._prepare_state_snapshot( - checkpoint_tuple, nodes, channels + state_snapshot = _prepare_state_snapshot( + checkpoint_tuple, + nodes, + channels, + self.managed_values_dict, + self.stream_channels_asis, ) checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot checkpoint_ns_to_checkpoint_id[ @@ -559,7 +566,7 @@ class Pregel( values={}, next=(), config=config, metadata=None, created_at=None ) - state_snapshot = self._assemble_state_snapshot_hierarchy( + state_snapshot = _assemble_state_snapshot_hierarchy( checkpoint_ns, checkpoint_ns_to_state_snapshots ) return state_snapshot @@ -611,13 +618,19 @@ class Pregel( if saved_checkpoint_ns not in checkpoint_ns_to_nodes_and_channels: checkpoint_ns_to_nodes_and_channels[ saved_checkpoint_ns - ] = self._get_nodes_and_channels(saved_checkpoint_ns) + ] = _get_nodes_and_channels( + self.nodes, self.channels, saved_checkpoint_ns + ) nodes, channels = checkpoint_ns_to_nodes_and_channels[ saved_checkpoint_ns ] - state_snapshot = await self._prepare_state_snapshot_async( - checkpoint_tuple, nodes, channels + state_snapshot = await _prepare_state_snapshot_async( + checkpoint_tuple, + nodes, + channels, + self.managed_values_dict, + self.stream_channels_asis, ) checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot checkpoint_ns_to_checkpoint_id[ @@ -629,7 +642,7 @@ class Pregel( values={}, next=(), config=config, metadata=None, created_at=None ) - state_snapshot = self._assemble_state_snapshot_hierarchy( + state_snapshot = _assemble_state_snapshot_hierarchy( checkpoint_ns, checkpoint_ns_to_state_snapshots ) return state_snapshot @@ -669,10 +682,18 @@ class Pregel( ) yield state_snapshot else: - nodes, channels = self._get_nodes_and_channels( - checkpoint_tuple.config["configurable"]["checkpoint_ns"] + nodes, channels = _get_nodes_and_channels( + self.nodes, + self.channels, + checkpoint_tuple.config["configurable"]["checkpoint_ns"], + ) + yield _prepare_state_snapshot( + checkpoint_tuple, + nodes, + channels, + self.managed_values_dict, + self.stream_channels_asis, ) - yield self._prepare_state_snapshot(checkpoint_tuple, nodes, channels) async def aget_state_history( self, @@ -709,11 +730,17 @@ class Pregel( ) yield state_snapshot else: - nodes, channels = self._get_nodes_and_channels( - checkpoint_tuple.config["configurable"]["checkpoint_ns"] + nodes, channels = _get_nodes_and_channels( + self.nodes, + self.channels, + checkpoint_tuple.config["configurable"]["checkpoint_ns"], ) - yield await self._prepare_state_snapshot_async( - checkpoint_tuple, nodes, channels + yield await _prepare_state_snapshot_async( + checkpoint_tuple, + nodes, + channels, + self.managed_values_dict, + self.stream_channels_asis, ) def update_state( From 6531ec7669767f505e584021d33e1720af17c509 Mon Sep 17 00:00:00 2001 From: vbarda Date: Wed, 14 Aug 2024 14:02:29 -0400 Subject: [PATCH 021/287] remove inherit checkpointer --- libs/langgraph/langgraph/graph/graph.py | 5 +- libs/langgraph/langgraph/graph/state.py | 4 +- libs/langgraph/langgraph/pregel/__init__.py | 67 +++++++++------------ libs/langgraph/tests/test_pregel.py | 23 +++---- libs/langgraph/tests/test_pregel_async.py | 23 +++---- 5 files changed, 50 insertions(+), 72 deletions(-) diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index 6fc4eb8cd..96ccda3b2 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -24,6 +24,7 @@ from langchain_core.runnables.graph import Graph as DrawableGraph from langchain_core.runnables.graph import Node as DrawableNode from langgraph.channels.ephemeral_value import EphemeralValue +from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.constants import ( CHECKPOINT_NAMESPACE_SEPARATOR, END, @@ -33,7 +34,7 @@ from langgraph.constants import ( Send, ) from langgraph.errors import InvalidUpdateError -from langgraph.pregel import Channel, CheckpointerType, Pregel +from langgraph.pregel import Channel, Pregel from langgraph.pregel.read import PregelNode from langgraph.pregel.types import All from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry @@ -374,7 +375,7 @@ class Graph: def compile( self, - checkpointer: Optional[CheckpointerType] = None, + checkpointer: Optional[BaseCheckpointSaver] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: bool = False, diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 8e7298616..f998b568a 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -29,6 +29,7 @@ from langgraph.channels.dynamic_barrier_value import DynamicBarrierValue, WaitFo from langgraph.channels.ephemeral_value import EphemeralValue from langgraph.channels.last_value import LastValue from langgraph.channels.named_barrier_value import NamedBarrierValue +from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.constants import ( CHECKPOINT_NAMESPACE_SEPARATOR, SEND_CHECKPOINT_NAMESPACE_SEPARATOR, @@ -44,7 +45,6 @@ from langgraph.graph.graph import ( Send, ) from langgraph.managed.base import ManagedValue, is_managed_value -from langgraph.pregel import CheckpointerType from langgraph.pregel.read import ChannelRead, PregelNode from langgraph.pregel.types import All, RetryPolicy from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry @@ -381,7 +381,7 @@ class StateGraph(Graph): def compile( self, - checkpointer: Optional[CheckpointerType] = None, + checkpointer: Optional[BaseCheckpointSaver] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: bool = False, diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index ffa53da85..9f583b5b5 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -13,7 +13,6 @@ from typing import ( Callable, Dict, Iterator, - Literal, Mapping, Optional, Sequence, @@ -117,10 +116,6 @@ WriteValue = Union[ ] -INHERIT_CHECKPOINTER = "inherit_checkpointer" -CheckpointerType = Union[BaseCheckpointSaver, Literal["inherit_checkpointer"]] - - class Channel: @overload @classmethod @@ -334,6 +329,16 @@ async def _prepare_state_snapshot_async( ) +def _has_nested_interrupts( + graph: Pregel, +) -> bool: + for child in graph.subgraphs: + if child.interrupt_after_nodes or child.interrupt_before_nodes: + return True + else: + return False + + class Pregel( RunnableSerializable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]] ): @@ -363,7 +368,7 @@ class Pregel( debug: bool = Field(default_factory=get_debug) """Whether to print debug information during execution. Defaults to False.""" - checkpointer: Optional[CheckpointerType] = None + checkpointer: Optional[BaseCheckpointSaver] = None """Checkpointer used to save and load graph state. Defaults to None.""" retry_policy: Optional[RetryPolicy] = None @@ -420,7 +425,6 @@ class Pregel( + ( self.checkpointer.config_specs if self.checkpointer is not None - and self.checkpointer != INHERIT_CHECKPOINTER else [] ) + ( @@ -500,6 +504,18 @@ class Pregel( if is_managed_value(v) } + @property + def subgraphs(self) -> Iterator[Pregel]: + for node in self.nodes.values(): + if isinstance(node.bound, Pregel): + yield node.bound + yield from node.bound.subgraphs + elif isinstance(node.bound, RunnableSequence): + for runnable in node.bound.steps: + if isinstance(runnable, Pregel): + yield runnable + yield from runnable.subgraphs + def get_state( self, config: RunnableConfig, *, include_subgraph_state: bool = False ) -> StateSnapshot: @@ -1009,44 +1025,19 @@ class Pregel( stream_mode = stream_mode if stream_mode is not None else self.stream_mode if not isinstance(stream_mode, list): stream_mode = [stream_mode] - - if config and config.get("configurable", {}).get(CONFIG_KEY_CHECKPOINTER): - parent_checkpointer = config["configurable"][CONFIG_KEY_CHECKPOINTER] - else: - parent_checkpointer = None - if config and config.get("configurable", {}).get(CONFIG_KEY_READ) is not None: # if being called as a node in another graph, always use values mode stream_mode = ["values"] - - if ( - (interrupt_before or interrupt_after) - and parent_checkpointer is not None - and self.checkpointer is None - ): - raise ValueError( - "Missing checkpointer for a subgraph with interrupts. " - "Please compile the subgraph graph with checkpointer=INHERIT_CHECKPOINTER (from langgraph.pregel import INHERIT_CHECKPOINTER)." - ) - - if ( - parent_checkpointer is not None - and self.checkpointer is not None - and self.checkpointer != INHERIT_CHECKPOINTER - ): - raise ValueError( - "Custom checkpointers for subgraphs are not allowed. " - "Please compile the subgraph graph with checkpointer=INHERIT_CHECKPOINTER (from langgraph.pregel import INHERIT_CHECKPOINTER)." - ) - if ( - parent_checkpointer is not None - and self.checkpointer == INHERIT_CHECKPOINTER + config is not None + and config.get("configurable", {}).get(CONFIG_KEY_CHECKPOINTER) + and (interrupt_before or interrupt_after or _has_nested_interrupts(self)) ): - checkpointer = parent_checkpointer + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"][ + CONFIG_KEY_CHECKPOINTER + ] else: checkpointer = self.checkpointer - return ( debug, stream_mode, diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index b64d97b57..aa1db664e 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -61,7 +61,6 @@ from langgraph.prebuilt.chat_agent_executor import ( ) from langgraph.prebuilt.tool_node import ToolNode from langgraph.pregel import ( - INHERIT_CHECKPOINTER, Channel, GraphRecursionError, Pregel, @@ -7745,7 +7744,7 @@ def test_nested_graph_interrupts( graph.add_node("outer_1", outer_1) graph.add_node( "inner", - inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + inner.compile(interrupt_before=["inner_2"]), ) graph.add_node("outer_2", outer_2) graph.set_entry_point("outer_1") @@ -8924,7 +8923,7 @@ def test_nested_graph_interrupts_parallel( graph = StateGraph(State) graph.add_node( "inner", - inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + inner.compile(interrupt_before=["inner_2"]), ) graph.add_node("outer_1", outer_1) graph.add_node("outer_2", outer_2) @@ -9045,9 +9044,7 @@ def test_doubly_nested_graph_interrupts( child = StateGraph(ChildState) child.add_node( "child_1", - grandchild.compile( - interrupt_before=["grandchild_2"], checkpointer=INHERIT_CHECKPOINTER - ), + grandchild.compile(interrupt_before=["grandchild_2"]), ) child.set_entry_point("child_1") child.set_finish_point("child_1") @@ -9060,7 +9057,7 @@ def test_doubly_nested_graph_interrupts( graph = StateGraph(State) graph.add_node("parent_1", parent_1) - graph.add_node("child", child.compile(checkpointer=INHERIT_CHECKPOINTER)) + graph.add_node("child", child.compile()) graph.add_node("parent_2", parent_2) graph.set_entry_point("parent_1") graph.add_edge("parent_1", "child") @@ -9154,7 +9151,7 @@ def test_nested_graph_state( graph.add_node("outer_1", outer_1) graph.add_node( "inner", - inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + inner.compile(interrupt_before=["inner_2"]), ) graph.add_node("outer_2", outer_2) graph.set_entry_point("outer_1") @@ -9632,9 +9629,7 @@ def test_doubly_nested_graph_state( child = StateGraph(ChildState) child.add_node( "child_1", - grandchild.compile( - interrupt_before=["grandchild_2"], checkpointer=INHERIT_CHECKPOINTER - ), + grandchild.compile(interrupt_before=["grandchild_2"]), ) child.set_entry_point("child_1") child.set_finish_point("child_1") @@ -9647,7 +9642,7 @@ def test_doubly_nested_graph_state( graph = StateGraph(State) graph.add_node("parent_1", parent_1) - graph.add_node("child", child.compile(checkpointer=INHERIT_CHECKPOINTER)) + graph.add_node("child", child.compile()) graph.add_node("parent_2", parent_2) graph.set_entry_point("parent_1") graph.add_edge("parent_1", "child") @@ -9917,9 +9912,7 @@ def test_send_to_nested_graphs( builder = StateGraph(OverallState) builder.add_node( "generate_joke", - subgraph.compile( - checkpointer=INHERIT_CHECKPOINTER, interrupt_before=["generate"] - ), + subgraph.compile(interrupt_before=["generate"]), ) builder.add_conditional_edges(START, continue_to_jokes) builder.add_edge("generate_joke", END) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 7b2e3222b..3c881e630 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -57,7 +57,6 @@ from langgraph.prebuilt.chat_agent_executor import ( from langgraph.prebuilt.tool_executor import ToolExecutor from langgraph.prebuilt.tool_node import ToolNode from langgraph.pregel import ( - INHERIT_CHECKPOINTER, Channel, GraphRecursionError, Pregel, @@ -6240,7 +6239,7 @@ async def test_nested_graph_interrupts( graph.add_node("outer_1", outer_1) graph.add_node( "inner", - inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + inner.compile(interrupt_before=["inner_2"]), ) graph.add_node("outer_2", outer_2) graph.set_entry_point("outer_1") @@ -7426,7 +7425,7 @@ async def test_nested_graph_interrupts_parallel( graph = StateGraph(State) graph.add_node( "inner", - inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + inner.compile(interrupt_before=["inner_2"]), ) graph.add_node("outer_1", outer_1) graph.add_node("outer_2", outer_2) @@ -7549,9 +7548,7 @@ async def test_doubly_nested_graph_interrupts( child = StateGraph(ChildState) child.add_node( "child_1", - grandchild.compile( - interrupt_before=["grandchild_2"], checkpointer=INHERIT_CHECKPOINTER - ), + grandchild.compile(interrupt_before=["grandchild_2"]), ) child.set_entry_point("child_1") child.set_finish_point("child_1") @@ -7564,7 +7561,7 @@ async def test_doubly_nested_graph_interrupts( graph = StateGraph(State) graph.add_node("parent_1", parent_1) - graph.add_node("child", child.compile(checkpointer=INHERIT_CHECKPOINTER)) + graph.add_node("child", child.compile()) graph.add_node("parent_2", parent_2) graph.set_entry_point("parent_1") graph.add_edge("parent_1", "child") @@ -7661,7 +7658,7 @@ async def test_nested_graph_state( graph.add_node("outer_1", outer_1) graph.add_node( "inner", - inner.compile(interrupt_before=["inner_2"], checkpointer=INHERIT_CHECKPOINTER), + inner.compile(interrupt_before=["inner_2"]), ) graph.add_node("outer_2", outer_2) graph.set_entry_point("outer_1") @@ -8143,9 +8140,7 @@ async def test_doubly_nested_graph_state( child = StateGraph(ChildState) child.add_node( "child_1", - grandchild.compile( - interrupt_before=["grandchild_2"], checkpointer=INHERIT_CHECKPOINTER - ), + grandchild.compile(interrupt_before=["grandchild_2"]), ) child.set_entry_point("child_1") child.set_finish_point("child_1") @@ -8158,7 +8153,7 @@ async def test_doubly_nested_graph_state( graph = StateGraph(State) graph.add_node("parent_1", parent_1) - graph.add_node("child", child.compile(checkpointer=INHERIT_CHECKPOINTER)) + graph.add_node("child", child.compile()) graph.add_node("parent_2", parent_2) graph.set_entry_point("parent_1") graph.add_edge("parent_1", "child") @@ -8428,9 +8423,7 @@ async def test_send_to_nested_graphs( builder = StateGraph(OverallState) builder.add_node( "generate_joke", - subgraph.compile( - checkpointer=INHERIT_CHECKPOINTER, interrupt_before=["generate"] - ), + subgraph.compile(interrupt_before=["generate"]), ) builder.add_conditional_edges(START, continue_to_jokes) builder.add_edge("generate_joke", END) From 7fa97898aaacc0dd0155674800e76852c148f6de Mon Sep 17 00:00:00 2001 From: vbarda Date: Wed, 14 Aug 2024 16:02:37 -0400 Subject: [PATCH 022/287] correctly propagate all subgraph attributes --- libs/langgraph/langgraph/pregel/__init__.py | 118 +++++++------------- libs/langgraph/tests/test_pregel.py | 72 ++++-------- libs/langgraph/tests/test_pregel_async.py | 74 +++++------- 3 files changed, 88 insertions(+), 176 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 9f583b5b5..5c89ed27f 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -190,15 +190,12 @@ class Channel: ) -def _get_nodes_and_channels( - nodes: Mapping[str, PregelNode], - channels: Mapping[str, BaseChannel], - checkpoint_ns: str, -) -> tuple[Mapping[str, PregelNode], Mapping[str, BaseChannel]]: +def _get_subgraph(graph: Pregel, checkpoint_ns: str) -> Pregel: if checkpoint_ns == "": - return nodes, channels + return graph path = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) + nodes = graph.nodes for subgraph_node_name in path: # if we have this separator it means we have a node that was triggered by Send if SEND_CHECKPOINT_NAMESPACE_SEPARATOR in subgraph_node_name: @@ -210,17 +207,17 @@ def _get_nodes_and_channels( if subgraph_node_name not in nodes: raise ValueError(f"Couldn't find node '{subgraph_node_name}'.") - subgraph_node = nodes[subgraph_node_name].get_node() - - if not isinstance(subgraph_node, RunnableSequence): - break - - first_step = subgraph_node.steps[0] - if isinstance(first_step, Pregel): - nodes = first_step.nodes - channels = first_step.channels - - return nodes, channels + subgraph_node = nodes[subgraph_node_name] + if isinstance(subgraph_node.bound, Pregel): + nodes = subgraph_node.bound.nodes + elif isinstance(subgraph_node.bound, RunnableSequence): + for runnable in subgraph_node.bound.steps: + if isinstance(runnable, Pregel): + nodes = runnable.nodes + break + else: + continue + return subgraph_node.bound def _assemble_state_snapshot_hierarchy( @@ -259,24 +256,21 @@ def _assemble_state_snapshot_hierarchy( def _prepare_state_snapshot( saved: CheckpointTuple, - nodes: Mapping[str, PregelNode], - channels: Mapping[str, BaseChannel], - managed_values_dict: dict[str, ManagedValueSpec], - select_channels: str | list[str], + graph: Pregel, ) -> StateSnapshot: with ChannelsManager( { k: LastValue(None) if isinstance(c, Context) else c - for k, c in channels.items() + for k, c in graph.channels.items() }, saved.checkpoint, saved.config, ) as channels, ManagedValuesManager( - managed_values_dict, ensure_config(saved.config) + graph.managed_values_dict, ensure_config(saved.config) ) as managed: next_tasks = prepare_next_tasks( saved.checkpoint, - nodes, + graph.nodes, channels, managed, saved.config, @@ -284,7 +278,7 @@ def _prepare_state_snapshot( for_execution=False, ) return StateSnapshot( - values=read_channels(channels, select_channels), + values=read_channels(channels, graph.stream_channels_asis), next=tuple(t.name for t in next_tasks), config=saved.config, metadata=saved.metadata, @@ -294,25 +288,21 @@ def _prepare_state_snapshot( async def _prepare_state_snapshot_async( - saved: CheckpointTuple, - nodes: Mapping[str, PregelNode], - channels: Mapping[str, BaseChannel], - managed_values_dict: dict[str, ManagedValueSpec], - select_channels: str | list[str], + saved: CheckpointTuple, graph: Pregel ) -> StateSnapshot: async with AsyncChannelsManager( { k: LastValue(None) if isinstance(c, Context) else c - for k, c in channels.items() + for k, c in graph.channels.items() }, saved.checkpoint, saved.config, ) as channels, AsyncManagedValuesManager( - managed_values_dict, ensure_config(saved.config) + graph.managed_values_dict, ensure_config(saved.config) ) as managed: next_tasks = prepare_next_tasks( saved.checkpoint, - nodes, + graph.nodes, channels, managed, saved.config, @@ -320,7 +310,7 @@ async def _prepare_state_snapshot_async( for_execution=False, ) return StateSnapshot( - values=read_channels(channels, select_channels), + values=read_channels(channels, graph.stream_channels_asis), next=tuple(t.name for t in next_tasks), config=saved.config, metadata=saved.metadata, @@ -534,9 +524,7 @@ class Pregel( checkpoint_id = checkpoint_config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} - checkpoint_ns_to_nodes_and_channels: dict[ - str, tuple[Mapping[str, PregelNode], Mapping[str, BaseChannel]] - ] = {} + checkpoint_ns_to_graph: dict[str, Pregel] = {} for checkpoint_tuple in checkpoint_tuples: saved_checkpoint_ns = checkpoint_tuple.config["configurable"][ "checkpoint_ns" @@ -555,22 +543,14 @@ class Pregel( existing_checkpoint_id is None or saved_checkpoint_id > existing_checkpoint_id ): - if saved_checkpoint_ns not in checkpoint_ns_to_nodes_and_channels: - checkpoint_ns_to_nodes_and_channels[ - saved_checkpoint_ns - ] = _get_nodes_and_channels( - self.nodes, self.channels, saved_checkpoint_ns + if saved_checkpoint_ns not in checkpoint_ns_to_graph: + checkpoint_ns_to_graph[saved_checkpoint_ns] = _get_subgraph( + self, saved_checkpoint_ns ) - nodes, channels = checkpoint_ns_to_nodes_and_channels[ - saved_checkpoint_ns - ] state_snapshot = _prepare_state_snapshot( checkpoint_tuple, - nodes, - channels, - self.managed_values_dict, - self.stream_channels_asis, + checkpoint_ns_to_graph[saved_checkpoint_ns], ) checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot checkpoint_ns_to_checkpoint_id[ @@ -610,9 +590,7 @@ class Pregel( checkpoint_id = checkpoint_config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} - checkpoint_ns_to_nodes_and_channels: dict[ - str, tuple[Mapping[str, PregelNode], Mapping[str, BaseChannel]] - ] = {} + checkpoint_ns_to_graph: dict[str, Pregel] = {} async for checkpoint_tuple in checkpoint_tuples: saved_checkpoint_ns = checkpoint_tuple.config["configurable"][ "checkpoint_ns" @@ -631,22 +609,14 @@ class Pregel( existing_checkpoint_id is None or saved_checkpoint_id > existing_checkpoint_id ): - if saved_checkpoint_ns not in checkpoint_ns_to_nodes_and_channels: - checkpoint_ns_to_nodes_and_channels[ - saved_checkpoint_ns - ] = _get_nodes_and_channels( - self.nodes, self.channels, saved_checkpoint_ns + if saved_checkpoint_ns not in checkpoint_ns_to_graph: + checkpoint_ns_to_graph[saved_checkpoint_ns] = _get_subgraph( + self, saved_checkpoint_ns ) - nodes, channels = checkpoint_ns_to_nodes_and_channels[ - saved_checkpoint_ns - ] state_snapshot = await _prepare_state_snapshot_async( checkpoint_tuple, - nodes, - channels, - self.managed_values_dict, - self.stream_channels_asis, + checkpoint_ns_to_graph[saved_checkpoint_ns], ) checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot checkpoint_ns_to_checkpoint_id[ @@ -698,17 +668,13 @@ class Pregel( ) yield state_snapshot else: - nodes, channels = _get_nodes_and_channels( - self.nodes, - self.channels, + graph = _get_subgraph( + self, checkpoint_tuple.config["configurable"]["checkpoint_ns"], ) yield _prepare_state_snapshot( checkpoint_tuple, - nodes, - channels, - self.managed_values_dict, - self.stream_channels_asis, + graph, ) async def aget_state_history( @@ -746,17 +712,13 @@ class Pregel( ) yield state_snapshot else: - nodes, channels = _get_nodes_and_channels( - self.nodes, - self.channels, + graph = _get_subgraph( + self, checkpoint_tuple.config["configurable"]["checkpoint_ns"], ) yield await _prepare_state_snapshot_async( checkpoint_tuple, - nodes, - channels, - self.managed_values_dict, - self.stream_channels_asis, + graph, ) def update_state( diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index aa1db664e..7bdc357d7 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -8602,7 +8602,7 @@ def test_nested_graph_interrupts( ] assert child_state_history == [ StateSnapshot( - values={"my_key": "hi my value here"}, + values={"my_key": "hi my value here", "my_other_key": "hi my value"}, next=("inner_2",), config={ "configurable": { @@ -9140,6 +9140,7 @@ def test_nested_graph_state( class State(TypedDict): my_key: str + other_parent_key: str def outer_1(state: State): return {"my_key": "hi " + state["my_key"]} @@ -9214,7 +9215,7 @@ def test_nested_graph_state( }, subgraph_state_snapshots={ "inner": StateSnapshot( - values={"my_key": "hi my value here"}, + values={"my_key": "hi my value here", "my_other_key": "hi my value"}, next=("inner_2",), config={ "configurable": { @@ -9271,7 +9272,10 @@ def test_nested_graph_state( }, subgraph_state_snapshots={ "inner": StateSnapshot( - values={"my_key": "hi my value here"}, + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, next=("inner_2",), config={ "configurable": { @@ -9376,7 +9380,10 @@ def test_nested_graph_state( {"configurable": {"thread_id": "1", "checkpoint_ns": "inner"}} ) assert child_snapshot == StateSnapshot( - values={"my_key": "hi my value here and there"}, + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, next=(), config={ "configurable": { @@ -9519,7 +9526,10 @@ def test_nested_graph_state( }, subgraph_state_snapshots={ "inner": StateSnapshot( - values={"my_key": "hi my value here and there"}, + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, next=(), config={ "configurable": { @@ -9931,6 +9941,13 @@ def test_send_to_nested_graphs( for subgraph_node in subgraph_nodes: assert subgraph_node.split(":")[0] == "generate_joke" + subgraph_state_snapshots = { + subgraph_node: graph.get_state( + {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} + ) + for subgraph_node in subgraph_nodes + } + expected_snapshot = StateSnapshot( values={"subjects": ["cats", "dogs"], "jokes": []}, next=("generate_joke", "generate_joke"), @@ -9950,50 +9967,7 @@ def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - subgraph_nodes[0]: StateSnapshot( - values={"jokes": []}, - next=("generate",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": {"edit": None}, "step": 1}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - subgraph_nodes[1]: StateSnapshot( - values={"jokes": []}, - next=("generate",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": {"edit": None}, "step": 1}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - }, + subgraph_state_snapshots=subgraph_state_snapshots, ) assert actual_snapshot == expected_snapshot diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 3c881e630..87d9240e0 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -7104,7 +7104,7 @@ async def test_nested_graph_interrupts( ] assert child_state_history == [ StateSnapshot( - values={"my_key": "hi my value here"}, + values={"my_key": "hi my value here", "my_other_key": "hi my value"}, next=("inner_2",), config={ "configurable": { @@ -7647,6 +7647,7 @@ async def test_nested_graph_state( class State(TypedDict): my_key: str + other_parent_key: str async def outer_1(state: State): return {"my_key": "hi " + state["my_key"]} @@ -7721,7 +7722,10 @@ async def test_nested_graph_state( }, subgraph_state_snapshots={ "inner": StateSnapshot( - values={"my_key": "hi my value here"}, + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, next=("inner_2",), config={ "configurable": { @@ -7780,7 +7784,10 @@ async def test_nested_graph_state( }, subgraph_state_snapshots={ "inner": StateSnapshot( - values={"my_key": "hi my value here"}, + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, next=("inner_2",), config={ "configurable": { @@ -7885,7 +7892,10 @@ async def test_nested_graph_state( {"configurable": {"thread_id": "1", "checkpoint_ns": "inner"}} ) assert child_snapshot == StateSnapshot( - values={"my_key": "hi my value here and there"}, + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, next=(), config={ "configurable": { @@ -8030,7 +8040,10 @@ async def test_nested_graph_state( }, subgraph_state_snapshots={ "inner": StateSnapshot( - values={"my_key": "hi my value here and there"}, + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, next=(), config={ "configurable": { @@ -8442,6 +8455,12 @@ async def test_send_to_nested_graphs( for subgraph_node in subgraph_nodes: assert subgraph_node.split(":")[0] == "generate_joke" + subgraph_state_snapshots = { + subgraph_node: await graph.aget_state( + {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} + ) + for subgraph_node in subgraph_nodes + } expected_snapshot = StateSnapshot( values={"subjects": ["cats", "dogs"], "jokes": []}, next=("generate_joke", "generate_joke"), @@ -8461,50 +8480,7 @@ async def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - subgraph_nodes[0]: StateSnapshot( - values={"jokes": []}, - next=("generate",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": {"edit": None}, "step": 1}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[0], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - subgraph_nodes[1]: StateSnapshot( - values={"jokes": []}, - next=("generate",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": {"edit": None}, "step": 1}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": subgraph_nodes[1], - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ), - }, + subgraph_state_snapshots=subgraph_state_snapshots, ) assert actual_snapshot == expected_snapshot From 45054df71ae48d9d8a44a0427f824f9bac2f759e Mon Sep 17 00:00:00 2001 From: vbarda Date: Wed, 14 Aug 2024 21:05:00 -0400 Subject: [PATCH 023/287] remove include_subgraph_state kwarg --- libs/langgraph/langgraph/pregel/__init__.py | 61 +-- libs/langgraph/tests/test_pregel.py | 489 ++++++++++++++++--- libs/langgraph/tests/test_pregel_async.py | 494 +++++++++++++++++--- 3 files changed, 862 insertions(+), 182 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 5c89ed27f..bc4fd48a0 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -506,26 +506,19 @@ class Pregel( yield runnable yield from runnable.subgraphs - def get_state( - self, config: RunnableConfig, *, include_subgraph_state: bool = False - ) -> StateSnapshot: + def get_state(self, config: RunnableConfig) -> StateSnapshot: """Get the current state of the graph.""" if not self.checkpointer: raise ValueError("No checkpointer set") checkpoint_tuple = self.checkpointer.get_tuple(config) - if include_subgraph_state: - checkpoint_tuples = self.checkpointer.list(config) - else: - checkpoint_tuples = iter([checkpoint_tuple] if checkpoint_tuple else []) - checkpoint_config = checkpoint_tuple.config if checkpoint_tuple else config checkpoint_ns = checkpoint_config["configurable"].get("checkpoint_ns", "") checkpoint_id = checkpoint_config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} checkpoint_ns_to_graph: dict[str, Pregel] = {} - for checkpoint_tuple in checkpoint_tuples: + for checkpoint_tuple in self.checkpointer.list(config): saved_checkpoint_ns = checkpoint_tuple.config["configurable"][ "checkpoint_ns" ] @@ -567,31 +560,19 @@ class Pregel( ) return state_snapshot - async def aget_state( - self, config: RunnableConfig, *, include_subgraph_state: bool = False - ) -> StateSnapshot: + async def aget_state(self, config: RunnableConfig) -> StateSnapshot: """Get the current state of the graph.""" if not self.checkpointer: raise ValueError("No checkpointer set") checkpoint_tuple = await self.checkpointer.aget_tuple(config) - if include_subgraph_state: - checkpoint_tuples = self.checkpointer.alist(config) - else: - - async def alist_checkpoints(): - if checkpoint_tuple: - yield checkpoint_tuple - - checkpoint_tuples = alist_checkpoints() - checkpoint_config = checkpoint_tuple.config if checkpoint_tuple else config checkpoint_ns = checkpoint_config["configurable"].get("checkpoint_ns", "") checkpoint_id = checkpoint_config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} checkpoint_ns_to_graph: dict[str, Pregel] = {} - async for checkpoint_tuple in checkpoint_tuples: + async for checkpoint_tuple in self.checkpointer.alist(config): saved_checkpoint_ns = checkpoint_tuple.config["configurable"][ "checkpoint_ns" ] @@ -640,7 +621,6 @@ class Pregel( filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, - include_subgraph_state: bool = False, ) -> Iterator[StateSnapshot]: """Get the history of the state of the graph.""" if not self.checkpointer: @@ -662,20 +642,8 @@ class Pregel( # only list root checkpoints here continue - if include_subgraph_state: - state_snapshot = self.get_state( - checkpoint_tuple.config, include_subgraph_state=True - ) - yield state_snapshot - else: - graph = _get_subgraph( - self, - checkpoint_tuple.config["configurable"]["checkpoint_ns"], - ) - yield _prepare_state_snapshot( - checkpoint_tuple, - graph, - ) + state_snapshot = self.get_state(checkpoint_tuple.config) + yield state_snapshot async def aget_state_history( self, @@ -684,7 +652,6 @@ class Pregel( filter: Optional[Dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None, - include_subgraph_state: bool = False, ) -> AsyncIterator[StateSnapshot]: """Get the history of the state of the graph.""" if not self.checkpointer: @@ -706,20 +673,8 @@ class Pregel( # only list root checkpoints here continue - if include_subgraph_state: - state_snapshot = await self.aget_state( - checkpoint_tuple.config, include_subgraph_state=True - ) - yield state_snapshot - else: - graph = _get_subgraph( - self, - checkpoint_tuple.config["configurable"]["checkpoint_ns"], - ) - yield await _prepare_state_snapshot_async( - checkpoint_tuple, - graph, - ) + state_snapshot = await self.aget_state(checkpoint_tuple.config) + yield state_snapshot def update_state( self, diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 7bdc357d7..24bfca34d 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -7783,6 +7783,41 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -7900,6 +7935,41 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8071,6 +8141,41 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8193,6 +8298,41 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + next=(), + config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8245,7 +8385,6 @@ def test_nested_graph_interrupts( "my_key": "hi my value", }, ] - # interrupted after "inner" assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value"}, @@ -8270,6 +8409,41 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8315,6 +8489,7 @@ def test_nested_graph_interrupts( "my_key": "hi my value here and there", }, ] + # interrupted after "inner" assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value here and there"}, @@ -8363,6 +8538,41 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + next=(), + config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8482,6 +8692,41 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + next=(), + config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8554,6 +8799,41 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8672,6 +8952,40 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + ), + }, ), StateSnapshot( values={"my_key": "hi my value"}, @@ -8696,6 +9010,40 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + ), + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8815,6 +9163,41 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "hi my value"}, @@ -8839,6 +9222,41 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + next=(), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -9165,32 +9583,7 @@ def test_nested_graph_state( config = {"configurable": {"thread_id": "1"}} app.invoke({"my_key": "my value"}, config, debug=True) # test state w/ nested subgraph state (right after interrupt) - assert app.get_state(config, include_subgraph_state=False) == StateSnapshot( - values={"my_key": "hi my value"}, - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + assert app.get_state(config) == StateSnapshot( values={"my_key": "hi my value"}, next=("inner",), config={ @@ -9246,7 +9639,7 @@ def test_nested_graph_state( ) }, ) - assert list(app.get_state_history(config, include_subgraph_state=True)) == [ + assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value"}, next=("inner",), @@ -9349,7 +9742,7 @@ def test_nested_graph_state( ] app.invoke(None, config, debug=True) # test state w/ nested subgraph state (after resuming from interrupt) - assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + assert app.get_state(config) == StateSnapshot( values={"my_key": "hi my value here and there and back again"}, next=(), config={ @@ -9421,7 +9814,6 @@ def test_nested_graph_state( "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], } }, - include_subgraph_state=True, ) == StateSnapshot( values={"my_key": "hi my value"}, next=("inner",), @@ -9448,7 +9840,7 @@ def test_nested_graph_state( subgraph_state_snapshots={"inner": child_snapshot}, ) # test full history at the end - assert list(app.get_state_history(config, include_subgraph_state=True)) == [ + assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value here and there and back again"}, next=(), @@ -9665,31 +10057,6 @@ def test_doubly_nested_graph_state( config = {"configurable": {"thread_id": "1"}} app.invoke({"my_key": "my value"}, config, debug=True) assert app.get_state(config) == StateSnapshot( - values={"my_key": "hi my value"}, - next=("child",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"parent_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( values={"my_key": "hi my value"}, next=("child",), config={ @@ -9763,7 +10130,7 @@ def test_doubly_nested_graph_state( }, ) app.invoke(None, config, debug=True) - assert app.get_state(config, include_subgraph_state=True) == StateSnapshot( + assert app.get_state(config) == StateSnapshot( values={"my_key": "hi my value here and there and back again"}, next=(), config={ @@ -9821,7 +10188,6 @@ def test_doubly_nested_graph_state( # test getting child snapshot child_snapshot = app.get_state( {"configurable": {"thread_id": "1", "checkpoint_ns": "child"}}, - include_subgraph_state=True, ) assert child_snapshot == StateSnapshot( values={"my_key": "hi my value here and there"}, @@ -9856,7 +10222,6 @@ def test_doubly_nested_graph_state( "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], } }, - include_subgraph_state=True, ) == StateSnapshot( values={"my_key": "hi my value"}, next=("child",), @@ -9935,7 +10300,7 @@ def test_send_to_nested_graphs( "subjects": ["cats", "dogs"], "jokes": [], } - actual_snapshot = graph.get_state(config, include_subgraph_state=True) + actual_snapshot = graph.get_state(config) subgraph_nodes = list(actual_snapshot.subgraph_state_snapshots.keys()) assert len(subgraph_nodes) == 2 for subgraph_node in subgraph_nodes: @@ -9977,7 +10342,7 @@ def test_send_to_nested_graphs( "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], } - actual_snapshot = graph.get_state(config, include_subgraph_state=True) + actual_snapshot = graph.get_state(config) expected_snapshot = StateSnapshot( values={ "subjects": ["cats", "dogs"], @@ -10013,7 +10378,7 @@ def test_send_to_nested_graphs( assert actual_snapshot == expected_snapshot # test full history - actual_history = list(graph.get_state_history(config, include_subgraph_state=True)) + actual_history = list(graph.get_state_history(config)) # get subgraph node state for expected history subgraph_state_snapshots = { diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 87d9240e0..7aae5308b 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -6278,6 +6278,41 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -6395,6 +6430,41 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -6571,6 +6641,41 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -6693,6 +6798,41 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + next=(), + config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -6772,6 +6912,41 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -6817,6 +6992,7 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here and there", }, ] + # interrupted after "inner" assert [s async for s in app.aget_state_history(config)] == [ StateSnapshot( values={"my_key": "hi my value here and there"}, @@ -6865,6 +7041,41 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + next=(), + config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -6984,6 +7195,41 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + next=(), + config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -7055,6 +7301,41 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -7174,6 +7455,40 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + ), + }, ), StateSnapshot( values={"my_key": "hi my value"}, @@ -7198,6 +7513,40 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + ), + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -7317,6 +7666,41 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "hi my value"}, @@ -7341,6 +7725,41 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, + subgraph_state_snapshots={ + "inner": StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + next=(), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, ), StateSnapshot( values={"my_key": "my value"}, @@ -7672,32 +8091,7 @@ async def test_nested_graph_state( config = {"configurable": {"thread_id": "1"}} await app.ainvoke({"my_key": "my value"}, config, debug=True) # test state w/ nested subgraph state (right after interrupt) - assert await app.aget_state(config, include_subgraph_state=False) == StateSnapshot( - values={"my_key": "hi my value"}, - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - assert await app.aget_state(config, include_subgraph_state=True) == StateSnapshot( + assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value"}, next=("inner",), config={ @@ -7756,9 +8150,7 @@ async def test_nested_graph_state( ) }, ) - assert [ - s async for s in app.aget_state_history(config, include_subgraph_state=True) - ] == [ + assert [s async for s in app.aget_state_history(config)] == [ StateSnapshot( values={"my_key": "hi my value"}, next=("inner",), @@ -7861,7 +8253,7 @@ async def test_nested_graph_state( ] await app.ainvoke(None, config, debug=True) # test state w/ nested subgraph state (after resuming from interrupt) - assert await app.aget_state(config, include_subgraph_state=True) == StateSnapshot( + assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value here and there and back again"}, next=(), config={ @@ -7933,7 +8325,6 @@ async def test_nested_graph_state( "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], } }, - include_subgraph_state=True, ) == StateSnapshot( values={"my_key": "hi my value"}, next=("inner",), @@ -7960,9 +8351,7 @@ async def test_nested_graph_state( subgraph_state_snapshots={"inner": child_snapshot}, ) # test full history at the end - assert [ - s async for s in app.aget_state_history(config, include_subgraph_state=True) - ] == [ + assert [s async for s in app.aget_state_history(config)] == [ StateSnapshot( values={"my_key": "hi my value here and there and back again"}, next=(), @@ -8179,31 +8568,6 @@ async def test_doubly_nested_graph_state( config = {"configurable": {"thread_id": "1"}} await app.ainvoke({"my_key": "my value"}, config, debug=True) assert await app.aget_state(config) == StateSnapshot( - values={"my_key": "hi my value"}, - next=("child",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"parent_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - assert await app.aget_state(config, include_subgraph_state=True) == StateSnapshot( values={"my_key": "hi my value"}, next=("child",), config={ @@ -8277,7 +8641,7 @@ async def test_doubly_nested_graph_state( }, ) await app.ainvoke(None, config, debug=True) - assert await app.aget_state(config, include_subgraph_state=True) == StateSnapshot( + assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value here and there and back again"}, next=(), config={ @@ -8335,7 +8699,6 @@ async def test_doubly_nested_graph_state( # test getting child snapshot child_snapshot = await app.aget_state( {"configurable": {"thread_id": "1", "checkpoint_ns": "child"}}, - include_subgraph_state=True, ) assert child_snapshot == StateSnapshot( values={"my_key": "hi my value here and there"}, @@ -8370,7 +8733,6 @@ async def test_doubly_nested_graph_state( "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], } }, - include_subgraph_state=True, ) == StateSnapshot( values={"my_key": "hi my value"}, next=("child",), @@ -8449,7 +8811,7 @@ async def test_send_to_nested_graphs( "subjects": ["cats", "dogs"], "jokes": [], } - actual_snapshot = await graph.aget_state(config, include_subgraph_state=True) + actual_snapshot = await graph.aget_state(config) subgraph_nodes = list(actual_snapshot.subgraph_state_snapshots.keys()) assert len(subgraph_nodes) == 2 for subgraph_node in subgraph_nodes: @@ -8490,7 +8852,7 @@ async def test_send_to_nested_graphs( "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], } - actual_snapshot = await graph.aget_state(config, include_subgraph_state=True) + actual_snapshot = await graph.aget_state(config) expected_snapshot = StateSnapshot( values={ "subjects": ["cats", "dogs"], @@ -8526,9 +8888,7 @@ async def test_send_to_nested_graphs( assert actual_snapshot == expected_snapshot # test full history - actual_history = [ - c async for c in graph.aget_state_history(config, include_subgraph_state=True) - ] + actual_history = [c async for c in graph.aget_state_history(config)] # get subgraph node state for expected history subgraph_state_snapshots = { subgraph_node: await graph.aget_state( From b5429b63424d948383e9ff5cfde83a6fd879189e Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Fri, 16 Aug 2024 15:42:05 -0700 Subject: [PATCH 024/287] Use START --- .../agent-simulation-evaluation.ipynb | 5 +- .../information-gather-prompting.ipynb | 2 + .../human_in_the_loop/review-tool-calls.ipynb | 65 +++--- examples/input_output_schema.ipynb | 4 + examples/llm-compiler/LLMCompiler.ipynb | 10 +- examples/many-tools.ipynb | 6 +- examples/pass-run-time-values-to-tools.ipynb | 1 + examples/pass_private_state.ipynb | 4 +- examples/persistence_mongodb.ipynb | 14 +- examples/persistence_postgres.ipynb | 6 +- examples/persistence_redis.ipynb | 12 +- examples/reflection/reflection.ipynb | 2 +- examples/reflexion/reflexion.ipynb | 1 + examples/streaming-content.ipynb | 4 +- ...-from-within-tools-without-langchain.ipynb | 201 +----------------- .../streaming-tokens-without-langchain.ipynb | 182 +--------------- .../tutorials/rag-agent-testing-local.ipynb | 21 +- .../tutorials/tool-calling-agent-local.ipynb | 4 + examples/web-navigation/web_voyager.ipynb | 4 +- 19 files changed, 114 insertions(+), 434 deletions(-) diff --git a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb index c41653c50..02f8782eb 100644 --- a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb +++ b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb @@ -235,7 +235,7 @@ " # Call the chat bot\n", " chat_bot_response = my_chat_bot(messages)\n", " # Respond with an AI Message\n", - " return {\"messages\":[AIMessage(content=chat_bot_response[\"content\"])]}" + " return {\"messages\": [AIMessage(content=chat_bot_response[\"content\"])]}" ] }, { @@ -270,7 +270,7 @@ " # Call the simulated user\n", " response = simulated_user.invoke({\"messages\": new_messages})\n", " # This response is an AI message - we need to flip this to be a human message\n", - " return {\"messages\":[HumanMessage(content=response.content)]}" + " return {\"messages\": [HumanMessage(content=response.content)]}" ] }, { @@ -331,6 +331,7 @@ "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "graph_builder = StateGraph(State)\n", "graph_builder.add_node(\"user\", simulated_user_node)\n", "graph_builder.add_node(\"chat_bot\", chat_bot_node)\n", diff --git a/examples/chatbots/information-gather-prompting.ipynb b/examples/chatbots/information-gather-prompting.ipynb index bc1248cda..d374f5d57 100644 --- a/examples/chatbots/information-gather-prompting.ipynb +++ b/examples/chatbots/information-gather-prompting.ipynb @@ -182,9 +182,11 @@ "from typing import Annotated\n", "from typing_extensions import TypedDict\n", "\n", + "\n", "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "memory = MemorySaver()\n", "workflow = StateGraph(State)\n", "workflow.add_node(\"info\", chain)\n", diff --git a/examples/human_in_the_loop/review-tool-calls.ipynb b/examples/human_in_the_loop/review-tool-calls.ipynb index 5a4aeaf56..84373b90b 100644 --- a/examples/human_in_the_loop/review-tool-calls.ipynb +++ b/examples/human_in_the_loop/review-tool-calls.ipynb @@ -141,15 +141,18 @@ " print(\"----\")\n", " return \"Sunny!\"\n", "\n", - "model = ChatAnthropic(model_name=\"claude-3-5-sonnet-20240620\").bind_tools([weather_search])\n", + "\n", + "model = ChatAnthropic(model_name=\"claude-3-5-sonnet-20240620\").bind_tools(\n", + " [weather_search]\n", + ")\n", + "\n", "\n", "class State(MessagesState):\n", " \"\"\"Simple state.\"\"\"\n", "\n", + "\n", "def call_llm(state):\n", - " return {\n", - " \"messages\": [model.invoke(state['messages'])]\n", - " }\n", + " return {\"messages\": [model.invoke(state[\"messages\"])]}\n", "\n", "\n", "def human_review_node(state):\n", @@ -159,28 +162,30 @@ "def run_tool(state):\n", " new_messages = []\n", " tools = {\"weather_search\": weather_search}\n", - " tool_calls = state['messages'][-1].tool_calls\n", + " tool_calls = state[\"messages\"][-1].tool_calls\n", " for tool_call in tool_calls:\n", - " tool = tools[tool_call['name']]\n", - " result = tool.invoke(tool_call['args'])\n", - " new_messages.append({\n", - " \"role\": \"tool\",\n", - " \"name\": tool_call['name'],\n", - " \"content\": result,\n", - " \"tool_call_id\": tool_call['id']\n", - " })\n", + " tool = tools[tool_call[\"name\"]]\n", + " result = tool.invoke(tool_call[\"args\"])\n", + " new_messages.append(\n", + " {\n", + " \"role\": \"tool\",\n", + " \"name\": tool_call[\"name\"],\n", + " \"content\": result,\n", + " \"tool_call_id\": tool_call[\"id\"],\n", + " }\n", + " )\n", " return {\"messages\": new_messages}\n", "\n", "\n", "def route_after_llm(state) -> Literal[END, \"human_review_node\"]:\n", - " if len(state['messages'][-1].tool_calls) == 0:\n", + " if len(state[\"messages\"][-1].tool_calls) == 0:\n", " return END\n", " else:\n", " return \"human_review_node\"\n", "\n", "\n", "def route_after_human(state) -> Literal[\"run_tool\", \"call_llm\"]:\n", - " if isinstance(state['messages'][-1], AIMessage):\n", + " if isinstance(state[\"messages\"][-1], AIMessage):\n", " return \"run_tool\"\n", " else:\n", " return \"call_llm\"\n", @@ -460,35 +465,35 @@ "print(\"Current State:\")\n", "print(state.values)\n", "print(\"\\nCurrent Tool Call ID:\")\n", - "current_content = state.values['messages'][-1].content\n", - "current_id = state.values['messages'][-1].id\n", - "tool_call_id = state.values['messages'][-1].tool_calls[0]['id']\n", + "current_content = state.values[\"messages\"][-1].content\n", + "current_id = state.values[\"messages\"][-1].id\n", + "tool_call_id = state.values[\"messages\"][-1].tool_calls[0][\"id\"]\n", "print(tool_call_id)\n", "\n", "# We now need to construct a replacement tool call.\n", "# We will change the argument to be `San Francisco, USA`\n", "# Note that we could change any number of arguments or tool names - it just has to be a valid one\n", "new_message = {\n", - " \"role\": \"assistant\", \n", + " \"role\": \"assistant\",\n", " \"content\": current_content,\n", " \"tool_calls\": [\n", " {\n", " \"id\": tool_call_id,\n", " \"name\": \"weather_search\",\n", - " \"args\": {\"city\": \"San Francisco, USA\"}\n", + " \"args\": {\"city\": \"San Francisco, USA\"},\n", " }\n", " ],\n", " # This is important - this needs to be the same as the message you replacing!\n", " # Otherwise, it will show up as a separate message\n", - " \"id\": current_id\n", + " \"id\": current_id,\n", "}\n", "graph.update_state(\n", " # This is the config which represents this thread\n", - " thread, \n", + " thread,\n", " # This is the updated value we want to push\n", - " {\"messages\": [new_message]}, \n", + " {\"messages\": [new_message]},\n", " # We push this update acting as our human_review_node\n", - " as_node=\"human_review_node\"\n", + " as_node=\"human_review_node\",\n", ")\n", "\n", "# Let's now continue executing from here\n", @@ -595,26 +600,26 @@ "print(\"Current State:\")\n", "print(state.values)\n", "print(\"\\nCurrent Tool Call ID:\")\n", - "tool_call_id = state.values['messages'][-1].tool_calls[0]['id']\n", + "tool_call_id = state.values[\"messages\"][-1].tool_calls[0][\"id\"]\n", "print(tool_call_id)\n", "\n", "# We now need to construct a replacement tool call.\n", "# We will change the argument to be `San Francisco, USA`\n", "# Note that we could change any number of arguments or tool names - it just has to be a valid one\n", "new_message = {\n", - " \"role\": \"tool\", \n", + " \"role\": \"tool\",\n", " # This is our natural language feedback\n", " \"content\": \"User requested changes: pass in the country as well\",\n", " \"name\": \"weather_search\",\n", - " \"tool_call_id\": tool_call_id\n", + " \"tool_call_id\": tool_call_id,\n", "}\n", "graph.update_state(\n", " # This is the config which represents this thread\n", - " thread, \n", + " thread,\n", " # This is the updated value we want to push\n", - " {\"messages\": [new_message]}, \n", + " {\"messages\": [new_message]},\n", " # We push this update acting as our human_review_node\n", - " as_node=\"human_review_node\"\n", + " as_node=\"human_review_node\",\n", ")\n", "\n", "# Let's now continue executing from here\n", diff --git a/examples/input_output_schema.ipynb b/examples/input_output_schema.ipynb index 16779ba6e..4837b40a8 100644 --- a/examples/input_output_schema.ipynb +++ b/examples/input_output_schema.ipynb @@ -33,15 +33,19 @@ "from langgraph.graph import StateGraph, START, END\n", "from typing import TypedDict\n", "\n", + "\n", "class InputState(TypedDict):\n", " question: str\n", "\n", + "\n", "class OutputState(TypedDict):\n", " answer: str\n", "\n", + "\n", "def answer_node(state: InputState):\n", " return {\"answer\": \"bye\"}\n", "\n", + "\n", "graph = StateGraph(input=InputState, output=OutputState)\n", "graph.add_node(answer_node)\n", "graph.add_edge(START, \"answer_node\")\n", diff --git a/examples/llm-compiler/LLMCompiler.ipynb b/examples/llm-compiler/LLMCompiler.ipynb index bfe5736d8..2f2aa81ed 100644 --- a/examples/llm-compiler/LLMCompiler.ipynb +++ b/examples/llm-compiler/LLMCompiler.ipynb @@ -526,7 +526,7 @@ " \"tasks\": tasks,\n", " }\n", " )\n", - " return {\"messages\":[scheduled_tasks]}" + " return {\"messages\": [scheduled_tasks]}" ] }, { @@ -653,7 +653,7 @@ " )\n", " ]\n", " else:\n", - " return {\"messages\":response + [AIMessage(content=decision.action.response)]}\n", + " return {\"messages\": response + [AIMessage(content=decision.action.response)]}\n", "\n", "\n", "def select_recent_messages(state) -> dict:\n", @@ -726,9 +726,11 @@ "from langgraph.graph.message import add_messages\n", "from typing import Annotated\n", "\n", + "\n", "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "graph_builder = StateGraph(State)\n", "\n", "# 1. Define vertices\n", @@ -794,7 +796,9 @@ } ], "source": [ - "for step in chain.stream({\"messages\":[HumanMessage(content=\"What's the GDP of New York?\")]}):\n", + "for step in chain.stream(\n", + " {\"messages\": [HumanMessage(content=\"What's the GDP of New York?\")]}\n", + "):\n", " print(step)\n", " print(\"---\")" ] diff --git a/examples/many-tools.ipynb b/examples/many-tools.ipynb index b415f5065..107c9921f 100644 --- a/examples/many-tools.ipynb +++ b/examples/many-tools.ipynb @@ -328,9 +328,9 @@ " \"set more_information_needed False and populate a blank string for the query.\"\n", " )\n", " input_messages = [system] + state[\"messages\"]\n", - " response = llm.bind_tools(\n", - " [QueryForTools], tool_choice=True\n", - " ).invoke(input_messages)\n", + " response = llm.bind_tools([QueryForTools], tool_choice=True).invoke(\n", + " input_messages\n", + " )\n", " query = response.tool_calls[0][\"args\"][\"query\"]\n", " tool_documents = vector_store.similarity_search(query)\n", " if hack_remove_tool_condition:\n", diff --git a/examples/pass-run-time-values-to-tools.ipynb b/examples/pass-run-time-values-to-tools.ipynb index 3c74bd06d..ab6680230 100644 --- a/examples/pass-run-time-values-to-tools.ipynb +++ b/examples/pass-run-time-values-to-tools.ipynb @@ -329,6 +329,7 @@ "\n", "tools = [get_context, cite_context_sources]\n", "\n", + "\n", "# Define the function that calls the model\n", "def call_model(state, config):\n", " messages = state[\"messages\"]\n", diff --git a/examples/pass_private_state.ipynb b/examples/pass_private_state.ipynb index 2e60d805e..d34ecc7e1 100644 --- a/examples/pass_private_state.ipynb +++ b/examples/pass_private_state.ipynb @@ -72,12 +72,12 @@ "# Node to retrieve documents\n", "def retrieve_documents(state: QueryOutputState) -> DocumentOutputState:\n", " # Replace this with real logic\n", - " return {\"docs\": [state['query']] * 2}\n", + " return {\"docs\": [state[\"query\"]] * 2}\n", "\n", "\n", "# Node to generate answer\n", "def generate(state: GenerateInputState) -> OverallState:\n", - " return {\"answer\": \"\\n\\n\".join(state['docs'] + [state['question']])}\n", + " return {\"answer\": \"\\n\\n\".join(state[\"docs\"] + [state[\"question\"]])}\n", "\n", "\n", "graph = StateGraph(OverallState)\n", diff --git a/examples/persistence_mongodb.ipynb b/examples/persistence_mongodb.ipynb index 99ce73057..54dfdc638 100644 --- a/examples/persistence_mongodb.ipynb +++ b/examples/persistence_mongodb.ipynb @@ -630,7 +630,7 @@ " upsert=True,\n", " )\n", " )\n", - " await self.db[\"checkpoint_writes\"].bulk_write(operations)\n" + " await self.db[\"checkpoint_writes\"].bulk_write(operations)" ] }, { @@ -685,7 +685,9 @@ "metadata": {}, "outputs": [], "source": [ - "with MongoDBSaver.from_conn_info(host=\"localhost\", port=27017, db_name=\"checkpoints\") as checkpointer:\n", + "with MongoDBSaver.from_conn_info(\n", + " host=\"localhost\", port=27017, db_name=\"checkpoints\"\n", + ") as checkpointer:\n", " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", " config = {\"configurable\": {\"thread_id\": \"1\"}}\n", " res = graph.invoke({\"messages\": [(\"human\", \"what's the weather in sf\")]}, config)\n", @@ -796,10 +798,14 @@ "metadata": {}, "outputs": [], "source": [ - "async with AsyncMongoDBSaver.from_conn_info(host=\"localhost\", port=27017, db_name=\"checkpoints\") as checkpointer:\n", + "async with AsyncMongoDBSaver.from_conn_info(\n", + " host=\"localhost\", port=27017, db_name=\"checkpoints\"\n", + ") as checkpointer:\n", " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", " config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - " res = await graph.ainvoke({\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config)\n", + " res = await graph.ainvoke(\n", + " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n", + " )\n", "\n", " latest_checkpoint = await checkpointer.aget(config)\n", " latest_checkpoint_tuple = await checkpointer.aget_tuple(config)\n", diff --git a/examples/persistence_postgres.ipynb b/examples/persistence_postgres.ipynb index ef8be17cb..6f0a814b4 100644 --- a/examples/persistence_postgres.ipynb +++ b/examples/persistence_postgres.ipynb @@ -134,7 +134,7 @@ "source": [ "from psycopg.rows import dict_row\n", "\n", - "connection_kwargs ={\n", + "connection_kwargs = {\n", " \"autocommit\": True,\n", " \"prepare_threshold\": 0,\n", " \"row_factory\": dict_row,\n", @@ -166,7 +166,7 @@ " # Example configuration\n", " conninfo=DB_URI,\n", " max_size=20,\n", - " kwargs=connection_kwargs\n", + " kwargs=connection_kwargs,\n", ")\n", "\n", "with pool.connection() as conn:\n", @@ -394,7 +394,7 @@ " # Example configuration\n", " conninfo=DB_URI,\n", " max_size=20,\n", - " kwargs=connection_kwargs\n", + " kwargs=connection_kwargs,\n", ") as pool, pool.connection() as conn:\n", " checkpointer = AsyncPostgresSaver(conn)\n", "\n", diff --git a/examples/persistence_redis.ipynb b/examples/persistence_redis.ipynb index 3b2ad1170..34d78a53f 100644 --- a/examples/persistence_redis.ipynb +++ b/examples/persistence_redis.ipynb @@ -530,7 +530,9 @@ "\n", " @classmethod\n", " @asynccontextmanager\n", - " async def from_conn_info(cls, *, host: str, port: int, db: int) -> AsyncIterator[\"AsyncRedisSaver\"]:\n", + " async def from_conn_info(\n", + " cls, *, host: str, port: int, db: int\n", + " ) -> AsyncIterator[\"AsyncRedisSaver\"]:\n", " conn = None\n", " try:\n", " conn = AsyncRedis(host=host, port=port, db=db)\n", @@ -887,10 +889,14 @@ "metadata": {}, "outputs": [], "source": [ - "async with AsyncRedisSaver.from_conn_info(host=\"localhost\", port=6379, db=0) as checkpointer:\n", + "async with AsyncRedisSaver.from_conn_info(\n", + " host=\"localhost\", port=6379, db=0\n", + ") as checkpointer:\n", " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", " config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - " res = await graph.ainvoke({\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config)\n", + " res = await graph.ainvoke(\n", + " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n", + " )\n", "\n", " latest_checkpoint = await checkpointer.aget(config)\n", " latest_checkpoint_tuple = await checkpointer.aget_tuple(config)\n", diff --git a/examples/reflection/reflection.ipynb b/examples/reflection/reflection.ipynb index 810990e38..ca42160bd 100644 --- a/examples/reflection/reflection.ipynb +++ b/examples/reflection/reflection.ipynb @@ -269,7 +269,7 @@ "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", - " \n", + "\n", "async def generation_node(state: Sequence[BaseMessage]):\n", " return await generate.ainvoke({\"messages\": state})\n", "\n", diff --git a/examples/reflexion/reflexion.ipynb b/examples/reflexion/reflexion.ipynb index 670e6eb5b..8c183c817 100644 --- a/examples/reflexion/reflexion.ipynb +++ b/examples/reflexion/reflexion.ipynb @@ -392,6 +392,7 @@ "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "MAX_ITERATIONS = 5\n", "builder = StateGraph(State)\n", "builder.add_node(\"draft\", first_responder.respond)\n", diff --git a/examples/streaming-content.ipynb b/examples/streaming-content.ipynb index ff182ca21..8c2c7ab36 100644 --- a/examples/streaming-content.ipynb +++ b/examples/streaming-content.ipynb @@ -68,7 +68,9 @@ " # It's completely optional, but useful if you have many functions with similar names\n", " gen = RunnableGenerator(my_generator).with_config(\n", " tags=[\"should_stream\"],\n", - " callbacks=config.get(\"callbacks\", []) # <-- Propagate callbacks (Python <= 3.10)\n", + " callbacks=config.get(\n", + " \"callbacks\", []\n", + " ), # <-- Propagate callbacks (Python <= 3.10)\n", " )\n", " async for message in gen.astream(state):\n", " messages.append(message)\n", diff --git a/examples/streaming-events-from-within-tools-without-langchain.ipynb b/examples/streaming-events-from-within-tools-without-langchain.ipynb index e0906c837..ab139c602 100644 --- a/examples/streaming-events-from-within-tools-without-langchain.ipynb +++ b/examples/streaming-events-from-within-tools-without-langchain.ipynb @@ -30,10 +30,7 @@ "id": "47f79af8-58d8-4a48-8d9a-88823d88701f", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph openai" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph openai"] }, { "cell_type": "code", @@ -49,18 +46,7 @@ ] } ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"] }, { "cell_type": "markdown", @@ -84,94 +70,7 @@ "id": "d59234f9-173e-469d-a725-c13e0979663e", "metadata": {}, "outputs": [], - "source": [ - "from openai import AsyncOpenAI\n", - "from langchain_core.language_models.chat_models import ChatGenerationChunk\n", - "from langchain_core.messages import AIMessageChunk\n", - "from langchain_core.runnables.config import (\n", - " ensure_config,\n", - " get_callback_manager_for_config,\n", - ")\n", - "\n", - "openai_client = AsyncOpenAI()\n", - "# define tool schema for openai tool calling\n", - "\n", - "tool = {\n", - " \"type\": \"function\",\n", - " \"function\": {\n", - " \"name\": \"get_items\",\n", - " \"description\": \"Use this tool to look up which items are in the given place.\",\n", - " \"parameters\": {\n", - " \"type\": \"object\",\n", - " \"properties\": {\"place\": {\"type\": \"string\"}},\n", - " \"required\": [\"place\"],\n", - " },\n", - " },\n", - "}\n", - "\n", - "\n", - "async def call_model(state, config=None):\n", - " config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n", - " callback_manager = get_callback_manager_for_config(config)\n", - " messages = state[\"messages\"]\n", - "\n", - " llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n", - " response = await openai_client.chat.completions.create(\n", - " messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n", - " )\n", - "\n", - " response_content = \"\"\n", - " role = None\n", - "\n", - " tool_call_id = None\n", - " tool_call_function_name = None\n", - " tool_call_function_arguments = \"\"\n", - " async for chunk in response:\n", - " delta = chunk.choices[0].delta\n", - " if delta.role is not None:\n", - " role = delta.role\n", - "\n", - " if delta.content:\n", - " response_content += delta.content\n", - " llm_run_manager.on_llm_new_token(delta.content)\n", - "\n", - " if delta.tool_calls:\n", - " # note: for simplicity we're only handling a single tool call here\n", - " if delta.tool_calls[0].function.name is not None:\n", - " tool_call_function_name = delta.tool_calls[0].function.name\n", - " tool_call_id = delta.tool_calls[0].id\n", - "\n", - " # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n", - " tool_call_chunk = ChatGenerationChunk(\n", - " message=AIMessageChunk(\n", - " content=\"\",\n", - " additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n", - " )\n", - " )\n", - " llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n", - " tool_call_function_arguments += delta.tool_calls[0].function.arguments\n", - "\n", - " if tool_call_function_name is not None:\n", - " tool_calls = [\n", - " {\n", - " \"id\": tool_call_id,\n", - " \"function\": {\n", - " \"name\": tool_call_function_name,\n", - " \"arguments\": tool_call_function_arguments,\n", - " },\n", - " \"type\": \"function\",\n", - " }\n", - " ]\n", - " else:\n", - " tool_calls = None\n", - "\n", - " response_message = {\n", - " \"role\": role,\n", - " \"content\": response_content,\n", - " \"tool_calls\": tool_calls,\n", - " }\n", - " return {\"messages\": [response_message]}" - ] + "source": ["from openai import AsyncOpenAI\nfrom langchain_core.language_models.chat_models import ChatGenerationChunk\nfrom langchain_core.messages import AIMessageChunk\nfrom langchain_core.runnables.config import (\n ensure_config,\n get_callback_manager_for_config,\n)\n\nopenai_client = AsyncOpenAI()\n# define tool schema for openai tool calling\n\ntool = {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"get_items\",\n \"description\": \"Use this tool to look up which items are in the given place.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\"place\": {\"type\": \"string\"}},\n \"required\": [\"place\"],\n },\n },\n}\n\n\nasync def call_model(state, config=None):\n config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n callback_manager = get_callback_manager_for_config(config)\n messages = state[\"messages\"]\n\n llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n response = await openai_client.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n )\n\n response_content = \"\"\n role = None\n\n tool_call_id = None\n tool_call_function_name = None\n tool_call_function_arguments = \"\"\n async for chunk in response:\n delta = chunk.choices[0].delta\n if delta.role is not None:\n role = delta.role\n\n if delta.content:\n response_content += delta.content\n llm_run_manager.on_llm_new_token(delta.content)\n\n if delta.tool_calls:\n # note: for simplicity we're only handling a single tool call here\n if delta.tool_calls[0].function.name is not None:\n tool_call_function_name = delta.tool_calls[0].function.name\n tool_call_id = delta.tool_calls[0].id\n\n # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n tool_call_chunk = ChatGenerationChunk(\n message=AIMessageChunk(\n content=\"\",\n additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n )\n )\n llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n tool_call_function_arguments += delta.tool_calls[0].function.arguments\n\n if tool_call_function_name is not None:\n tool_calls = [\n {\n \"id\": tool_call_id,\n \"function\": {\n \"name\": tool_call_function_name,\n \"arguments\": tool_call_function_arguments,\n },\n \"type\": \"function\",\n }\n ]\n else:\n tool_calls = None\n\n response_message = {\n \"role\": role,\n \"content\": response_content,\n \"tool_calls\": tool_calls,\n }\n return {\"messages\": [response_message]}"] }, { "cell_type": "markdown", @@ -187,62 +86,7 @@ "id": "b90941d8-afe4-42ec-9262-9c3b87c3b1ec", "metadata": {}, "outputs": [], - "source": [ - "import json\n", - "from langchain_core.callbacks import adispatch_custom_event\n", - "\n", - "\n", - "async def get_items(place: str) -> str:\n", - " \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n", - "\n", - " # this can be replaced with any actual streaming logic that you might have\n", - " def stream(place: str):\n", - " if \"bed\" in place: # For under the bed\n", - " yield from [\"socks\", \"shoes\", \"dust bunnies\"]\n", - " elif \"shelf\" in place: # For 'shelf'\n", - " yield from [\"books\", \"penciles\", \"pictures\"]\n", - " else: # if the agent decides to ask about a different place\n", - " yield \"cat snacks\"\n", - "\n", - " tokens = []\n", - " for token in stream(place):\n", - " await adispatch_custom_event(\n", - " # this will allow you to filter events by name\n", - " \"tool_call_token_stream\",\n", - " {\n", - " \"function_name\": \"get_items\",\n", - " \"arguments\": {\"place\": place},\n", - " \"tool_output_token\": token,\n", - " },\n", - " # this will allow you to filter events by tags\n", - " config={\"tags\": [\"tool_call\"]},\n", - " )\n", - " tokens.append(token)\n", - "\n", - " return \", \".join(tokens)\n", - "\n", - "\n", - "# define mapping to look up functions when running tools\n", - "function_name_to_function = {\"get_items\": get_items}\n", - "\n", - "\n", - "async def call_tools(state):\n", - " messages = state[\"messages\"]\n", - "\n", - " tool_call = messages[-1][\"tool_calls\"][0]\n", - " function_name = tool_call[\"function\"][\"name\"]\n", - " function_arguments = tool_call[\"function\"][\"arguments\"]\n", - " arguments = json.loads(function_arguments)\n", - "\n", - " function_response = await function_name_to_function[function_name](**arguments)\n", - " tool_message = {\n", - " \"tool_call_id\": tool_call[\"id\"],\n", - " \"role\": \"tool\",\n", - " \"name\": function_name,\n", - " \"content\": function_response,\n", - " }\n", - " return {\"messages\": [tool_message]}" - ] + "source": ["import json\nfrom langchain_core.callbacks import adispatch_custom_event\n\n\nasync def get_items(place: str) -> str:\n \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n\n # this can be replaced with any actual streaming logic that you might have\n def stream(place: str):\n if \"bed\" in place: # For under the bed\n yield from [\"socks\", \"shoes\", \"dust bunnies\"]\n elif \"shelf\" in place: # For 'shelf'\n yield from [\"books\", \"penciles\", \"pictures\"]\n else: # if the agent decides to ask about a different place\n yield \"cat snacks\"\n\n tokens = []\n for token in stream(place):\n await adispatch_custom_event(\n # this will allow you to filter events by name\n \"tool_call_token_stream\",\n {\n \"function_name\": \"get_items\",\n \"arguments\": {\"place\": place},\n \"tool_output_token\": token,\n },\n # this will allow you to filter events by tags\n config={\"tags\": [\"tool_call\"]},\n )\n tokens.append(token)\n\n return \", \".join(tokens)\n\n\n# define mapping to look up functions when running tools\nfunction_name_to_function = {\"get_items\": get_items}\n\n\nasync def call_tools(state):\n messages = state[\"messages\"]\n\n tool_call = messages[-1][\"tool_calls\"][0]\n function_name = tool_call[\"function\"][\"name\"]\n function_arguments = tool_call[\"function\"][\"arguments\"]\n arguments = json.loads(function_arguments)\n\n function_response = await function_name_to_function[function_name](**arguments)\n tool_message = {\n \"tool_call_id\": tool_call[\"id\"],\n \"role\": \"tool\",\n \"name\": function_name,\n \"content\": function_response,\n }\n return {\"messages\": [tool_message]}"] }, { "cell_type": "markdown", @@ -258,33 +102,7 @@ "id": "228260be-1f9a-4195-80e0-9604f8a5dba6", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, TypedDict, Literal\n", - "\n", - "from langgraph.graph import StateGraph, END\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, operator.add]\n", - "\n", - "\n", - "def should_continue(state) -> Literal[\"tools\", END]:\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " if last_message[\"tool_calls\"]:\n", - " return \"tools\"\n", - " return END\n", - "\n", - "\n", - "workflow = StateGraph(State)\n", - "workflow.set_entry_point(\"model\")\n", - "workflow.add_node(\"model\", call_model) # i.e. our \"agent\"\n", - "workflow.add_node(\"tools\", call_tools)\n", - "workflow.add_conditional_edges(\"model\", should_continue)\n", - "workflow.add_edge(\"tools\", \"model\")\n", - "graph = workflow.compile()" - ] + "source": ["import operator\nfrom typing import Annotated, TypedDict, Literal\n\nfrom langgraph.graph import StateGraph, END, START\n\n\nclass State(TypedDict):\n messages: Annotated[list, operator.add]\n\n\ndef should_continue(state) -> Literal[\"tools\", END]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n if last_message[\"tool_calls\"]:\n return \"tools\"\n return END\n\n\nworkflow = StateGraph(State)\nworkflow.add_edge(START, \"model\")\nworkflow.add_node(\"model\", call_model) # i.e. our \"agent\"\nworkflow.add_node(\"tools\", call_tools)\nworkflow.add_conditional_edges(\"model\", should_continue)\nworkflow.add_edge(\"tools\", \"model\")\ngraph = workflow.compile()"] }, { "cell_type": "markdown", @@ -318,14 +136,7 @@ ] } ], - "source": [ - "async for event in graph.astream_events(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n", - "):\n", - " tags = event.get(\"tags\", [])\n", - " if event[\"event\"] == \"on_custom_event\" and \"tool_call\" in tags:\n", - " print(\"Tool token\", event[\"data\"][\"tool_output_token\"])" - ] + "source": ["async for event in graph.astream_events(\n {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n):\n tags = event.get(\"tags\", [])\n if event[\"event\"] == \"on_custom_event\" and \"tool_call\" in tags:\n print(\"Tool token\", event[\"data\"][\"tool_output_token\"])"] } ], "metadata": { diff --git a/examples/streaming-tokens-without-langchain.ipynb b/examples/streaming-tokens-without-langchain.ipynb index d31f287f8..40ff751e0 100644 --- a/examples/streaming-tokens-without-langchain.ipynb +++ b/examples/streaming-tokens-without-langchain.ipynb @@ -30,10 +30,7 @@ "id": "47f79af8-58d8-4a48-8d9a-88823d88701f", "metadata": {}, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph openai" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph openai"] }, { "cell_type": "code", @@ -49,18 +46,7 @@ ] } ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] + "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")"] }, { "cell_type": "markdown", @@ -84,94 +70,7 @@ "id": "d59234f9-173e-469d-a725-c13e0979663e", "metadata": {}, "outputs": [], - "source": [ - "from openai import AsyncOpenAI\n", - "from langchain_core.language_models.chat_models import ChatGenerationChunk\n", - "from langchain_core.messages import AIMessageChunk\n", - "from langchain_core.runnables.config import (\n", - " ensure_config,\n", - " get_callback_manager_for_config,\n", - ")\n", - "\n", - "openai_client = AsyncOpenAI()\n", - "# define tool schema for openai tool calling\n", - "\n", - "tool = {\n", - " \"type\": \"function\",\n", - " \"function\": {\n", - " \"name\": \"get_items\",\n", - " \"description\": \"Use this tool to look up which items are in the given place.\",\n", - " \"parameters\": {\n", - " \"type\": \"object\",\n", - " \"properties\": {\"place\": {\"type\": \"string\"}},\n", - " \"required\": [\"place\"],\n", - " },\n", - " },\n", - "}\n", - "\n", - "\n", - "async def call_model(state, config=None):\n", - " config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n", - " callback_manager = get_callback_manager_for_config(config)\n", - " messages = state[\"messages\"]\n", - "\n", - " llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n", - " response = await openai_client.chat.completions.create(\n", - " messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n", - " )\n", - "\n", - " response_content = \"\"\n", - " role = None\n", - "\n", - " tool_call_id = None\n", - " tool_call_function_name = None\n", - " tool_call_function_arguments = \"\"\n", - " async for chunk in response:\n", - " delta = chunk.choices[0].delta\n", - " if delta.role is not None:\n", - " role = delta.role\n", - "\n", - " if delta.content:\n", - " response_content += delta.content\n", - " llm_run_manager.on_llm_new_token(delta.content)\n", - "\n", - " if delta.tool_calls:\n", - " # note: for simplicity we're only handling a single tool call here\n", - " if delta.tool_calls[0].function.name is not None:\n", - " tool_call_function_name = delta.tool_calls[0].function.name\n", - " tool_call_id = delta.tool_calls[0].id\n", - "\n", - " # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n", - " tool_call_chunk = ChatGenerationChunk(\n", - " message=AIMessageChunk(\n", - " content=\"\",\n", - " additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n", - " )\n", - " )\n", - " llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n", - " tool_call_function_arguments += delta.tool_calls[0].function.arguments\n", - "\n", - " if tool_call_function_name is not None:\n", - " tool_calls = [\n", - " {\n", - " \"id\": tool_call_id,\n", - " \"function\": {\n", - " \"name\": tool_call_function_name,\n", - " \"arguments\": tool_call_function_arguments,\n", - " },\n", - " \"type\": \"function\",\n", - " }\n", - " ]\n", - " else:\n", - " tool_calls = None\n", - "\n", - " response_message = {\n", - " \"role\": role,\n", - " \"content\": response_content,\n", - " \"tool_calls\": tool_calls,\n", - " }\n", - " return {\"messages\": [response_message]}" - ] + "source": ["from openai import AsyncOpenAI\nfrom langchain_core.language_models.chat_models import ChatGenerationChunk\nfrom langchain_core.messages import AIMessageChunk\nfrom langchain_core.runnables.config import (\n ensure_config,\n get_callback_manager_for_config,\n)\n\nopenai_client = AsyncOpenAI()\n# define tool schema for openai tool calling\n\ntool = {\n \"type\": \"function\",\n \"function\": {\n \"name\": \"get_items\",\n \"description\": \"Use this tool to look up which items are in the given place.\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\"place\": {\"type\": \"string\"}},\n \"required\": [\"place\"],\n },\n },\n}\n\n\nasync def call_model(state, config=None):\n config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n callback_manager = get_callback_manager_for_config(config)\n messages = state[\"messages\"]\n\n llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n response = await openai_client.chat.completions.create(\n messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n )\n\n response_content = \"\"\n role = None\n\n tool_call_id = None\n tool_call_function_name = None\n tool_call_function_arguments = \"\"\n async for chunk in response:\n delta = chunk.choices[0].delta\n if delta.role is not None:\n role = delta.role\n\n if delta.content:\n response_content += delta.content\n llm_run_manager.on_llm_new_token(delta.content)\n\n if delta.tool_calls:\n # note: for simplicity we're only handling a single tool call here\n if delta.tool_calls[0].function.name is not None:\n tool_call_function_name = delta.tool_calls[0].function.name\n tool_call_id = delta.tool_calls[0].id\n\n # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n tool_call_chunk = ChatGenerationChunk(\n message=AIMessageChunk(\n content=\"\",\n additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n )\n )\n llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n tool_call_function_arguments += delta.tool_calls[0].function.arguments\n\n if tool_call_function_name is not None:\n tool_calls = [\n {\n \"id\": tool_call_id,\n \"function\": {\n \"name\": tool_call_function_name,\n \"arguments\": tool_call_function_arguments,\n },\n \"type\": \"function\",\n }\n ]\n else:\n tool_calls = None\n\n response_message = {\n \"role\": role,\n \"content\": response_content,\n \"tool_calls\": tool_calls,\n }\n return {\"messages\": [response_message]}"] }, { "cell_type": "markdown", @@ -187,41 +86,7 @@ "id": "b756ea32", "metadata": {}, "outputs": [], - "source": [ - "import json\n", - "\n", - "\n", - "async def get_items(place: str) -> str:\n", - " \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n", - " if \"bed\" in place: # For under the bed\n", - " return \"socks, shoes and dust bunnies\"\n", - " if \"shelf\" in place: # For 'shelf'\n", - " return \"books, penciles and pictures\"\n", - " else: # if the agent decides to ask about a different place\n", - " return \"cat snacks\"\n", - "\n", - "\n", - "# define mapping to look up functions when running tools\n", - "function_name_to_function = {\"get_items\": get_items}\n", - "\n", - "\n", - "async def call_tools(state):\n", - " messages = state[\"messages\"]\n", - "\n", - " tool_call = messages[-1][\"tool_calls\"][0]\n", - " function_name = tool_call[\"function\"][\"name\"]\n", - " function_arguments = tool_call[\"function\"][\"arguments\"]\n", - " arguments = json.loads(function_arguments)\n", - "\n", - " function_response = await function_name_to_function[function_name](**arguments)\n", - " tool_message = {\n", - " \"tool_call_id\": tool_call[\"id\"],\n", - " \"role\": \"tool\",\n", - " \"name\": function_name,\n", - " \"content\": function_response,\n", - " }\n", - " return {\"messages\": [tool_message]}" - ] + "source": ["import json\n\n\nasync def get_items(place: str) -> str:\n \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n if \"bed\" in place: # For under the bed\n return \"socks, shoes and dust bunnies\"\n if \"shelf\" in place: # For 'shelf'\n return \"books, penciles and pictures\"\n else: # if the agent decides to ask about a different place\n return \"cat snacks\"\n\n\n# define mapping to look up functions when running tools\nfunction_name_to_function = {\"get_items\": get_items}\n\n\nasync def call_tools(state):\n messages = state[\"messages\"]\n\n tool_call = messages[-1][\"tool_calls\"][0]\n function_name = tool_call[\"function\"][\"name\"]\n function_arguments = tool_call[\"function\"][\"arguments\"]\n arguments = json.loads(function_arguments)\n\n function_response = await function_name_to_function[function_name](**arguments)\n tool_message = {\n \"tool_call_id\": tool_call[\"id\"],\n \"role\": \"tool\",\n \"name\": function_name,\n \"content\": function_response,\n }\n return {\"messages\": [tool_message]}"] }, { "cell_type": "markdown", @@ -237,33 +102,7 @@ "id": "228260be-1f9a-4195-80e0-9604f8a5dba6", "metadata": {}, "outputs": [], - "source": [ - "import operator\n", - "from typing import Annotated, TypedDict, Literal\n", - "\n", - "from langgraph.graph import StateGraph, END\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, operator.add]\n", - "\n", - "\n", - "def should_continue(state) -> Literal[\"tools\", END]:\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " if last_message[\"tool_calls\"]:\n", - " return \"tools\"\n", - " return END\n", - "\n", - "\n", - "workflow = StateGraph(State)\n", - "workflow.set_entry_point(\"model\")\n", - "workflow.add_node(\"model\", call_model) # i.e. our \"agent\"\n", - "workflow.add_node(\"tools\", call_tools)\n", - "workflow.add_conditional_edges(\"model\", should_continue)\n", - "workflow.add_edge(\"tools\", \"model\")\n", - "graph = workflow.compile()" - ] + "source": ["import operator\nfrom typing import Annotated, TypedDict, Literal\n\nfrom langgraph.graph import StateGraph, END, START\n\n\nclass State(TypedDict):\n messages: Annotated[list, operator.add]\n\n\ndef should_continue(state) -> Literal[\"tools\", END]:\n messages = state[\"messages\"]\n last_message = messages[-1]\n if last_message[\"tool_calls\"]:\n return \"tools\"\n return END\n\n\nworkflow = StateGraph(State)\nworkflow.add_edge(START, \"model\")\nworkflow.add_node(\"model\", call_model) # i.e. our \"agent\"\nworkflow.add_node(\"tools\", call_tools)\nworkflow.add_conditional_edges(\"model\", should_continue)\nworkflow.add_edge(\"tools\", \"model\")\ngraph = workflow.compile()"] }, { "cell_type": "markdown", @@ -328,14 +167,7 @@ ] } ], - "source": [ - "async for event in graph.astream_events(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n", - "):\n", - " tags = event.get(\"tags\", [])\n", - " if event[\"event\"] == \"on_chat_model_stream\" and \"agent_llm\" in tags:\n", - " print(\"LLM token\", event[\"data\"][\"chunk\"].dict())" - ] + "source": ["async for event in graph.astream_events(\n {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n):\n tags = event.get(\"tags\", [])\n if event[\"event\"] == \"on_chat_model_stream\" and \"agent_llm\" in tags:\n print(\"LLM token\", event[\"data\"][\"chunk\"].dict())"] }, { "cell_type": "code", @@ -343,7 +175,7 @@ "id": "adb0f7bc-6e51-478e-bd32-8f72df072d6c", "metadata": {}, "outputs": [], - "source": [] + "source": [""] } ], "metadata": { diff --git a/examples/tutorials/rag-agent-testing-local.ipynb b/examples/tutorials/rag-agent-testing-local.ipynb index f9e89e56d..7fd810943 100644 --- a/examples/tutorials/rag-agent-testing-local.ipynb +++ b/examples/tutorials/rag-agent-testing-local.ipynb @@ -169,9 +169,7 @@ "from langchain_core.output_parsers import JsonOutputParser\n", "\n", "# JSON\n", - "llm = ChatOllama(model=\"llama3.1\", \n", - " format=\"json\", \n", - " temperature=0)\n", + "llm = ChatOllama(model=\"llama3.1\", format=\"json\", temperature=0)\n", "\n", "\n", "prompt = PromptTemplate(\n", @@ -210,6 +208,7 @@ "from IPython.display import Image, display\n", "from langgraph.graph import START, END, StateGraph\n", "\n", + "\n", "class GraphState(TypedDict):\n", " \"\"\"\n", " Represents the state of our graph.\n", @@ -356,7 +355,7 @@ "workflow.add_node(\"web_search\", web_search) # web search\n", "\n", "# Build graph\n", - "workflow.set_entry_point(\"retrieve\")\n", + "workflow.add_edge(START, retrieve)\n", "workflow.add_edge(\"retrieve\", \"grade_documents\")\n", "workflow.add_conditional_edges(\n", " \"grade_documents\",\n", @@ -381,21 +380,22 @@ "metadata": {}, "outputs": [], "source": [ - "import uuid \n", + "import uuid\n", + "\n", "\n", "def predict_custom_agent_answer(example: dict):\n", - " \n", " config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n", - " \n", + "\n", " state_dict = custom_graph.invoke(\n", " {\"question\": example[\"input\"], \"steps\": []}, config\n", " )\n", - " \n", + "\n", " return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n", "\n", + "\n", "example = {\"input\": \"What are the types of agent memory?\"}\n", - "#response = predict_custom_agent_answer(example)\n", - "#response" + "# response = predict_custom_agent_answer(example)\n", + "# response" ] }, { @@ -544,6 +544,7 @@ " \"generate_answer\",\n", "]\n", "\n", + "\n", "def check_trajectory_custom(root_run: Run, example: Example) -> dict:\n", " \"\"\"\n", " Check if all expected tools are called in exact order and without any additional tool calls.\n", diff --git a/examples/tutorials/tool-calling-agent-local.ipynb b/examples/tutorials/tool-calling-agent-local.ipynb index 5c60336e8..12c43038f 100644 --- a/examples/tutorials/tool-calling-agent-local.ipynb +++ b/examples/tutorials/tool-calling-agent-local.ipynb @@ -134,6 +134,7 @@ " for d in web_results\n", " ]\n", "\n", + "\n", "# Tool list\n", "tools = [retrieve_documents, web_search]" ] @@ -152,9 +153,11 @@ "from langgraph.graph.message import AnyMessage, add_messages\n", "from typing_extensions import TypedDict\n", "\n", + "\n", "class State(TypedDict):\n", " messages: Annotated[list[AnyMessage], add_messages]\n", "\n", + "\n", "class Assistant:\n", " def __init__(self, runnable: Runnable):\n", " \"\"\"\n", @@ -291,6 +294,7 @@ "source": [ "import uuid\n", "\n", + "\n", "def predict_react_agent_answer(example: dict):\n", " \"\"\"Use this for answer evaluation\"\"\"\n", "\n", diff --git a/examples/web-navigation/web_voyager.ipynb b/examples/web-navigation/web_voyager.ipynb index 728003605..8959d2025 100644 --- a/examples/web-navigation/web_voyager.ipynb +++ b/examples/web-navigation/web_voyager.ipynb @@ -457,13 +457,13 @@ "source": [ "from langchain_core.runnables import RunnableLambda\n", "\n", - "from langgraph.graph import END, StateGraph\n", + "from langgraph.graph import END, START, StateGraph\n", "\n", "graph_builder = StateGraph(AgentState)\n", "\n", "\n", "graph_builder.add_node(\"agent\", agent)\n", - "graph_builder.set_entry_point(\"agent\")\n", + "graph_builder.add_edge(START, \"agent\")\n", "\n", "graph_builder.add_node(\"update_scratchpad\", update_scratchpad)\n", "graph_builder.add_edge(\"update_scratchpad\", \"agent\")\n", From 1bd40b2ebf1e18470a3f6eb699a26de895a9b60f Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Fri, 16 Aug 2024 15:45:56 -0700 Subject: [PATCH 025/287] Do markdown --- README.md | 4 ++-- docs/docs/cloud/deployment/graph_rebuild.md | 10 +++++----- docs/docs/cloud/deployment/setup.md | 4 ++-- docs/docs/cloud/deployment/setup_pyproject.md | 4 ++-- libs/langgraph/README.md | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 87d19961f..c3da88ec4 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ from langchain_core.messages import HumanMessage from langchain_anthropic import ChatAnthropic from langchain_core.tools import tool from langgraph.checkpoint.memory import MemorySaver -from langgraph.graph import END, StateGraph, MessagesState +from langgraph.graph import END, START, StateGraph, MessagesState from langgraph.prebuilt import ToolNode @@ -107,7 +107,7 @@ workflow.add_node("tools", tool_node) # Set the entrypoint as `agent` # This means that this node is the first one called -workflow.set_entry_point("agent") +workflow.add_edge(START, "agent") # We now add a conditional edge workflow.add_conditional_edges( diff --git a/docs/docs/cloud/deployment/graph_rebuild.md b/docs/docs/cloud/deployment/graph_rebuild.md index c7853b30a..b1034cd0f 100644 --- a/docs/docs/cloud/deployment/graph_rebuild.md +++ b/docs/docs/cloud/deployment/graph_rebuild.md @@ -28,7 +28,7 @@ In the standard LangGraph API configuration, the server uses the compiled graph ```python from langchain_openai import ChatOpenAI -from langgraph.graph import END, MessageGraph +from langgraph.graph import END, START, MessageGraph model = ChatOpenAI(temperature=0) @@ -36,7 +36,7 @@ graph_workflow = MessageGraph() graph_workflow.add_node("agent", model) graph_workflow.add_edge("agent", END) -graph_workflow.set_entry_point("agent") +graph_workflow.add_edge(START, "agent") agent = graph_workflow.compile() ``` @@ -60,7 +60,7 @@ To make your graph rebuild on each new run with custom configuration, you need t ```python from typing import Annotated, TypedDict from langchain_openai import ChatOpenAI -from langgraph.graph import END, MessageGraph +from langgraph.graph import END, START, MessageGraph from langgraph.graph.state import StateGraph from langgraph.graph.message import add_messages from langgraph.prebuilt import ToolNode @@ -83,7 +83,7 @@ def make_default_graph(): graph_workflow.add_node("agent", call_model) graph_workflow.add_edge("agent", END) - graph_workflow.set_entry_point("agent") + graph_workflow.add_edge(START, "agent") agent = graph_workflow.compile() return agent @@ -113,7 +113,7 @@ def make_alternative_graph(): graph_workflow.add_node("agent", call_model) graph_workflow.add_node("tools", tool_node) graph_workflow.add_edge("tools", "agent") - graph_workflow.set_entry_point("agent") + graph_workflow.add_edge(START, "agent") graph_workflow.add_conditional_edges("agent", should_continue) agent = graph_workflow.compile() diff --git a/docs/docs/cloud/deployment/setup.md b/docs/docs/cloud/deployment/setup.md index 170e19394..c37070572 100644 --- a/docs/docs/cloud/deployment/setup.md +++ b/docs/docs/cloud/deployment/setup.md @@ -99,7 +99,7 @@ Example `agent.py` file, which shows how to import from other modules you define # my_agent/agent.py from typing import TypedDict, Literal -from langgraph.graph import StateGraph, END +from langgraph.graph import StateGraph, END, START from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes from my_agent.utils.state import AgentState # import state @@ -110,7 +110,7 @@ class GraphConfig(TypedDict): workflow = StateGraph(AgentState, config_schema=GraphConfig) workflow.add_node("agent", call_model) workflow.add_node("action", tool_node) -workflow.set_entry_point("agent") +workflow.add_edge(START, "agent") workflow.add_conditional_edges( "agent", should_continue, diff --git a/docs/docs/cloud/deployment/setup_pyproject.md b/docs/docs/cloud/deployment/setup_pyproject.md index 767171f05..c53b2d2b8 100644 --- a/docs/docs/cloud/deployment/setup_pyproject.md +++ b/docs/docs/cloud/deployment/setup_pyproject.md @@ -109,7 +109,7 @@ Example `agent.py` file, which shows how to import from other modules you define # my_agent/agent.py from typing import TypedDict, Literal -from langgraph.graph import StateGraph, END +from langgraph.graph import StateGraph, END, START from my_agent.utils.nodes import call_model, should_continue, tool_node # import nodes from my_agent.utils.state import AgentState # import state @@ -120,7 +120,7 @@ class GraphConfig(TypedDict): workflow = StateGraph(AgentState, config_schema=GraphConfig) workflow.add_node("agent", call_model) workflow.add_node("action", tool_node) -workflow.set_entry_point("agent") +workflow.add_edge(START, "agent") workflow.add_conditional_edges( "agent", should_continue, diff --git a/libs/langgraph/README.md b/libs/langgraph/README.md index 87d19961f..c3da88ec4 100644 --- a/libs/langgraph/README.md +++ b/libs/langgraph/README.md @@ -59,7 +59,7 @@ from langchain_core.messages import HumanMessage from langchain_anthropic import ChatAnthropic from langchain_core.tools import tool from langgraph.checkpoint.memory import MemorySaver -from langgraph.graph import END, StateGraph, MessagesState +from langgraph.graph import END, START, StateGraph, MessagesState from langgraph.prebuilt import ToolNode @@ -107,7 +107,7 @@ workflow.add_node("tools", tool_node) # Set the entrypoint as `agent` # This means that this node is the first one called -workflow.set_entry_point("agent") +workflow.add_edge(START, "agent") # We now add a conditional edge workflow.add_conditional_edges( From 3d33b575d6af7610dd6f2713e18528baa96d8acb Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 20 Aug 2024 16:59:04 -0700 Subject: [PATCH 026/287] 0.2.5 --- 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 3aac6ed84..edac6bc67 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.4" +version = "0.2.5" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From ebf060675ddb5b2b9b94551065aeefbdd544495e Mon Sep 17 00:00:00 2001 From: David Duong Date: Wed, 21 Aug 2024 17:00:10 +0200 Subject: [PATCH 027/287] feat(js-sdk): add config and parent_config to thread state schema (#1411) * feat(js-sdk): add config and parent_config to thread state schema * Bump to 0.0.5 --- libs/sdk-js/package.json | 2 +- libs/sdk-js/src/schema.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json index 300578131..db509e9c2 100644 --- a/libs/sdk-js/package.json +++ b/libs/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-sdk", - "version": "0.0.4", + "version": "0.0.5", "description": "Client library for interacting with the LangGraph API", "type": "module", "packageManager": "yarn@1.22.19", diff --git a/libs/sdk-js/src/schema.ts b/libs/sdk-js/src/schema.ts index 897ed1ed8..9c2df19a9 100644 --- a/libs/sdk-js/src/schema.ts +++ b/libs/sdk-js/src/schema.ts @@ -101,6 +101,9 @@ export interface ThreadState { metadata: Metadata; created_at: Optional; parent_checkpoint_id: Optional; + + config: Config; + parent_config?: Config; } export interface Run { From f8b053a28682e3f27caf9ae752a3c5b2abeacb7d Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 09:19:15 -0700 Subject: [PATCH 028/287] Enable raising Interrupt from inside a node, add list of current interrupts to get_state (#1354) * Enable raising Interrupt from inside a node, add list of current interrupts to get_state * Lint * Allow multiple interrupt values in exception * Better typings * Fix some tests * Fix up * Lint * Add test * Lint * WIP stay=True * Fix tests for get_state * Remove ids * Fix step count * 999 * less fun * Undo * Update debug interface * Remove ability to pass multiple values * Undo --- libs/langgraph/langgraph/constants.py | 11 +- libs/langgraph/langgraph/errors.py | 14 +- libs/langgraph/langgraph/pregel/__init__.py | 138 +++++++++++--- libs/langgraph/langgraph/pregel/algo.py | 25 ++- libs/langgraph/langgraph/pregel/debug.py | 18 +- libs/langgraph/langgraph/pregel/loop.py | 23 ++- libs/langgraph/langgraph/pregel/types.py | 2 + .../tests/__snapshots__/test_pregel.ambr | 18 ++ libs/langgraph/tests/test_pregel.py | 176 +++++++++++++++--- libs/langgraph/tests/test_pregel_async.py | 167 +++++++++++++---- 10 files changed, 473 insertions(+), 119 deletions(-) diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index af4ba4d8d..f85b33ba3 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -1,10 +1,12 @@ -from typing import Any +from dataclasses import dataclass +from typing import Any, Literal INPUT = "__input__" CONFIG_KEY_SEND = "__pregel_send" CONFIG_KEY_READ = "__pregel_read" CONFIG_KEY_CHECKPOINTER = "__pregel_checkpointer" CONFIG_KEY_RESUMING = "__pregel_resuming" +CONFIG_KEY_TASK_ID = "__pregel_task_id" INTERRUPT = "__interrupt__" ERROR = "__error__" TASKS = "__pregel_tasks" @@ -16,6 +18,7 @@ RESERVED = { CONFIG_KEY_READ, CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_RESUMING, + CONFIG_KEY_TASK_ID, INPUT, } TAG_HIDDEN = "langsmith:hidden" @@ -93,3 +96,9 @@ class Send: and self.node == value.node and self.arg == value.arg ) + + +@dataclass +class Interrupt: + when: Literal["before", "during", "after"] + value: Any = None diff --git a/libs/langgraph/langgraph/errors.py b/libs/langgraph/langgraph/errors.py index ea54cc55f..00dc36e58 100644 --- a/libs/langgraph/langgraph/errors.py +++ b/libs/langgraph/langgraph/errors.py @@ -1,4 +1,7 @@ +from typing import Any + from langgraph.checkpoint.base import EmptyChannelError +from langgraph.constants import Interrupt class GraphRecursionError(RecursionError): @@ -29,7 +32,15 @@ class InvalidUpdateError(Exception): class GraphInterrupt(Exception): """Raised when a subgraph is interrupted.""" - pass + def __init__(self, interrupts: list[Interrupt]) -> None: + super().__init__(interrupts) + + +class NodeInterrupt(GraphInterrupt): + """Raised by a node to interrupt execution.""" + + def __init__(self, value: Any) -> None: + super().__init__([Interrupt("during", value)]) class EmptyInputError(Exception): @@ -42,6 +53,7 @@ __all__ = [ "GraphRecursionError", "InvalidUpdateError", "GraphInterrupt", + "NodeInterrupt", "EmptyInputError", "EmptyChannelError", ] diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 7b2fdb5f9..d2f0790cc 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -70,8 +70,9 @@ from langgraph.constants import ( CONFIG_KEY_SEND, ERROR, INTERRUPT, + Interrupt, ) -from langgraph.errors import GraphRecursionError, InvalidUpdateError +from langgraph.errors import GraphInterrupt, GraphRecursionError, InvalidUpdateError from langgraph.managed.base import ( AsyncManagedValuesManager, ManagedValuesManager, @@ -82,6 +83,7 @@ from langgraph.pregel.algo import ( apply_writes, local_read, prepare_next_tasks, + should_interrupt, ) from langgraph.pregel.debug import ( map_debug_task_results, @@ -381,6 +383,7 @@ class Pregel( saved.metadata.get("step", -1) + 1 if saved else -1, for_execution=False, ) + return StateSnapshot( read_channels(channels, self.stream_channels_asis), tuple(t.name for t in next_tasks), @@ -550,7 +553,7 @@ class Pregel( saved = self.checkpointer.get_tuple(config) checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() checkpoint_previous_versions = ( - saved.checkpoint["channel_versions"] if saved else {} + saved.checkpoint["channel_versions"].copy() if saved else {} ) step = saved.metadata.get("step", -1) if saved else -1 # merge configurable fields with previous checkpoint config @@ -576,7 +579,7 @@ class Pregel( create_checkpoint(checkpoint, None, step), { "source": "update", - "step": step, + "step": step + 1, "writes": {}, }, {}, @@ -606,7 +609,11 @@ class Pregel( if as_node not in self.nodes: raise InvalidUpdateError(f"Node {as_node} does not exist") # update channels - with ChannelsManager(self.channels, checkpoint, config) as channels: + with ChannelsManager( + self.channels, checkpoint, config + ) as channels, ManagedValuesManager( + self.managed_values_dict, ensure_config(config) + ) as managed: # create task to run all writers of the chosen node writers = self.nodes[as_node].get_writers() if not writers: @@ -640,19 +647,43 @@ class Pregel( apply_writes( checkpoint, channels, [task], self.checkpointer.get_next_version ) - - new_versions = get_new_channel_versions( - checkpoint_previous_versions, checkpoint["channel_versions"] - ) + checkpoint = create_checkpoint(checkpoint, channels, step + 1) + # check interrupt before + if tasks := should_interrupt( + checkpoint, + self.interrupt_before_nodes, + prepare_next_tasks( + checkpoint, + self.nodes, + channels, + managed, + config, + step + 2, + for_execution=False, + ), + ): + for t in tasks: + self.checkpointer.put_writes( + { + "configurable": { + **checkpoint_config["configurable"], + "checkpoint_id": checkpoint["id"], + } + }, + [(INTERRUPT, Interrupt("before"))], + t.id, + ) return self.checkpointer.put( checkpoint_config, - create_checkpoint(checkpoint, channels, step + 1), + checkpoint, { "source": "update", "step": step + 1, "writes": {as_node: values}, }, - new_versions, + get_new_channel_versions( + checkpoint_previous_versions, checkpoint["channel_versions"] + ), ) async def aupdate_state( @@ -668,7 +699,7 @@ class Pregel( saved = await self.checkpointer.aget_tuple(config) checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() checkpoint_previous_versions = ( - saved.checkpoint["channel_versions"] if saved else {} + saved.checkpoint["channel_versions"].copy() if saved else {} ) step = saved.metadata.get("step", -1) if saved else -1 # merge configurable fields with previous checkpoint config @@ -694,7 +725,7 @@ class Pregel( create_checkpoint(checkpoint, None, step), { "source": "update", - "step": step, + "step": step + 1, "writes": {}, }, {}, @@ -722,7 +753,11 @@ class Pregel( if as_node not in self.nodes: raise InvalidUpdateError(f"Node {as_node} does not exist") # update channels, acting as the chosen node - async with AsyncChannelsManager(self.channels, checkpoint, config) as channels: + async with AsyncChannelsManager( + self.channels, checkpoint, config + ) as channels, AsyncManagedValuesManager( + self.managed_values_dict, ensure_config(config) + ) as managed: # create task to run all writers of the chosen node writers = self.nodes[as_node].get_writers() if not writers: @@ -756,19 +791,47 @@ class Pregel( apply_writes( checkpoint, channels, [task], self.checkpointer.get_next_version ) - - new_versions = get_new_channel_versions( - checkpoint_previous_versions, checkpoint["channel_versions"] - ) + checkpoint = create_checkpoint(checkpoint, channels, step + 1) + # check interrupt before + if tasks := should_interrupt( + checkpoint, + self.interrupt_before_nodes, + prepare_next_tasks( + checkpoint, + self.nodes, + channels, + managed, + config, + step + 2, + for_execution=False, + ), + ): + await asyncio.gather( + *( + self.checkpointer.aput_writes( + { + "configurable": { + **checkpoint_config["configurable"], + "checkpoint_id": checkpoint["id"], + } + }, + [(INTERRUPT, Interrupt("before"))], + t.id, + ) + for t in tasks + ) + ) return await self.checkpointer.aput( checkpoint_config, - create_checkpoint(checkpoint, channels, step + 1), + checkpoint, { "source": "update", "step": step + 1, "writes": {as_node: values}, }, - new_versions, + get_new_channel_versions( + checkpoint_previous_versions, checkpoint["channel_versions"] + ), ) def _defaults( @@ -975,6 +1038,7 @@ class Pregel( for task in loop.tasks if not task.writes } + all_futures = futures.copy() end_time = ( self.step_timeout + time.monotonic() if self.step_timeout @@ -998,7 +1062,12 @@ class Pregel( task = futures.pop(fut) if exc := _exception(fut): # save error to checkpointer - loop.put_writes(task.id, [(ERROR, exc)]) + if isinstance(exc, GraphInterrupt): + loop.put_writes( + task.id, [(INTERRUPT, i) for i in exc.args[0]] + ) + else: + loop.put_writes(task.id, [(ERROR, exc)]) else: # save task writes to checkpointer loop.put_writes(task.id, task.writes) @@ -1026,7 +1095,7 @@ class Pregel( break # panic on failure or timeout - _panic_or_proceed(done, inflight, loop.step) + _panic_or_proceed(all_futures, loop.step) # don't keep futures around in memory longer than needed del done, inflight, futures # debug flag @@ -1223,6 +1292,7 @@ class Pregel( for task in loop.tasks if not task.writes } + all_futures = futures.copy() end_time = ( self.step_timeout + aioloop.time() if self.step_timeout @@ -1244,7 +1314,12 @@ class Pregel( task = futures.pop(fut) if exc := _exception(fut): # save error to checkpointer - loop.put_writes(task.id, [(ERROR, exc)]) + if isinstance(exc, GraphInterrupt): + loop.put_writes( + task.id, [(INTERRUPT, i) for i in exc.args[0]] + ) + else: + loop.put_writes(task.id, [(ERROR, exc)]) else: # save task writes to checkpointer loop.put_writes(task.id, task.writes) @@ -1274,7 +1349,7 @@ class Pregel( break # panic on failure or timeout - _panic_or_proceed(done, inflight, loop.step, asyncio.TimeoutError) + _panic_or_proceed(all_futures, loop.step, asyncio.TimeoutError) # don't keep futures around in memory longer than needed del done, inflight, futures # debug flag @@ -1421,9 +1496,8 @@ def _should_stop_others( for fut in done: if fut.cancelled(): return True - if fut.exception() is not None: - # TODO don't stop others if exception is interrupt - return True + if exc := fut.exception(): + return not isinstance(exc, GraphInterrupt) else: return False @@ -1441,14 +1515,20 @@ def _exception( def _panic_or_proceed( - done: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], - inflight: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], + futs: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], step: int, timeout_exc_cls: Type[Exception] = TimeoutError, ) -> None: + done: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set() + inflight: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set() + for fut in futs: + if fut.done(): + done.add(fut) + else: + inflight.add(fut) while done: # if any task failed - if exc := done.pop().exception(): + if exc := _exception(done.pop()): # cancel all pending tasks while inflight: inflight.pop().cancel() diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index fd725627a..5922fd14e 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -38,6 +38,7 @@ from langgraph.constants import ( CONFIG_KEY_READ, CONFIG_KEY_RESUMING, CONFIG_KEY_SEND, + CONFIG_KEY_TASK_ID, INTERRUPT, RESERVED, TAG_HIDDEN, @@ -68,26 +69,28 @@ def should_interrupt( checkpoint: Checkpoint, interrupt_nodes: Union[All, Sequence[str]], tasks: list[PregelExecutableTask], -) -> bool: +) -> list[PregelExecutableTask]: version_type = type(next(iter(checkpoint["channel_versions"].values()), None)) null_version = version_type() seen = checkpoint["versions_seen"].get(INTERRUPT, {}) + # interrupt if any channel has been updated since last interrupt + any_updates_since_prev_interrupt = any( + version > seen.get(chan, null_version) + for chan, version in checkpoint["channel_versions"].items() + ) + # and any triggered node is in interrupt_nodes list return ( - # interrupt if any channel has been updated since last interrupt - any( - version > seen.get(chan, null_version) - for chan, version in checkpoint["channel_versions"].items() - ) - # and any triggered node is in interrupt_nodes list - and any( - task.name + [ + task for task in tasks if ( (not task.config or TAG_HIDDEN not in task.config.get("tags")) if interrupt_nodes == "*" else task.name in interrupt_nodes ) - ) + ] + if any_updates_since_prev_interrupt + else [] ) @@ -308,6 +311,7 @@ def prepare_next_tasks( else None ), configurable={ + CONFIG_KEY_TASK_ID: task_id, # deque.extend is thread-safe CONFIG_KEY_SEND: partial( local_write, writes.extend, processes, channels @@ -398,6 +402,7 @@ def prepare_next_tasks( else None ), configurable={ + CONFIG_KEY_TASK_ID: task_id, # deque.extend is thread-safe CONFIG_KEY_SEND: partial( local_write, writes.extend, processes, channels diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index 931f3a180..5102cd8c5 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -10,7 +10,7 @@ from langchain_core.utils.input import get_bolded_text, get_colored_text from langgraph.channels.base import BaseChannel from langgraph.checkpoint.base import Checkpoint, CheckpointMetadata, PendingWrite -from langgraph.constants import ERROR, TAG_HIDDEN +from langgraph.constants import ERROR, INTERRUPT, TAG_HIDDEN from langgraph.pregel.io import read_channels from langgraph.pregel.types import PregelExecutableTask, PregelTask @@ -32,6 +32,7 @@ class CheckpointTask(TypedDict): id: str name: str error: Optional[str] + interrupts: list[dict] class CheckpointPayload(TypedDict): @@ -149,6 +150,7 @@ def map_debug_checkpoint( else { "id": t.id, "name": t.name, + "interrupts": t.interrupts, } for t in tasks_w_writes(tasks, pending_writes) ], @@ -190,8 +192,11 @@ def print_step_writes( def print_step_checkpoint( - step: int, channels: Mapping[str, BaseChannel], whitelist: Sequence[str] + metadata: CheckpointMetadata, + channels: Mapping[str, BaseChannel], + whitelist: Sequence[str], ) -> None: + step = metadata["step"] print( f"{get_colored_text(f'[{step}:checkpoint]', color='blue')} " + get_bolded_text(f"State at the end of step {step}:\n") @@ -203,6 +208,7 @@ def tasks_w_writes( tasks: list[PregelExecutableTask], pending_writes: Optional[list[PendingWrite]], ) -> tuple[PregelTask, ...]: + pending_writes = pending_writes or [] return tuple( PregelTask( task.id, @@ -210,12 +216,14 @@ def tasks_w_writes( next( ( exc - for tid, n, exc in pending_writes or [] - if tid == task.id - if n == ERROR + for tid, n, exc in pending_writes + if tid == task.id and n == ERROR ), None, ), + tuple( + v for tid, n, v in pending_writes if tid == task.id and n == INTERRUPT + ), ) for task in tasks ) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index f43aa25a7..f48f86fcd 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -45,6 +45,7 @@ from langgraph.constants import ( ERROR, INPUT, INTERRUPT, + Interrupt, ) from langgraph.errors import EmptyInputError, GraphInterrupt from langgraph.managed.base import ( @@ -206,10 +207,13 @@ class PregelLoop: } ) # after execution, check if we should interrupt - if should_interrupt(self.checkpoint, interrupt_after, self.tasks): + if tasks := should_interrupt(self.checkpoint, interrupt_after, self.tasks): self.status = "interrupt_after" + interrupts = [(t.id, Interrupt("after")) for t in tasks] + for tid, interrupt in interrupts: + self.put_writes(tid, [(INTERRUPT, interrupt)]) if self.is_nested: - raise GraphInterrupt(self) + raise GraphInterrupt([i[1] for i in interrupts]) else: return False else: @@ -258,7 +262,7 @@ class PregelLoop: # if there are pending writes from a previous loop, apply them if self.checkpoint_pending_writes: for tid, k, v in self.checkpoint_pending_writes: - if k == ERROR: # TODO same for INTERRUPT + if k in (ERROR, INTERRUPT): continue if task := next((t for t in self.tasks if t.id == tid), None): task.writes.append((k, v)) @@ -273,10 +277,13 @@ class PregelLoop: ) # before execution, check if we should interrupt - if should_interrupt(self.checkpoint, interrupt_before, self.tasks): + if tasks := should_interrupt(self.checkpoint, interrupt_before, self.tasks): self.status = "interrupt_before" + interrupts = [(t.id, Interrupt("before")) for t in tasks] + for tid, interrupt in interrupts: + self.put_writes(tid, [(INTERRUPT, interrupt)]) if self.is_nested: - raise GraphInterrupt() + raise GraphInterrupt([i[1] for i in interrupts]) else: return False @@ -394,7 +401,7 @@ class PregelLoop: exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> Optional[bool]: - if exc_type is GraphInterrupt and not self.is_nested: + if isinstance(exc_value, GraphInterrupt) and not self.is_nested: return True @@ -409,7 +416,6 @@ class SyncPregelLoop(PregelLoop, ContextManager): ) -> None: super().__init__(input, config=config, checkpointer=checkpointer, graph=graph) self.stack = ExitStack() - self.stack.push(self._suppress_interrupt) if checkpointer: self.checkpointer_get_next_version = checkpointer.get_next_version self.checkpointer_put_writes = checkpointer.put_writes @@ -457,6 +463,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): self.managed = self.stack.enter_context( ManagedValuesManager(self.graph.managed_values_dict, self.config) ) + self.stack.push(self._suppress_interrupt) self.status = "pending" self.step = self.checkpoint_metadata["step"] + 1 self.stop = self.step + self.config["recursion_limit"] + 1 @@ -486,7 +493,6 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): ) -> None: super().__init__(input, config=config, checkpointer=checkpointer, graph=graph) self.stack = AsyncExitStack() - self.stack.push(self._suppress_interrupt) if checkpointer: self.checkpointer_get_next_version = checkpointer.get_next_version self.checkpointer_put_writes = checkpointer.aput_writes @@ -536,6 +542,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): self.managed = await self.stack.enter_async_context( AsyncManagedValuesManager(self.graph.managed_values_dict, self.config) ) + self.stack.push(self._suppress_interrupt) self.status = "pending" self.step = self.checkpoint_metadata["step"] + 1 self.stop = self.step + self.config["recursion_limit"] + 1 diff --git a/libs/langgraph/langgraph/pregel/types.py b/libs/langgraph/langgraph/pregel/types.py index 8343329a4..a8c5a7beb 100644 --- a/libs/langgraph/langgraph/pregel/types.py +++ b/libs/langgraph/langgraph/pregel/types.py @@ -4,6 +4,7 @@ from typing import Any, Callable, Literal, NamedTuple, Optional, Type, Union from langchain_core.runnables import Runnable, RunnableConfig from langgraph.checkpoint.base import CheckpointMetadata +from langgraph.constants import Interrupt def default_retry_on(exc: Exception) -> bool: @@ -60,6 +61,7 @@ class PregelTask(NamedTuple): id: str name: str error: Optional[Exception] = None + interrupts: tuple[Interrupt, ...] = () class PregelExecutableTask(NamedTuple): diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index 644c99b95..c92bc4688 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -682,6 +682,24 @@ ''' # --- +# name: test_dynamic_interrupt + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- # name: test_in_one_fan_out_state_graph_waiting_edge ''' graph TD; diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 14a96b577..138d4dba5 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -52,8 +52,8 @@ from langgraph.checkpoint.memory import MemorySaver from langgraph.checkpoint.serde.base import SerializerProtocol from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer from langgraph.checkpoint.sqlite import SqliteSaver -from langgraph.constants import ERROR, Send -from langgraph.errors import InvalidUpdateError +from langgraph.constants import ERROR, Interrupt, Send +from langgraph.errors import InvalidUpdateError, NodeInterrupt from langgraph.graph import END, Graph from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages @@ -2195,7 +2195,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: ), }, }, - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -2239,7 +2239,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "input": "what is weather in sf", }, }, - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -2400,7 +2400,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: ), }, }, - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -2999,7 +2999,7 @@ def test_conditional_state_graph( ), "intermediate_steps": [], }, - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -3039,7 +3039,7 @@ def test_conditional_state_graph( ), "intermediate_steps": [], }, - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -3148,7 +3148,7 @@ def test_conditional_state_graph( values={ "intermediate_steps": [], }, - tasks=(PregelTask(AnyStr(), "agent"),), + tasks=(PregelTask(AnyStr(), "agent", interrupts=(Interrupt("before"),)),), next=("agent",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -3173,7 +3173,7 @@ def test_conditional_state_graph( ), "intermediate_steps": [], }, - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -3226,7 +3226,7 @@ def test_conditional_state_graph( ) ], }, - tasks=(PregelTask(AnyStr(), "agent"),), + tasks=(PregelTask(AnyStr(), "agent", interrupts=(Interrupt("before"),)),), next=("agent",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -4828,7 +4828,13 @@ def test_message_graph( id="ai1", ), ], - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=( + PregelTask( + AnyStr(), + "tools", + interrupts=(Interrupt("before"),), + ), + ), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -4873,7 +4879,7 @@ def test_message_graph( ], ), ], - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -4955,7 +4961,7 @@ def test_message_graph( id="ai2", ), ], - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -5547,7 +5553,13 @@ def test_root_graph( id="ai1", ), ], - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=( + PregelTask( + AnyStr(), + "tools", + interrupts=(Interrupt("before"),), + ), + ), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -5592,7 +5604,7 @@ def test_root_graph( ], ), ], - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -5674,7 +5686,7 @@ def test_root_graph( id="ai2", ), ], - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -6120,6 +6132,72 @@ def test_in_one_fan_out_out_one_graph_state() -> None: ] +def test_dynamic_interrupt(snapshot: SnapshotAssertion) -> None: + class State(TypedDict): + my_key: Annotated[str, operator.add] + market: str + + def tool_two_node(s: State) -> State: + if s["market"] == "DE": + raise NodeInterrupt("Just because...") + return {"my_key": " all good"} + + tool_two_graph = StateGraph(State) + tool_two_graph.add_node("tool_two", tool_two_node) + tool_two_graph.add_edge(START, "tool_two") + tool_two = tool_two_graph.compile() + + assert tool_two.invoke({"my_key": "value", "market": "DE"}) == { + "my_key": "value", + "market": "DE", + } + assert tool_two.invoke({"my_key": "value", "market": "US"}) == { + "my_key": "value all good", + "market": "US", + } + + with SqliteSaver.from_conn_string(":memory:") as saver: + tool_two = tool_two_graph.compile(checkpointer=saver) + + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + tool_two.invoke({"my_key": "value", "market": "DE"}) + + thread1 = {"configurable": {"thread_id": "1"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == { + "my_key": "value ⛰️", + "market": "DE", + } + assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ + { + "source": "loop", + "step": 0, + "writes": None, + }, + { + "source": "input", + "step": -1, + "writes": {"my_key": "value ⛰️", "market": "DE"}, + }, + ] + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value ⛰️", "market": "DE"}, + next=("tool_two",), + tasks=( + PregelTask( + AnyStr(), + "tool_two", + interrupts=(Interrupt("during", "Just because..."),), + ), + ), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={"source": "loop", "step": 0, "writes": None}, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + + def test_start_branch_then(snapshot: SnapshotAssertion) -> None: class State(TypedDict): my_key: Annotated[str, operator.add] @@ -6172,7 +6250,13 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: ] assert tool_two.get_state(thread1) == StateSnapshot( values={"my_key": "value ⛰️", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_slow", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_slow",), config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], @@ -6206,7 +6290,13 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: } assert tool_two.get_state(thread2) == StateSnapshot( values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_fast", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_fast",), config=tool_two.checkpointer.get_tuple(thread2).config, created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], @@ -6240,7 +6330,13 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: } assert tool_two.get_state(thread3) == StateSnapshot( values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_fast", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_fast",), config=tool_two.checkpointer.get_tuple(thread3).config, created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], @@ -6251,7 +6347,13 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: tool_two.update_state(thread3, {"my_key": "key"}) # appends to my_key assert tool_two.get_state(thread3) == StateSnapshot( values={"my_key": "valuekey", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_fast", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_fast",), config=tool_two.checkpointer.get_tuple(thread3).config, created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], @@ -6344,7 +6446,7 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "writes": {"my_key": "value", "market": "DE"}, }, "next": ["__start__"], - "tasks": [{"id": AnyStr(), "name": "__start__"}], + "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], }, }, { @@ -6373,7 +6475,7 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "writes": None, }, "next": ["prepare"], - "tasks": [{"id": AnyStr(), "name": "prepare"}], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], }, }, { @@ -6423,7 +6525,9 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "writes": {"prepare": {"my_key": " prepared"}}, }, "next": ["tool_two_slow"], - "tasks": [{"id": AnyStr(), "name": "tool_two_slow"}], + "tasks": [ + {"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()} + ], }, }, { @@ -6473,7 +6577,7 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "writes": {"tool_two_slow": {"my_key": " slow"}}, }, "next": ["finish"], - "tasks": [{"id": AnyStr(), "name": "finish"}], + "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], }, }, { @@ -6544,7 +6648,13 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: } assert tool_two.get_state(thread1) == StateSnapshot( values={"my_key": "value prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_slow", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_slow",), config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], @@ -6582,7 +6692,13 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: } assert tool_two.get_state(thread2) == StateSnapshot( values={"my_key": "value prepared", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_fast", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_fast",), config=tool_two.checkpointer.get_tuple(thread2).config, created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], @@ -6629,7 +6745,7 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "my_key": "value prepared slow", "market": "DE", }, - tasks=(PregelTask(AnyStr(), "finish"),), + tasks=(PregelTask(AnyStr(), "finish", interrupts=(Interrupt("before"),)),), next=("finish",), config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], @@ -6648,7 +6764,7 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "my_key": "value prepared slower", "market": "DE", }, - tasks=(PregelTask(AnyStr(), "finish"),), + tasks=(PregelTask(AnyStr(), "finish", interrupts=(Interrupt("before"),)),), next=("finish",), config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], @@ -6905,7 +7021,7 @@ def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> "query": "analyzed: query: what is weather in sf", "docs": ["doc1", "doc2", "doc3", "doc4", "doc5"], }, - tasks=(PregelTask(AnyStr(), "qa"),), + tasks=(PregelTask(AnyStr(), "qa", interrupts=(Interrupt("before"),)),), next=("qa",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -7856,7 +7972,7 @@ def test_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) == { "my_key": "hi my value", } assert list(app.get_state_history(config)) == [ diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index b50ecd36a..7c5742aa0 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -47,8 +47,8 @@ from langgraph.checkpoint.base import ( ) from langgraph.checkpoint.memory import MemorySaver from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver -from langgraph.constants import ERROR, Send -from langgraph.errors import InvalidUpdateError +from langgraph.constants import ERROR, Interrupt, Send +from langgraph.errors import InvalidUpdateError, NodeInterrupt from langgraph.graph import END, Graph, StateGraph from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages @@ -204,6 +204,61 @@ async def test_node_cancellation_on_other_node_exception() -> None: assert inner_task_cancelled +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], +) +async def test_node_not_cancelled_on_other_node_interrupted( + checkpointer_name: str, request: pytest.FixtureRequest +) -> None: + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") + + class State(TypedDict): + hello: str + + awhiles = 0 + inner_task_cancelled = False + + async def awhile(input: State) -> None: + nonlocal awhiles + + awhiles += 1 + try: + await asyncio.sleep(1) + return {"hello": "again"} + except asyncio.CancelledError: + nonlocal inner_task_cancelled + inner_task_cancelled = True + raise + + async def iambad(input: State) -> None: + if input["hello"] != "bye": + raise NodeInterrupt("I am bad") + + builder = StateGraph(State) + builder.add_node("agent", awhile) + builder.add_node("bad", iambad) + builder.set_conditional_entry_point(lambda _: ["agent", "bad"], then=END) + + graph = builder.compile(checkpointer=checkpointer) + thread = {"configurable": {"thread_id": "1"}} + + assert await graph.ainvoke({"hello": "world"}, thread) == {"hello": "world"} + + assert not inner_task_cancelled + assert awhiles == 1 + + assert await graph.ainvoke(None, thread, debug=True) is None + + assert not inner_task_cancelled + assert awhiles == 1 + + assert await graph.ainvoke({"hello": "bye"}, thread) == {"hello": "again"} + + assert not inner_task_cancelled + assert awhiles == 2 + + async def test_step_timeout_on_stream_hang() -> None: inner_task_cancelled = False @@ -2366,7 +2421,13 @@ async def test_conditional_graph() -> None: ), }, }, - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=( + PregelTask( + AnyStr(), + "tools", + interrupts=(Interrupt("before"),), + ), + ), next=("tools",), config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ @@ -2414,7 +2475,13 @@ async def test_conditional_graph() -> None: "input": "what is weather in sf", }, }, - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=( + PregelTask( + AnyStr(), + "tools", + interrupts=(Interrupt("before"),), + ), + ), next=("tools",), config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ @@ -2586,7 +2653,13 @@ async def test_conditional_graph() -> None: ), }, }, - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=( + PregelTask( + AnyStr(), + "tools", + interrupts=(Interrupt("before"),), + ), + ), next=("tools",), config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ @@ -3152,7 +3225,13 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: ), "intermediate_steps": [], }, - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=( + PregelTask( + AnyStr(), + "tools", + interrupts=(Interrupt("before"),), + ), + ), next=("tools",), config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ @@ -3196,7 +3275,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: ), "intermediate_steps": [], }, - tasks=(PregelTask(AnyStr(), "tools"),), + tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), next=("tools",), config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ @@ -4746,7 +4825,13 @@ async def test_start_branch_then() -> None: ] assert await tool_two.aget_state(thread1) == StateSnapshot( values={"my_key": "value", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_slow", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_slow",), config=(await tool_two.checkpointer.aget_tuple(thread1)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ @@ -4788,7 +4873,13 @@ async def test_start_branch_then() -> None: } assert await tool_two.aget_state(thread2) == StateSnapshot( values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_fast", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_fast",), config=(await tool_two.checkpointer.aget_tuple(thread2)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ @@ -4830,7 +4921,13 @@ async def test_start_branch_then() -> None: } assert await tool_two.aget_state(thread3) == StateSnapshot( values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_fast", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_fast",), config=(await tool_two.checkpointer.aget_tuple(thread3)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ @@ -4845,7 +4942,13 @@ async def test_start_branch_then() -> None: await tool_two.aupdate_state(thread3, {"my_key": "key"}) # appends to my_key assert await tool_two.aget_state(thread3) == StateSnapshot( values={"my_key": "valuekey", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_fast", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_fast",), config=(await tool_two.checkpointer.aget_tuple(thread3)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ @@ -4945,12 +5048,7 @@ async def test_branch_then() -> None: "writes": {"my_key": "value", "market": "DE"}, }, "next": ["__start__"], - "tasks": [ - { - "id": AnyStr(), - "name": "__start__", - } - ], + "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], }, }, { @@ -4979,12 +5077,7 @@ async def test_branch_then() -> None: "writes": None, }, "next": ["prepare"], - "tasks": [ - { - "id": AnyStr(), - "name": "prepare", - } - ], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], }, }, { @@ -5035,10 +5128,7 @@ async def test_branch_then() -> None: }, "next": ["tool_two_slow"], "tasks": [ - { - "id": AnyStr(), - "name": "tool_two_slow", - } + {"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()} ], }, }, @@ -5089,12 +5179,7 @@ async def test_branch_then() -> None: "writes": {"tool_two_slow": {"my_key": " slow"}}, }, "next": ["finish"], - "tasks": [ - { - "id": AnyStr(), - "name": "finish", - } - ], + "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], }, }, { @@ -5165,7 +5250,13 @@ async def test_branch_then() -> None: } assert await tool_two.aget_state(thread1) == StateSnapshot( values={"my_key": "value prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_slow", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_slow",), config=(await tool_two.checkpointer.aget_tuple(thread1)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ @@ -5211,7 +5302,13 @@ async def test_branch_then() -> None: } assert await tool_two.aget_state(thread2) == StateSnapshot( values={"my_key": "value prepared", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + tasks=( + PregelTask( + AnyStr(), + "tool_two_fast", + interrupts=(Interrupt("before"),), + ), + ), next=("tool_two_fast",), config=(await tool_two.checkpointer.aget_tuple(thread2)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ From d1b7a787ee82277c8376edb1abb805609771fef9 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 09:22:27 -0700 Subject: [PATCH 029/287] lib0.2.6 --- 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 edac6bc67..4dff54572 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.5" +version = "0.2.6" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From 1cc02825ea2cc47fc53ccc83761d6f94db2cf329 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 14 Aug 2024 15:53:40 -0700 Subject: [PATCH 030/287] Add ScopedValue - state shared between threads --- libs/langgraph/langgraph/channels/base.py | 5 + libs/langgraph/langgraph/constants.py | 2 + libs/langgraph/langgraph/graph/state.py | 30 ++++- libs/langgraph/langgraph/kv/__init__.py | 0 libs/langgraph/langgraph/kv/base.py | 113 ++++++++++++++++++ libs/langgraph/langgraph/kv/memory.py | 33 +++++ libs/langgraph/langgraph/managed/base.py | 43 ++++++- .../langgraph/managed/scoped_value.py | 99 +++++++++++++++ libs/langgraph/langgraph/pregel/__init__.py | 8 +- libs/langgraph/langgraph/pregel/algo.py | 11 +- libs/langgraph/langgraph/pregel/loop.py | 34 +++++- libs/langgraph/tests/test_kv.py | 32 +++++ libs/langgraph/tests/test_pregel.py | 41 ++++++- 13 files changed, 424 insertions(+), 27 deletions(-) create mode 100644 libs/langgraph/langgraph/kv/__init__.py create mode 100644 libs/langgraph/langgraph/kv/base.py create mode 100644 libs/langgraph/langgraph/kv/memory.py create mode 100644 libs/langgraph/langgraph/managed/scoped_value.py create mode 100644 libs/langgraph/tests/test_kv.py diff --git a/libs/langgraph/langgraph/channels/base.py b/libs/langgraph/langgraph/channels/base.py index fe47f0d8f..9c7794c46 100644 --- a/libs/langgraph/langgraph/channels/base.py +++ b/libs/langgraph/langgraph/channels/base.py @@ -33,6 +33,11 @@ class BaseChannel(Generic[Value, Update, C], ABC): # serialize/deserialize methods + def tap(self) -> Optional[C]: + """Return the current checkpoint of the channel, without consuming it. + By default, it just calls checkpoint().""" + return self.checkpoint() + @abstractmethod def checkpoint(self) -> Optional[C]: """Return a serializable representation of the channel's current state. diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index f85b33ba3..cf87be337 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -5,6 +5,7 @@ INPUT = "__input__" CONFIG_KEY_SEND = "__pregel_send" CONFIG_KEY_READ = "__pregel_read" CONFIG_KEY_CHECKPOINTER = "__pregel_checkpointer" +CONFIG_KEY_KV = "__pregel_kv" CONFIG_KEY_RESUMING = "__pregel_resuming" CONFIG_KEY_TASK_ID = "__pregel_task_id" INTERRUPT = "__interrupt__" @@ -17,6 +18,7 @@ RESERVED = { CONFIG_KEY_SEND, CONFIG_KEY_READ, CONFIG_KEY_CHECKPOINTER, + CONFIG_KEY_KV, CONFIG_KEY_RESUMING, CONFIG_KEY_TASK_ID, INPUT, diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 5810aaf29..cda357607 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -40,7 +40,14 @@ from langgraph.graph.graph import ( Graph, Send, ) -from langgraph.managed.base import ManagedValue, is_managed_value +from langgraph.kv.base import BaseKV +from langgraph.managed.base import ( + ChannelKeyPlaceholder, + ConfiguredManagedValue, + ManagedValue, + is_managed_value, + is_writable_managed_value, +) from langgraph.pregel.read import ChannelRead, PregelNode from langgraph.pregel.types import All, RetryPolicy from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry @@ -373,6 +380,8 @@ class StateGraph(Graph): def compile( self, + *, + kv: Optional[BaseKV] = None, checkpointer: Optional[BaseCheckpointSaver] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, @@ -442,6 +451,7 @@ class StateGraph(Graph): interrupt_after_nodes=interrupt_after, auto_validate=False, debug=debug, + kv=kv, ) compiled.attach_node(START, None) @@ -511,7 +521,11 @@ class CompiledStateGraph(CompiledGraph): if not isinstance(v, Context) and not is_managed_value(v) ] else: - output_keys = list(self.builder.channels) + output_keys = list(self.builder.channels) + [ + k + for k, v in self.builder.managed.items() + if is_writable_managed_value(v) + ] def _get_state_key( input: Union[None, dict, Any], config: RunnableConfig, *, key: str @@ -684,7 +698,7 @@ def _get_channels( return {"__root__": _get_channel(schema, allow_managed=False)}, {} all_keys = { - name: _get_channel(typ) + name: _get_channel(name, typ) for name, typ in get_type_hints(schema, include_extras=True).items() if name != "__slots__" } @@ -695,9 +709,9 @@ def _get_channels( def _get_channel( - annotation: Any, *, allow_managed: bool = True + name: str, annotation: Any, *, allow_managed: bool = True ) -> Union[BaseChannel, Type[ManagedValue]]: - if manager := _is_field_managed_value(annotation): + if manager := _is_field_managed_value(name, annotation): if allow_managed: return manager else: @@ -736,12 +750,16 @@ def _is_field_binop(typ: Type[Any]) -> Optional[BinaryOperatorAggregate]: return None -def _is_field_managed_value(typ: Type[Any]) -> Optional[Type[ManagedValue]]: +def _is_field_managed_value(name: str, typ: Type[Any]) -> Optional[Type[ManagedValue]]: if hasattr(typ, "__metadata__"): meta = typ.__metadata__ if len(meta) >= 1: decoration = get_origin(meta[-1]) or meta[-1] if is_managed_value(decoration): + if isinstance(decoration, ConfiguredManagedValue): + for k, v in decoration.kwargs.items(): + if v is ChannelKeyPlaceholder: + decoration.kwargs[k] = name return decoration return None diff --git a/libs/langgraph/langgraph/kv/__init__.py b/libs/langgraph/langgraph/kv/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/libs/langgraph/langgraph/kv/base.py b/libs/langgraph/langgraph/kv/base.py new file mode 100644 index 000000000..570783ec1 --- /dev/null +++ b/libs/langgraph/langgraph/kv/base.py @@ -0,0 +1,113 @@ +import asyncio +from typing import Any, List, NamedTuple, Optional, Union + +V = dict[str, Any] + + +class BaseKV: + def get(self, pairs: List[tuple[str, str]]) -> dict[tuple[str, str], Optional[V]]: + # list[(namespace, key)] -> dict[(namespace, key), value | none] + raise NotImplementedError + + def list(self, prefixes: List[str]) -> dict[str, dict[str, V]]: + # list[namespace] -> dict[namespace, list[value]] + raise NotImplementedError + + def put(self, writes: List[tuple[str, str, Optional[V]]]) -> None: + # list[(namespace, key, value | none)] -> None + raise NotImplementedError + + async def aget( + self, pairs: List[tuple[str, str]] + ) -> dict[tuple[str, str], Optional[V]]: + # list[(namespace, key)] -> dict[(namespace, key), value | none] + raise NotImplementedError + + async def alist(self, prefixes: List[str]) -> dict[str, dict[str, V]]: + # list[namespace] -> dict[namespace, list[value]] + raise NotImplementedError + + async def aput(self, writes: List[tuple[str, str, Optional[V]]]) -> None: + # list[(namespace, key, value | none)] -> None + raise NotImplementedError + + +class GetOp(NamedTuple): + pairs: List[tuple[str, str]] + + +class ListOp(NamedTuple): + prefixes: List[str] + + +class PutOp(NamedTuple): + writes: List[tuple[str, str, Optional[V]]] + + +class KeyValueStore(BaseKV): + def __init__(self, kv: BaseKV) -> None: + self.kv = kv + self.aqueue: dict[asyncio.Future, Union[GetOp, ListOp, PutOp]] = {} + self.task = asyncio.create_task(_run(self.aqueue, self.kv)) + + def __del__(self) -> None: + self.task.cancel() + + async def aget( + self, pairs: List[tuple[str, str]] + ) -> dict[tuple[str, str], Optional[V]]: + fut = asyncio.get_running_loop().create_future() + self.aqueue[fut] = GetOp(pairs) + return await fut + + async def alist(self, prefixes: List[str]) -> dict[str, dict[str, V]]: + fut = asyncio.get_running_loop().create_future() + self.aqueue[fut] = ListOp(prefixes) + return await fut + + async def aput(self, writes: List[tuple[str, str, Optional[V]]]) -> None: + fut = asyncio.get_running_loop().create_future() + self.aqueue[fut] = PutOp(writes) + return await fut + + +async def _run( + aqueue: dict[asyncio.Future, Union[GetOp, ListOp, PutOp]], kv: BaseKV +) -> None: + while True: + await asyncio.sleep(0) + if not aqueue: + continue + # this could use a lock, if we want thread safety + taken = aqueue.copy() + aqueue.clear() + # action each operation + gets = {f: o for f, o in taken.items() if isinstance(o, GetOp)} + if gets: + try: + results = await kv.aget([p for op in gets.values() for p in op.pairs]) + for fut, op in gets.items(): + fut.set_result({k: results.get(k) for k in op.pairs}) + except Exception as e: + for fut in gets: + fut.set_exception(e) + lists = {f: o for f, o in taken.items() if isinstance(o, ListOp)} + if lists: + try: + results = await kv.alist( + [p for op in lists.values() for p in op.prefixes] + ) + for fut, op in lists.items(): + fut.set_result({k: results.get(k) for k in op.prefixes}) + except Exception as e: + for fut in lists: + fut.set_exception(e) + puts = {f: o for f, o in taken.items() if isinstance(o, PutOp)} + if puts: + try: + await kv.aput([w for op in puts.values() for w in op.writes]) + for fut in puts: + fut.set_result(None) + except Exception as e: + for fut in puts: + fut.set_exception(e) diff --git a/libs/langgraph/langgraph/kv/memory.py b/libs/langgraph/langgraph/kv/memory.py new file mode 100644 index 000000000..387ea1e7a --- /dev/null +++ b/libs/langgraph/langgraph/kv/memory.py @@ -0,0 +1,33 @@ +from collections import defaultdict +from typing import List + +from langgraph.kv.base import BaseKV, V + + +class MemoryKV(BaseKV): + def __init__(self) -> None: + self.data: dict[str, dict[str, V]] = defaultdict(dict) + + def get(self, pairs: List[tuple[str, str]]) -> dict[tuple[str, str], V | None]: + return {pair: self.data[pair[0]].get(pair[1]) for pair in pairs} + + async def aget( + self, pairs: List[tuple[str, str]] + ) -> dict[tuple[str, str], V | None]: + return self.get(pairs) + + def list(self, prefixes: List[str]) -> dict[str, dict[str, V]]: + return {prefix: self.data[prefix] for prefix in prefixes} + + async def alist(self, prefixes: List[str]) -> dict[str, dict[str, V]]: + return self.list(prefixes) + + def put(self, writes: List[tuple[str, str, V | None]]) -> None: + for namespace, key, value in writes: + if value is None: + self.data[namespace].pop(key, None) + else: + self.data[namespace][key] = value + + async def aput(self, writes: List[tuple[str, str, V | None]]) -> None: + self.put(writes) diff --git a/libs/langgraph/langgraph/managed/base.py b/libs/langgraph/langgraph/managed/base.py index 0455ed58b..5820383f8 100644 --- a/libs/langgraph/langgraph/managed/base.py +++ b/libs/langgraph/langgraph/managed/base.py @@ -5,9 +5,12 @@ from inspect import isclass from typing import ( Any, AsyncGenerator, + AsyncIterator, Generator, Generic, + Iterator, NamedTuple, + Sequence, Type, TypeVar, Union, @@ -17,6 +20,7 @@ from langchain_core.runnables import RunnableConfig from typing_extensions import Self, TypeGuard V = TypeVar("V") +U = TypeVar("U") class ManagedValue(ABC, Generic[V]): @@ -25,9 +29,7 @@ class ManagedValue(ABC, Generic[V]): @classmethod @contextmanager - def enter( - cls, config: RunnableConfig, **kwargs: Any - ) -> Generator[Self, None, None]: + def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]: try: value = cls(config, **kwargs) yield value @@ -41,9 +43,7 @@ class ManagedValue(ABC, Generic[V]): @classmethod @asynccontextmanager - async def aenter( - cls, config: RunnableConfig, **kwargs: Any - ) -> AsyncGenerator[Self, None]: + async def aenter(cls, config: RunnableConfig, **kwargs: Any) -> AsyncIterator[Self]: try: value = cls(config, **kwargs) yield value @@ -60,6 +60,16 @@ class ManagedValue(ABC, Generic[V]): ... +class WritableManagedValue(Generic[V, U], ManagedValue[V], ABC): + @abstractmethod + def update(self, writes: Sequence[U]) -> None: + ... + + @abstractmethod + async def aupdate(self, writes: Sequence[U]) -> None: + ... + + class ConfiguredManagedValue(NamedTuple): cls: Type[ManagedValue] kwargs: dict[str, Any] @@ -76,6 +86,24 @@ def is_managed_value(value: Any) -> TypeGuard[ManagedValueSpec]: ) +def is_readonly_managed_value(value: Any) -> TypeGuard[Type[ManagedValue]]: + return ( + isclass(value) + and issubclass(value, ManagedValue) + and not issubclass(value, WritableManagedValue) + ) or ( + isinstance(value, ConfiguredManagedValue) + and not issubclass(value.cls, WritableManagedValue) + ) + + +def is_writable_managed_value(value: Any) -> TypeGuard[Type[WritableManagedValue]]: + return (isclass(value) and issubclass(value, WritableManagedValue)) or ( + isinstance(value, ConfiguredManagedValue) + and issubclass(value.cls, WritableManagedValue) + ) + + @contextmanager def ManagedValuesManager( values: dict[str, ManagedValueSpec], @@ -119,3 +147,6 @@ async def AsyncManagedValuesManager( yield {tasks[task]: task.result() for task in done} else: yield {} + + +ChannelKeyPlaceholder = object() diff --git a/libs/langgraph/langgraph/managed/scoped_value.py b/libs/langgraph/langgraph/managed/scoped_value.py new file mode 100644 index 000000000..adb16e1b4 --- /dev/null +++ b/libs/langgraph/langgraph/managed/scoped_value.py @@ -0,0 +1,99 @@ +from contextlib import asynccontextmanager, contextmanager +from typing import ( + Any, + AsyncIterator, + Iterator, + Optional, + Self, + Sequence, +) + +from langchain_core.runnables import RunnableConfig + +from langgraph.constants import CONFIG_KEY_KV +from langgraph.errors import InvalidUpdateError +from langgraph.kv.base import BaseKV +from langgraph.managed.base import ( + ChannelKeyPlaceholder, + ConfiguredManagedValue, + WritableManagedValue, +) +from langgraph.pregel.types import PregelTaskDescription + +V = dict[str, Any] + + +Value = dict[str, V] +Update = dict[str, Optional[V]] + + +class ScopedValue(WritableManagedValue[Value, Update]): + @staticmethod + def configure(scope: str) -> ConfiguredManagedValue: + return ConfiguredManagedValue( + ScopedValue, {"scope": scope, "key": ChannelKeyPlaceholder} + ) + + @classmethod + @contextmanager + def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]: + with super().enter(config, **kwargs) as value: + if value.kv is not None: + saved = value.kv.list([value.ns]) + value.value = saved[value.ns] + yield value + + @classmethod + @asynccontextmanager + async def aenter(cls, config: RunnableConfig, **kwargs: Any) -> AsyncIterator[Self]: + async with super().aenter(config, **kwargs) as value: + if value.kv is not None: + saved = await value.kv.alist([value.ns]) + value.value = saved[value.ns] + yield value + + def __init__(self, config: RunnableConfig, *, scope: str, key: str) -> None: + self.scope = scope + self.config = config + self.value: Value = {} + self.kv: BaseKV = config["configurable"].get(CONFIG_KEY_KV) + if self.kv is None: + self.ns: Optional[str] = None + elif scope_value := config["configurable"].get(self.scope): + self.ns = f"scoped:{scope}:{key}:{scope_value}" + else: + raise ValueError( + f"Scope {scope} for shared state key not in config.configurable" + ) + + def __call__(self, step: int, task: PregelTaskDescription) -> Value: + return self.value.copy() + + def _process_update( + self, values: Sequence[Update] + ) -> list[tuple[str, str, Optional[dict[str, Any]]]]: + writes = [] + for vv in values: + for k, v in vv.items(): + if v is None: + if k in self.value: + self.value[k] = None + writes.append((self.ns, k, None)) + elif not isinstance(v, dict): + raise InvalidUpdateError("Received a non-dict value") + else: + self.value[k] = v + writes.append((self.ns, k, v)) + return writes + + def update(self, values: Sequence[Update]) -> None: + if self.kv is None: + self._process_update(values) + else: + return self.kv.put(self._process_update(values)) + + async def aupdate(self, writes: Sequence[Update]) -> None: + if self.kv is None: + self._process_update(writes) + else: + return await self.kv.aput(self._process_update(writes)) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index d2f0790cc..833453197 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -73,6 +73,7 @@ from langgraph.constants import ( Interrupt, ) from langgraph.errors import GraphInterrupt, GraphRecursionError, InvalidUpdateError +from langgraph.kv.base import BaseKV from langgraph.managed.base import ( AsyncManagedValuesManager, ManagedValuesManager, @@ -223,6 +224,9 @@ class Pregel( checkpointer: Optional[BaseCheckpointSaver] = None """Checkpointer used to save and load graph state. Defaults to None.""" + kv: Optional[BaseKV] = None + """Key-value store to use. Defaults to None.""" + retry_policy: Optional[RetryPolicy] = None """Retry policy to use when running tasks. Set to None to disable.""" @@ -644,7 +648,7 @@ class Pregel( ), ) # apply to checkpoint and save - apply_writes( + assert not apply_writes( checkpoint, channels, [task], self.checkpointer.get_next_version ) checkpoint = create_checkpoint(checkpoint, channels, step + 1) @@ -788,7 +792,7 @@ class Pregel( ), ) # apply to checkpoint and save - apply_writes( + assert not apply_writes( checkpoint, channels, [task], self.checkpointer.get_next_version ) checkpoint = create_checkpoint(checkpoint, channels, step + 1) diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 5922fd14e..994af018e 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -145,7 +145,7 @@ def apply_writes( channels: Mapping[str, BaseChannel], tasks: Sequence[WritesProtocol], get_next_version: Optional[Callable[[int, BaseChannel], int]], -) -> None: +) -> dict[str, list[Any]]: # update seen versions for task in tasks: checkpoint["versions_seen"].setdefault(task.name, {}).update( @@ -161,6 +161,7 @@ def apply_writes( max_version = max(checkpoint["channel_versions"].values()) else: max_version = None + # Consume all channels that were read for chan in { chan for task in tasks for chan in task.triggers if chan not in RESERVED @@ -177,12 +178,15 @@ def apply_writes( # Group writes by channel pending_writes_by_channel: dict[str, list[Any]] = defaultdict(list) + pending_writes_by_managed: dict[str, list[Any]] = defaultdict(list) for task in tasks: for chan, val in task.writes: if chan == TASKS: checkpoint["pending_sends"].append(val) - else: + elif chan in channels: pending_writes_by_channel[chan].append(val) + else: + pending_writes_by_managed[chan].append(val) # Find the highest version of all channels if checkpoint["channel_versions"]: @@ -214,6 +218,9 @@ def apply_writes( max_version, channels[chan] ) + # Return managed values writes to be applied externally + return pending_writes_by_managed + @overload def prepare_next_tasks( diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index f48f86fcd..aaa43be67 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -18,10 +18,11 @@ from typing import ( Type, TypeVar, Union, + cast, ) from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager -from langchain_core.runnables import RunnableConfig +from langchain_core.runnables import RunnableConfig, patch_config from typing_extensions import Self from langgraph.channels.base import BaseChannel @@ -40,6 +41,7 @@ from langgraph.checkpoint.base import ( empty_checkpoint, ) from langgraph.constants import ( + CONFIG_KEY_KV, CONFIG_KEY_READ, CONFIG_KEY_RESUMING, ERROR, @@ -52,6 +54,7 @@ from langgraph.managed.base import ( AsyncManagedValuesManager, ManagedValueMapping, ManagedValuesManager, + WritableManagedValue, ) from langgraph.pregel.algo import ( PregelTaskWrites, @@ -182,12 +185,15 @@ class PregelLoop: elif all(task.writes for task in self.tasks): writes = [w for t in self.tasks for w in t.writes] # all tasks have finished - apply_writes( + mv_writes = apply_writes( self.checkpoint, self.channels, self.tasks, self.checkpointer_get_next_version, ) + # apply writes to managed values + for key, values in mv_writes.items(): + self._update_mv(key, values) # produce values output self.stream.extend( ("values", v) @@ -324,12 +330,13 @@ class PregelLoop: manager=None, ) # apply input writes - apply_writes( + mv_writes = apply_writes( self.checkpoint, self.channels, discard_tasks + [PregelTaskWrites(INPUT, input_writes, [])], self.checkpointer_get_next_version, ) + assert not mv_writes # save input checkpoint self._put_checkpoint({"source": "input", "writes": self.input}) else: @@ -395,6 +402,9 @@ class PregelLoop: # increment step self.step += 1 + def _update_mv(self, key: str, values: Sequence[Any]) -> None: + raise NotImplementedError + def _suppress_interrupt( self, exc_type: Optional[Type[BaseException]], @@ -438,6 +448,9 @@ class SyncPregelLoop(PregelLoop, ContextManager): finally: self.checkpointer.put(config, checkpoint, metadata, new_versions) + def _update_mv(self, key: str, values: Sequence[Any]) -> None: + return self.submit(cast(WritableManagedValue, self.managed[key]).update, values) + # context manager def __enter__(self) -> Self: @@ -461,7 +474,10 @@ class SyncPregelLoop(PregelLoop, ContextManager): ChannelsManager(self.graph.channels, self.checkpoint, self.config) ) self.managed = self.stack.enter_context( - ManagedValuesManager(self.graph.managed_values_dict, self.config) + ManagedValuesManager( + self.graph.managed_values_dict, + patch_config(self.config, configurable={CONFIG_KEY_KV: self.graph.kv}), + ) ) self.stack.push(self._suppress_interrupt) self.status = "pending" @@ -515,6 +531,11 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): finally: await self.checkpointer.aput(config, checkpoint, metadata, new_versions) + def _update_mv(self, key: str, values: Sequence[Any]) -> None: + return self.submit( + cast(WritableManagedValue, self.managed[key]).aupdate, values + ) + # context manager async def __aenter__(self) -> Self: @@ -540,7 +561,10 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): AsyncChannelsManager(self.graph.channels, self.checkpoint, self.config) ) self.managed = await self.stack.enter_async_context( - AsyncManagedValuesManager(self.graph.managed_values_dict, self.config) + AsyncManagedValuesManager( + self.graph.managed_values_dict, + patch_config(self.config, configurable={CONFIG_KEY_KV: self.graph.kv}), + ) ) self.stack.push(self._suppress_interrupt) self.status = "pending" diff --git a/libs/langgraph/tests/test_kv.py b/libs/langgraph/tests/test_kv.py new file mode 100644 index 000000000..2e24ee07d --- /dev/null +++ b/libs/langgraph/tests/test_kv.py @@ -0,0 +1,32 @@ +import asyncio +from typing import Any, List + +from pytest_mock import MockerFixture + +from langgraph.kv.base import BaseKV, KeyValueStore + + +async def test_kv_queue(mocker: MockerFixture) -> None: + aget = mocker.stub() + + class MockKV(BaseKV): + async def aget( + self, pairs: List[tuple[str, str]] + ) -> dict[tuple[str, str], dict[str, Any] | None]: + aget(pairs) + return {pair: {0: pair[0], 1: pair[1]} for pair in pairs} + + store = KeyValueStore(MockKV()) + + # concurrent calls are batched + results = await asyncio.gather( + store.aget([("a", "b")]), + store.aget([("c", "d")]), + ) + assert results == [ + {("a", "b"): {0: "a", 1: "b"}}, + {("c", "d"): {0: "c", 1: "d"}}, + ] + assert [c.args for c in aget.call_args_list] == [ + ([("a", "b"), ("c", "d")],), + ] diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 138d4dba5..3cc1c4d23 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -58,6 +58,8 @@ from langgraph.graph import END, Graph from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages from langgraph.graph.state import StateGraph +from langgraph.kv.memory import MemoryKV +from langgraph.managed.scoped_value import ScopedValue from langgraph.prebuilt.chat_agent_executor import ( create_tool_calling_executor, ) @@ -165,6 +167,7 @@ def test_graph_validation() -> None: class State(TypedDict): hello: str + shared_things: Annotated[dict[str, dict[str, Any]], ScopedValue("assistant_id")] def node_a(state: State) -> State: # typo @@ -6202,10 +6205,34 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: class State(TypedDict): my_key: Annotated[str, operator.add] market: str + shared: Annotated[ + dict[str, dict[str, Any]], ScopedValue.configure("assistant_id") + ] + + def assert_shared_value(data: State, config: RunnableConfig) -> State: + assert "shared" in data + if thread_id := config["configurable"].get("thread_id"): + if thread_id == "1": + # this is the first thread, so should not see a value + assert data["shared"] == {} + return {"shared": {"1": {"hello": "world"}}} + elif thread_id == "2": + # this should get value saved by thread 1 + assert data["shared"] == {"1": {"hello": "world"}} + elif thread_id == "3": + # this is a different assistant, so should not see previous value + assert data["shared"] == {} + return {} + + def tool_two_slow(data: State, config: RunnableConfig) -> State: + return {"my_key": " slow", **assert_shared_value(data, config)} + + def tool_two_fast(data: State, config: RunnableConfig) -> State: + return {"my_key": " fast", **assert_shared_value(data, config)} tool_two_graph = StateGraph(State) - tool_two_graph.add_node("tool_two_slow", lambda s: {"my_key": " slow"}) - tool_two_graph.add_node("tool_two_fast", lambda s: {"my_key": " fast"}) + tool_two_graph.add_node("tool_two_slow", tool_two_slow) + tool_two_graph.add_node("tool_two_fast", tool_two_fast) tool_two_graph.set_conditional_entry_point( lambda s: "tool_two_slow" if s["market"] == "DE" else "tool_two_fast", then=END ) @@ -6223,14 +6250,16 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: with SqliteSaver.from_conn_string(":memory:") as saver: tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"] + kv=MemoryKV(), + checkpointer=saver, + interrupt_before=["tool_two_fast", "tool_two_slow"], ) # missing thread_id with pytest.raises(ValueError, match="thread_id"): tool_two.invoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1"}} + thread1 = {"configurable": {"thread_id": "1", "assistant_id": "a"}} # stop when about to enter node assert tool_two.invoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == { "my_key": "value ⛰️", @@ -6282,7 +6311,7 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, ) - thread2 = {"configurable": {"thread_id": "2"}} + thread2 = {"configurable": {"thread_id": "2", "assistant_id": "a"}} # stop when about to enter node assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { "my_key": "value", @@ -6322,7 +6351,7 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, ) - thread3 = {"configurable": {"thread_id": "3"}} + thread3 = {"configurable": {"thread_id": "3", "assistant_id": "b"}} # stop when about to enter node assert tool_two.invoke({"my_key": "value", "market": "US"}, thread3) == { "my_key": "value", From 1dc09dc45fd53ed9c7a795f344f14fd23c9c13d2 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 16 Aug 2024 09:58:49 -0700 Subject: [PATCH 031/287] Remove warning on write to managed channel --- libs/langgraph/langgraph/pregel/algo.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 994af018e..a52d15f4d 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -122,6 +122,7 @@ def local_write( processes: Mapping[str, PregelNode], channels: Mapping[str, BaseChannel], writes: Sequence[tuple[str, Any]], + managed: ManagedValueMapping, ) -> None: for chan, value in writes: if chan == TASKS: @@ -131,7 +132,7 @@ def local_write( ) if value.node not in processes: raise InvalidUpdateError(f"Invalid node name {value.node} in packet") - elif chan not in channels: + elif chan not in channels and chan not in managed: logger.warning(f"Skipping write for channel '{chan}' which has no readers") commit(writes) @@ -321,7 +322,11 @@ def prepare_next_tasks( CONFIG_KEY_TASK_ID: task_id, # deque.extend is thread-safe CONFIG_KEY_SEND: partial( - local_write, writes.extend, processes, channels + local_write, + writes.extend, + processes, + channels, + managed, ), CONFIG_KEY_READ: partial( local_read, @@ -412,7 +417,11 @@ def prepare_next_tasks( CONFIG_KEY_TASK_ID: task_id, # deque.extend is thread-safe CONFIG_KEY_SEND: partial( - local_write, writes.extend, processes, channels + local_write, + writes.extend, + processes, + channels, + managed, ), CONFIG_KEY_READ: partial( local_read, From 2bc0e2df4299259fc7e3187c3c288a0c5145dfc1 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 16 Aug 2024 15:53:05 -0700 Subject: [PATCH 032/287] Fix --- libs/langgraph/langgraph/channels/base.py | 5 ----- libs/langgraph/langgraph/managed/scoped_value.py | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/libs/langgraph/langgraph/channels/base.py b/libs/langgraph/langgraph/channels/base.py index 9c7794c46..fe47f0d8f 100644 --- a/libs/langgraph/langgraph/channels/base.py +++ b/libs/langgraph/langgraph/channels/base.py @@ -33,11 +33,6 @@ class BaseChannel(Generic[Value, Update, C], ABC): # serialize/deserialize methods - def tap(self) -> Optional[C]: - """Return the current checkpoint of the channel, without consuming it. - By default, it just calls checkpoint().""" - return self.checkpoint() - @abstractmethod def checkpoint(self) -> Optional[C]: """Return a serializable representation of the channel's current state. diff --git a/libs/langgraph/langgraph/managed/scoped_value.py b/libs/langgraph/langgraph/managed/scoped_value.py index adb16e1b4..997bc9469 100644 --- a/libs/langgraph/langgraph/managed/scoped_value.py +++ b/libs/langgraph/langgraph/managed/scoped_value.py @@ -18,7 +18,6 @@ from langgraph.managed.base import ( ConfiguredManagedValue, WritableManagedValue, ) -from langgraph.pregel.types import PregelTaskDescription V = dict[str, Any] @@ -66,7 +65,7 @@ class ScopedValue(WritableManagedValue[Value, Update]): f"Scope {scope} for shared state key not in config.configurable" ) - def __call__(self, step: int, task: PregelTaskDescription) -> Value: + def __call__(self, step: int) -> Value: return self.value.copy() def _process_update( From 7ca37afc74095c86ad0e86f36ce948d0ed7e33b7 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 16 Aug 2024 16:03:51 -0700 Subject: [PATCH 033/287] Fix --- libs/langgraph/langgraph/graph/state.py | 2 +- libs/langgraph/langgraph/managed/scoped_value.py | 6 +++--- libs/langgraph/langgraph/pregel/algo.py | 2 +- libs/langgraph/tests/test_pregel.py | 10 +++++----- libs/langgraph/tests/test_pregel_async.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index cda357607..1c9242ea1 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -695,7 +695,7 @@ def _get_channels( schema: Type[dict], ) -> tuple[dict[str, BaseChannel], dict[str, Type[ManagedValue]]]: if not hasattr(schema, "__annotations__"): - return {"__root__": _get_channel(schema, allow_managed=False)}, {} + return {"__root__": _get_channel("__root__", schema, allow_managed=False)}, {} all_keys = { name: _get_channel(name, typ) diff --git a/libs/langgraph/langgraph/managed/scoped_value.py b/libs/langgraph/langgraph/managed/scoped_value.py index 997bc9469..38a666163 100644 --- a/libs/langgraph/langgraph/managed/scoped_value.py +++ b/libs/langgraph/langgraph/managed/scoped_value.py @@ -26,11 +26,11 @@ Value = dict[str, V] Update = dict[str, Optional[V]] -class ScopedValue(WritableManagedValue[Value, Update]): +class SharedValue(WritableManagedValue[Value, Update]): @staticmethod - def configure(scope: str) -> ConfiguredManagedValue: + def on(scope: str) -> ConfiguredManagedValue: return ConfiguredManagedValue( - ScopedValue, {"scope": scope, "key": ChannelKeyPlaceholder} + SharedValue, {"scope": scope, "key": ChannelKeyPlaceholder} ) @classmethod diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index a52d15f4d..37f168bf7 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -121,8 +121,8 @@ def local_write( commit: Callable[[Sequence[tuple[str, Any]]], None], processes: Mapping[str, PregelNode], channels: Mapping[str, BaseChannel], - writes: Sequence[tuple[str, Any]], managed: ManagedValueMapping, + writes: Sequence[tuple[str, Any]], ) -> None: for chan, value in writes: if chan == TASKS: diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 3cc1c4d23..4bcce7ccd 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -59,7 +59,7 @@ from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages from langgraph.graph.state import StateGraph from langgraph.kv.memory import MemoryKV -from langgraph.managed.scoped_value import ScopedValue +from langgraph.managed.scoped_value import SharedValue from langgraph.prebuilt.chat_agent_executor import ( create_tool_calling_executor, ) @@ -167,7 +167,9 @@ def test_graph_validation() -> None: class State(TypedDict): hello: str - shared_things: Annotated[dict[str, dict[str, Any]], ScopedValue("assistant_id")] + shared_things: Annotated[ + dict[str, dict[str, Any]], SharedValue.on("assistant_id") + ] def node_a(state: State) -> State: # typo @@ -6205,9 +6207,7 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: class State(TypedDict): my_key: Annotated[str, operator.add] market: str - shared: Annotated[ - dict[str, dict[str, Any]], ScopedValue.configure("assistant_id") - ] + shared: Annotated[dict[str, dict[str, Any]], SharedValue.on("assistant_id")] def assert_shared_value(data: State, config: RunnableConfig) -> State: assert "shared" in data diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 7c5742aa0..3048ddbb4 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -563,7 +563,7 @@ async def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} assert app.output_schema.schema() == {"title": "LangGraphOutput", "type": "integer"} - assert await app.ainvoke(2) == 3 + assert await app.ainvoke(2, debug=True) == 3 assert await app.ainvoke(2, output_keys=["output"]) == {"output": 3} assert await gapp.ainvoke(2) == 3 From 77d7deb033820834dc431b735cd8b7c09b07aa98 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 16 Aug 2024 16:23:57 -0700 Subject: [PATCH 034/287] Rename --- .../langgraph/managed/{scoped_value.py => shared_value.py} | 2 +- libs/langgraph/tests/test_pregel.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename libs/langgraph/langgraph/managed/{scoped_value.py => shared_value.py} (98%) diff --git a/libs/langgraph/langgraph/managed/scoped_value.py b/libs/langgraph/langgraph/managed/shared_value.py similarity index 98% rename from libs/langgraph/langgraph/managed/scoped_value.py rename to libs/langgraph/langgraph/managed/shared_value.py index 38a666163..6853306f3 100644 --- a/libs/langgraph/langgraph/managed/scoped_value.py +++ b/libs/langgraph/langgraph/managed/shared_value.py @@ -4,11 +4,11 @@ from typing import ( AsyncIterator, Iterator, Optional, - Self, Sequence, ) from langchain_core.runnables import RunnableConfig +from typing_extensions import Self from langgraph.constants import CONFIG_KEY_KV from langgraph.errors import InvalidUpdateError diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 4bcce7ccd..fef936c80 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -59,7 +59,7 @@ from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages from langgraph.graph.state import StateGraph from langgraph.kv.memory import MemoryKV -from langgraph.managed.scoped_value import SharedValue +from langgraph.managed.shared_value import SharedValue from langgraph.prebuilt.chat_agent_executor import ( create_tool_calling_executor, ) From 9b90a24d9460ca70ee15a26541c7af706d82a886 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 16 Aug 2024 16:34:02 -0700 Subject: [PATCH 035/287] Lint --- libs/langgraph/langgraph/managed/base.py | 1 + .../langgraph/managed/shared_value.py | 26 +++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/langgraph/managed/base.py b/libs/langgraph/langgraph/managed/base.py index 5820383f8..8dbf5c30c 100644 --- a/libs/langgraph/langgraph/managed/base.py +++ b/libs/langgraph/langgraph/managed/base.py @@ -150,3 +150,4 @@ async def AsyncManagedValuesManager( ChannelKeyPlaceholder = object() +ChannelTypePlaceholder = object() diff --git a/libs/langgraph/langgraph/managed/shared_value.py b/libs/langgraph/langgraph/managed/shared_value.py index 6853306f3..f1b8d9516 100644 --- a/libs/langgraph/langgraph/managed/shared_value.py +++ b/libs/langgraph/langgraph/managed/shared_value.py @@ -1,3 +1,4 @@ +import collections.abc from contextlib import asynccontextmanager, contextmanager from typing import ( Any, @@ -5,10 +6,11 @@ from typing import ( Iterator, Optional, Sequence, + Type, ) from langchain_core.runnables import RunnableConfig -from typing_extensions import Self +from typing_extensions import NotRequired, Required, Self from langgraph.constants import CONFIG_KEY_KV from langgraph.errors import InvalidUpdateError @@ -26,6 +28,17 @@ Value = dict[str, V] Update = dict[str, Optional[V]] +# Adapted from typing_extensions +def _strip_extras(t): + """Strips Annotated, Required and NotRequired from a given type.""" + if hasattr(t, "__origin__"): + return _strip_extras(t.__origin__) + if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired): + return _strip_extras(t.__args__[0]) + + return t + + class SharedValue(WritableManagedValue[Value, Update]): @staticmethod def on(scope: str) -> ConfiguredManagedValue: @@ -51,7 +64,16 @@ class SharedValue(WritableManagedValue[Value, Update]): value.value = saved[value.ns] yield value - def __init__(self, config: RunnableConfig, *, scope: str, key: str) -> None: + def __init__( + self, config: RunnableConfig, *, typ: Type[Any], scope: str, key: str + ) -> None: + if typ := _strip_extras(typ): + if typ not in ( + dict, + collections.abc.Mapping, + collections.abc.MutableMapping, + ): + raise ValueError("SharedValue must be a dict") self.scope = scope self.config = config self.value: Value = {} From 656f89e16ab7074c972d1920d774bf0852346996 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 16 Aug 2024 16:38:46 -0700 Subject: [PATCH 036/287] Lint --- libs/langgraph/langgraph/graph/state.py | 3 +++ libs/langgraph/langgraph/managed/shared_value.py | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 1c9242ea1..261fbf1d5 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -43,6 +43,7 @@ from langgraph.graph.graph import ( from langgraph.kv.base import BaseKV from langgraph.managed.base import ( ChannelKeyPlaceholder, + ChannelTypePlaceholder, ConfiguredManagedValue, ManagedValue, is_managed_value, @@ -760,6 +761,8 @@ def _is_field_managed_value(name: str, typ: Type[Any]) -> Optional[Type[ManagedV for k, v in decoration.kwargs.items(): if v is ChannelKeyPlaceholder: decoration.kwargs[k] = name + if v is ChannelTypePlaceholder: + decoration.kwargs[k] = typ.__origin__ return decoration return None diff --git a/libs/langgraph/langgraph/managed/shared_value.py b/libs/langgraph/langgraph/managed/shared_value.py index f1b8d9516..7c55fc858 100644 --- a/libs/langgraph/langgraph/managed/shared_value.py +++ b/libs/langgraph/langgraph/managed/shared_value.py @@ -17,6 +17,7 @@ from langgraph.errors import InvalidUpdateError from langgraph.kv.base import BaseKV from langgraph.managed.base import ( ChannelKeyPlaceholder, + ChannelTypePlaceholder, ConfiguredManagedValue, WritableManagedValue, ) @@ -43,7 +44,12 @@ class SharedValue(WritableManagedValue[Value, Update]): @staticmethod def on(scope: str) -> ConfiguredManagedValue: return ConfiguredManagedValue( - SharedValue, {"scope": scope, "key": ChannelKeyPlaceholder} + SharedValue, + { + "scope": scope, + "key": ChannelKeyPlaceholder, + "typ": ChannelTypePlaceholder, + }, ) @classmethod @@ -68,6 +74,7 @@ class SharedValue(WritableManagedValue[Value, Update]): self, config: RunnableConfig, *, typ: Type[Any], scope: str, key: str ) -> None: if typ := _strip_extras(typ): + print(typ) if typ not in ( dict, collections.abc.Mapping, From 8f8f3849fcfd55eefcdaf316ac0d0554e244d9bf Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 20 Aug 2024 13:30:37 -0700 Subject: [PATCH 037/287] Lint --- libs/langgraph/langgraph/kv/memory.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/langgraph/langgraph/kv/memory.py b/libs/langgraph/langgraph/kv/memory.py index 387ea1e7a..265dfe77a 100644 --- a/libs/langgraph/langgraph/kv/memory.py +++ b/libs/langgraph/langgraph/kv/memory.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import List +from typing import List, Optional from langgraph.kv.base import BaseKV, V @@ -8,12 +8,12 @@ class MemoryKV(BaseKV): def __init__(self) -> None: self.data: dict[str, dict[str, V]] = defaultdict(dict) - def get(self, pairs: List[tuple[str, str]]) -> dict[tuple[str, str], V | None]: + def get(self, pairs: List[tuple[str, str]]) -> dict[tuple[str, str], Optional[V]]: return {pair: self.data[pair[0]].get(pair[1]) for pair in pairs} async def aget( self, pairs: List[tuple[str, str]] - ) -> dict[tuple[str, str], V | None]: + ) -> dict[tuple[str, str], Optional[V]]: return self.get(pairs) def list(self, prefixes: List[str]) -> dict[str, dict[str, V]]: @@ -22,12 +22,12 @@ class MemoryKV(BaseKV): async def alist(self, prefixes: List[str]) -> dict[str, dict[str, V]]: return self.list(prefixes) - def put(self, writes: List[tuple[str, str, V | None]]) -> None: + def put(self, writes: List[tuple[str, str, Optional[V]]]) -> None: for namespace, key, value in writes: if value is None: self.data[namespace].pop(key, None) else: self.data[namespace][key] = value - async def aput(self, writes: List[tuple[str, str, V | None]]) -> None: - self.put(writes) + async def aput(self, writes: List[tuple[str, str, Optional[V]]]) -> None: + return self.put(writes) From 630d9c79edca85ef0417afd78727487c973e25e2 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 20 Aug 2024 13:55:28 -0700 Subject: [PATCH 038/287] Split out async batch to sep file --- libs/langgraph/langgraph/graph/state.py | 2 +- libs/langgraph/langgraph/kv/base.py | 84 +----------------------- libs/langgraph/langgraph/kv/batch.py | 85 +++++++++++++++++++++++++ libs/langgraph/tests/test_kv.py | 31 ++++++--- 4 files changed, 110 insertions(+), 92 deletions(-) create mode 100644 libs/langgraph/langgraph/kv/batch.py diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 261fbf1d5..c116e6d47 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -381,9 +381,9 @@ class StateGraph(Graph): def compile( self, + checkpointer: Optional[BaseCheckpointSaver] = None, *, kv: Optional[BaseKV] = None, - checkpointer: Optional[BaseCheckpointSaver] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: bool = False, diff --git a/libs/langgraph/langgraph/kv/base.py b/libs/langgraph/langgraph/kv/base.py index 570783ec1..37e07ee84 100644 --- a/libs/langgraph/langgraph/kv/base.py +++ b/libs/langgraph/langgraph/kv/base.py @@ -1,5 +1,4 @@ -import asyncio -from typing import Any, List, NamedTuple, Optional, Union +from typing import Any, List, Optional V = dict[str, Any] @@ -30,84 +29,3 @@ class BaseKV: async def aput(self, writes: List[tuple[str, str, Optional[V]]]) -> None: # list[(namespace, key, value | none)] -> None raise NotImplementedError - - -class GetOp(NamedTuple): - pairs: List[tuple[str, str]] - - -class ListOp(NamedTuple): - prefixes: List[str] - - -class PutOp(NamedTuple): - writes: List[tuple[str, str, Optional[V]]] - - -class KeyValueStore(BaseKV): - def __init__(self, kv: BaseKV) -> None: - self.kv = kv - self.aqueue: dict[asyncio.Future, Union[GetOp, ListOp, PutOp]] = {} - self.task = asyncio.create_task(_run(self.aqueue, self.kv)) - - def __del__(self) -> None: - self.task.cancel() - - async def aget( - self, pairs: List[tuple[str, str]] - ) -> dict[tuple[str, str], Optional[V]]: - fut = asyncio.get_running_loop().create_future() - self.aqueue[fut] = GetOp(pairs) - return await fut - - async def alist(self, prefixes: List[str]) -> dict[str, dict[str, V]]: - fut = asyncio.get_running_loop().create_future() - self.aqueue[fut] = ListOp(prefixes) - return await fut - - async def aput(self, writes: List[tuple[str, str, Optional[V]]]) -> None: - fut = asyncio.get_running_loop().create_future() - self.aqueue[fut] = PutOp(writes) - return await fut - - -async def _run( - aqueue: dict[asyncio.Future, Union[GetOp, ListOp, PutOp]], kv: BaseKV -) -> None: - while True: - await asyncio.sleep(0) - if not aqueue: - continue - # this could use a lock, if we want thread safety - taken = aqueue.copy() - aqueue.clear() - # action each operation - gets = {f: o for f, o in taken.items() if isinstance(o, GetOp)} - if gets: - try: - results = await kv.aget([p for op in gets.values() for p in op.pairs]) - for fut, op in gets.items(): - fut.set_result({k: results.get(k) for k in op.pairs}) - except Exception as e: - for fut in gets: - fut.set_exception(e) - lists = {f: o for f, o in taken.items() if isinstance(o, ListOp)} - if lists: - try: - results = await kv.alist( - [p for op in lists.values() for p in op.prefixes] - ) - for fut, op in lists.items(): - fut.set_result({k: results.get(k) for k in op.prefixes}) - except Exception as e: - for fut in lists: - fut.set_exception(e) - puts = {f: o for f, o in taken.items() if isinstance(o, PutOp)} - if puts: - try: - await kv.aput([w for op in puts.values() for w in op.writes]) - for fut in puts: - fut.set_result(None) - except Exception as e: - for fut in puts: - fut.set_exception(e) diff --git a/libs/langgraph/langgraph/kv/batch.py b/libs/langgraph/langgraph/kv/batch.py new file mode 100644 index 000000000..fa6cfc59b --- /dev/null +++ b/libs/langgraph/langgraph/kv/batch.py @@ -0,0 +1,85 @@ +import asyncio +from typing import NamedTuple, Optional, Union + +from langgraph.kv.base import BaseKV, V + + +class GetOp(NamedTuple): + pairs: list[tuple[str, str]] + + +class ListOp(NamedTuple): + prefixes: list[str] + + +class PutOp(NamedTuple): + writes: list[tuple[str, str, Optional[V]]] + + +class AsyncBatchedKV(BaseKV): + def __init__(self, kv: BaseKV) -> None: + self.kv = kv + self.aqueue: dict[asyncio.Future, Union[GetOp, ListOp, PutOp]] = {} + self.task = asyncio.create_task(_run(self.aqueue, self.kv)) + + def __del__(self) -> None: + self.task.cancel() + + async def aget( + self, pairs: list[tuple[str, str]] + ) -> dict[tuple[str, str], Optional[V]]: + fut = asyncio.get_running_loop().create_future() + self.aqueue[fut] = GetOp(pairs) + return await fut + + async def alist(self, prefixes: list[str]) -> dict[str, dict[str, V]]: + fut = asyncio.get_running_loop().create_future() + self.aqueue[fut] = ListOp(prefixes) + return await fut + + async def aput(self, writes: list[tuple[str, str, Optional[V]]]) -> None: + fut = asyncio.get_running_loop().create_future() + self.aqueue[fut] = PutOp(writes) + return await fut + + +async def _run( + aqueue: dict[asyncio.Future, Union[GetOp, ListOp, PutOp]], kv: BaseKV +) -> None: + while True: + await asyncio.sleep(0) + if not aqueue: + continue + # this could use a lock, if we want thread safety + taken = aqueue.copy() + aqueue.clear() + # action each operation + gets = {f: o for f, o in taken.items() if isinstance(o, GetOp)} + if gets: + try: + results = await kv.aget([p for op in gets.values() for p in op.pairs]) + for fut, op in gets.items(): + fut.set_result({k: results.get(k) for k in op.pairs}) + except Exception as e: + for fut in gets: + fut.set_exception(e) + lists = {f: o for f, o in taken.items() if isinstance(o, ListOp)} + if lists: + try: + results = await kv.alist( + [p for op in lists.values() for p in op.prefixes] + ) + for fut, op in lists.items(): + fut.set_result({k: results.get(k) for k in op.prefixes}) + except Exception as e: + for fut in lists: + fut.set_exception(e) + puts = {f: o for f, o in taken.items() if isinstance(o, PutOp)} + if puts: + try: + await kv.aput([w for op in puts.values() for w in op.writes]) + for fut in puts: + fut.set_result(None) + except Exception as e: + for fut in puts: + fut.set_exception(e) diff --git a/libs/langgraph/tests/test_kv.py b/libs/langgraph/tests/test_kv.py index 2e24ee07d..5af46ef9c 100644 --- a/libs/langgraph/tests/test_kv.py +++ b/libs/langgraph/tests/test_kv.py @@ -1,22 +1,28 @@ import asyncio -from typing import Any, List +from typing import Any from pytest_mock import MockerFixture -from langgraph.kv.base import BaseKV, KeyValueStore +from langgraph.kv.base import BaseKV +from langgraph.kv.batch import AsyncBatchedKV -async def test_kv_queue(mocker: MockerFixture) -> None: +async def test_kv_async_batch(mocker: MockerFixture) -> None: aget = mocker.stub() + alist = mocker.stub() class MockKV(BaseKV): async def aget( - self, pairs: List[tuple[str, str]] + self, pairs: list[tuple[str, str]] ) -> dict[tuple[str, str], dict[str, Any] | None]: aget(pairs) - return {pair: {0: pair[0], 1: pair[1]} for pair in pairs} + return {pair: 1 for pair in pairs} - store = KeyValueStore(MockKV()) + async def alist(self, prefixes: list[str]) -> dict[str, dict[str, Any]]: + alist(prefixes) + return {prefix: {prefix: 1} for prefix in prefixes} + + store = AsyncBatchedKV(MockKV()) # concurrent calls are batched results = await asyncio.gather( @@ -24,9 +30,18 @@ async def test_kv_queue(mocker: MockerFixture) -> None: store.aget([("c", "d")]), ) assert results == [ - {("a", "b"): {0: "a", 1: "b"}}, - {("c", "d"): {0: "c", 1: "d"}}, + {("a", "b"): 1}, + {("c", "d"): 1}, ] assert [c.args for c in aget.call_args_list] == [ ([("a", "b"), ("c", "d")],), ] + + results = await asyncio.gather( + store.alist(["a", "b"]), + store.alist(["c", "d"]), + ) + assert results == [{"a": {"a": 1}, "b": {"b": 1}}, {"c": {"c": 1}, "d": {"d": 1}}] + assert [c.args for c in alist.call_args_list] == [ + (["a", "b", "c", "d"],), + ] From 4e1db854f6773758a06b0c288cc79f1cbfbd47c8 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 20 Aug 2024 14:55:23 -0700 Subject: [PATCH 039/287] Add async test --- .../langgraph/managed/shared_value.py | 1 - libs/langgraph/tests/test_pregel_async.py | 40 ++++++++++++++++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/libs/langgraph/langgraph/managed/shared_value.py b/libs/langgraph/langgraph/managed/shared_value.py index 7c55fc858..c12482c5d 100644 --- a/libs/langgraph/langgraph/managed/shared_value.py +++ b/libs/langgraph/langgraph/managed/shared_value.py @@ -74,7 +74,6 @@ class SharedValue(WritableManagedValue[Value, Update]): self, config: RunnableConfig, *, typ: Type[Any], scope: str, key: str ) -> None: if typ := _strip_extras(typ): - print(typ) if typ not in ( dict, collections.abc.Mapping, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 3048ddbb4..02cd637ac 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -52,6 +52,9 @@ from langgraph.errors import InvalidUpdateError, NodeInterrupt from langgraph.graph import END, Graph, StateGraph from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages +from langgraph.kv.batch import AsyncBatchedKV +from langgraph.kv.memory import MemoryKV +from langgraph.managed.shared_value import SharedValue from langgraph.prebuilt.chat_agent_executor import ( create_tool_calling_executor, ) @@ -4778,10 +4781,33 @@ async def test_start_branch_then() -> None: class State(TypedDict): my_key: Annotated[str, operator.add] market: str + shared: Annotated[dict[str, dict[str, Any]], SharedValue.on("assistant_id")] + other: Annotated[dict[str, dict[str, Any]], SharedValue.on("assistant_id")] + + def assert_shared_value(data: State, config: RunnableConfig) -> State: + assert "shared" in data + if thread_id := config["configurable"].get("thread_id"): + if thread_id == "1": + # this is the first thread, so should not see a value + assert data["shared"] == {} + return {"shared": {"1": {"hello": "world"}}, "other": {"2": {1: 2}}} + elif thread_id == "2": + # this should get value saved by thread 1 + assert data["shared"] == {"1": {"hello": "world"}} + elif thread_id == "3": + # this is a different assistant, so should not see previous value + assert data["shared"] == {} + return {} + + def tool_two_slow(data: State, config: RunnableConfig) -> State: + return {"my_key": " slow", **assert_shared_value(data, config)} + + def tool_two_fast(data: State, config: RunnableConfig) -> State: + return {"my_key": " fast", **assert_shared_value(data, config)} tool_two_graph = StateGraph(State) - tool_two_graph.add_node("tool_two_slow", lambda s, config: {"my_key": " slow"}) - tool_two_graph.add_node("tool_two_fast", lambda s: {"my_key": " fast"}) + tool_two_graph.add_node("tool_two_slow", tool_two_slow) + tool_two_graph.add_node("tool_two_fast", tool_two_fast) tool_two_graph.set_conditional_entry_point( lambda s: "tool_two_slow" if s["market"] == "DE" else "tool_two_fast", then=END ) @@ -4798,14 +4824,16 @@ async def test_start_branch_then() -> None: async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"] + kv=AsyncBatchedKV(MemoryKV()), + checkpointer=saver, + interrupt_before=["tool_two_fast", "tool_two_slow"], ) # missing thread_id with pytest.raises(ValueError, match="thread_id"): await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1"}} + thread1 = {"configurable": {"thread_id": "1", "assistant_id": "a"}} # stop when about to enter node assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { "my_key": "value", @@ -4865,7 +4893,7 @@ async def test_start_branch_then() -> None: ][-1].config, ) - thread2 = {"configurable": {"thread_id": "2"}} + thread2 = {"configurable": {"thread_id": "2", "assistant_id": "a"}} # stop when about to enter node assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { "my_key": "value", @@ -4913,7 +4941,7 @@ async def test_start_branch_then() -> None: ][-1].config, ) - thread3 = {"configurable": {"thread_id": "3"}} + thread3 = {"configurable": {"thread_id": "3", "assistant_id": "b"}} # stop when about to enter node assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread3) == { "my_key": "value", From c3794f1fd3211bbc55bfec225d6ae3aa64f4e0d5 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 20 Aug 2024 15:03:09 -0700 Subject: [PATCH 040/287] Use batched async kv inside loop --- libs/langgraph/langgraph/pregel/loop.py | 10 +++++++--- libs/langgraph/tests/test_pregel_async.py | 3 +-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index aaa43be67..903a3a644 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -50,6 +50,8 @@ from langgraph.constants import ( Interrupt, ) from langgraph.errors import EmptyInputError, GraphInterrupt +from langgraph.kv.base import BaseKV +from langgraph.kv.batch import AsyncBatchedKV from langgraph.managed.base import ( AsyncManagedValuesManager, ManagedValueMapping, @@ -103,7 +105,7 @@ class PregelLoop: ] ] graph: "Pregel" - + kv: Optional[BaseKV] submit: Submit channels: Mapping[str, BaseChannel] managed: ManagedValueMapping @@ -425,6 +427,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): graph: "Pregel", ) -> None: super().__init__(input, config=config, checkpointer=checkpointer, graph=graph) + self.kv = graph.kv self.stack = ExitStack() if checkpointer: self.checkpointer_get_next_version = checkpointer.get_next_version @@ -476,7 +479,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): self.managed = self.stack.enter_context( ManagedValuesManager( self.graph.managed_values_dict, - patch_config(self.config, configurable={CONFIG_KEY_KV: self.graph.kv}), + patch_config(self.config, configurable={CONFIG_KEY_KV: self.kv}), ) ) self.stack.push(self._suppress_interrupt) @@ -508,6 +511,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): graph: "Pregel", ) -> None: super().__init__(input, config=config, checkpointer=checkpointer, graph=graph) + self.kv = AsyncBatchedKV(graph.kv) if graph.kv else None self.stack = AsyncExitStack() if checkpointer: self.checkpointer_get_next_version = checkpointer.get_next_version @@ -563,7 +567,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): self.managed = await self.stack.enter_async_context( AsyncManagedValuesManager( self.graph.managed_values_dict, - patch_config(self.config, configurable={CONFIG_KEY_KV: self.graph.kv}), + patch_config(self.config, configurable={CONFIG_KEY_KV: self.kv}), ) ) self.stack.push(self._suppress_interrupt) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 02cd637ac..b383a4d3f 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -52,7 +52,6 @@ from langgraph.errors import InvalidUpdateError, NodeInterrupt from langgraph.graph import END, Graph, StateGraph from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages -from langgraph.kv.batch import AsyncBatchedKV from langgraph.kv.memory import MemoryKV from langgraph.managed.shared_value import SharedValue from langgraph.prebuilt.chat_agent_executor import ( @@ -4824,7 +4823,7 @@ async def test_start_branch_then() -> None: async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: tool_two = tool_two_graph.compile( - kv=AsyncBatchedKV(MemoryKV()), + kv=MemoryKV(), checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"], ) From b228fc1a9bf75fcdf99de3694d12eb930a23293c Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 20 Aug 2024 16:36:35 -0700 Subject: [PATCH 041/287] Add serde --- libs/langgraph/langgraph/kv/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libs/langgraph/langgraph/kv/base.py b/libs/langgraph/langgraph/kv/base.py index 37e07ee84..bbd2e0899 100644 --- a/libs/langgraph/langgraph/kv/base.py +++ b/libs/langgraph/langgraph/kv/base.py @@ -1,9 +1,15 @@ from typing import Any, List, Optional +from langgraph.checkpoint.serde.base import SerializerProtocol +from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer + V = dict[str, Any] class BaseKV: + def __init__(self, *, serde: SerializerProtocol = JsonPlusSerializer()) -> None: + self.serde = serde + def get(self, pairs: List[tuple[str, str]]) -> dict[tuple[str, str], Optional[V]]: # list[(namespace, key)] -> dict[(namespace, key), value | none] raise NotImplementedError From bd7b9cca21bb74868de6ff9b9407b9793215ba62 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 09:04:43 -0700 Subject: [PATCH 042/287] WIP --- libs/langgraph/langgraph/graph/state.py | 4 +-- libs/langgraph/langgraph/kv/base.py | 18 +---------- libs/langgraph/langgraph/kv/batch.py | 30 ++++--------------- libs/langgraph/langgraph/kv/memory.py | 12 ++------ .../langgraph/managed/shared_value.py | 4 +-- libs/langgraph/langgraph/pregel/__init__.py | 4 +-- libs/langgraph/langgraph/pregel/loop.py | 4 +-- libs/langgraph/tests/test_kv.py | 4 +-- 8 files changed, 18 insertions(+), 62 deletions(-) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index c116e6d47..26770bd20 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -40,7 +40,7 @@ from langgraph.graph.graph import ( Graph, Send, ) -from langgraph.kv.base import BaseKV +from langgraph.kv.base import BaseMemory from langgraph.managed.base import ( ChannelKeyPlaceholder, ChannelTypePlaceholder, @@ -383,7 +383,7 @@ class StateGraph(Graph): self, checkpointer: Optional[BaseCheckpointSaver] = None, *, - kv: Optional[BaseKV] = None, + kv: Optional[BaseMemory] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: bool = False, diff --git a/libs/langgraph/langgraph/kv/base.py b/libs/langgraph/langgraph/kv/base.py index bbd2e0899..0c5791eb6 100644 --- a/libs/langgraph/langgraph/kv/base.py +++ b/libs/langgraph/langgraph/kv/base.py @@ -1,19 +1,9 @@ from typing import Any, List, Optional -from langgraph.checkpoint.serde.base import SerializerProtocol -from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer - V = dict[str, Any] -class BaseKV: - def __init__(self, *, serde: SerializerProtocol = JsonPlusSerializer()) -> None: - self.serde = serde - - def get(self, pairs: List[tuple[str, str]]) -> dict[tuple[str, str], Optional[V]]: - # list[(namespace, key)] -> dict[(namespace, key), value | none] - raise NotImplementedError - +class BaseMemory: def list(self, prefixes: List[str]) -> dict[str, dict[str, V]]: # list[namespace] -> dict[namespace, list[value]] raise NotImplementedError @@ -22,12 +12,6 @@ class BaseKV: # list[(namespace, key, value | none)] -> None raise NotImplementedError - async def aget( - self, pairs: List[tuple[str, str]] - ) -> dict[tuple[str, str], Optional[V]]: - # list[(namespace, key)] -> dict[(namespace, key), value | none] - raise NotImplementedError - async def alist(self, prefixes: List[str]) -> dict[str, dict[str, V]]: # list[namespace] -> dict[namespace, list[value]] raise NotImplementedError diff --git a/libs/langgraph/langgraph/kv/batch.py b/libs/langgraph/langgraph/kv/batch.py index fa6cfc59b..971d50df4 100644 --- a/libs/langgraph/langgraph/kv/batch.py +++ b/libs/langgraph/langgraph/kv/batch.py @@ -1,11 +1,7 @@ import asyncio from typing import NamedTuple, Optional, Union -from langgraph.kv.base import BaseKV, V - - -class GetOp(NamedTuple): - pairs: list[tuple[str, str]] +from langgraph.kv.base import BaseMemory, V class ListOp(NamedTuple): @@ -16,22 +12,15 @@ class PutOp(NamedTuple): writes: list[tuple[str, str, Optional[V]]] -class AsyncBatchedKV(BaseKV): - def __init__(self, kv: BaseKV) -> None: +class AsyncBatchedKV(BaseMemory): + def __init__(self, kv: BaseMemory) -> None: self.kv = kv - self.aqueue: dict[asyncio.Future, Union[GetOp, ListOp, PutOp]] = {} + self.aqueue: dict[asyncio.Future, Union[ListOp, PutOp]] = {} self.task = asyncio.create_task(_run(self.aqueue, self.kv)) def __del__(self) -> None: self.task.cancel() - async def aget( - self, pairs: list[tuple[str, str]] - ) -> dict[tuple[str, str], Optional[V]]: - fut = asyncio.get_running_loop().create_future() - self.aqueue[fut] = GetOp(pairs) - return await fut - async def alist(self, prefixes: list[str]) -> dict[str, dict[str, V]]: fut = asyncio.get_running_loop().create_future() self.aqueue[fut] = ListOp(prefixes) @@ -44,7 +33,7 @@ class AsyncBatchedKV(BaseKV): async def _run( - aqueue: dict[asyncio.Future, Union[GetOp, ListOp, PutOp]], kv: BaseKV + aqueue: dict[asyncio.Future, Union[ListOp, PutOp]], kv: BaseMemory ) -> None: while True: await asyncio.sleep(0) @@ -54,15 +43,6 @@ async def _run( taken = aqueue.copy() aqueue.clear() # action each operation - gets = {f: o for f, o in taken.items() if isinstance(o, GetOp)} - if gets: - try: - results = await kv.aget([p for op in gets.values() for p in op.pairs]) - for fut, op in gets.items(): - fut.set_result({k: results.get(k) for k in op.pairs}) - except Exception as e: - for fut in gets: - fut.set_exception(e) lists = {f: o for f, o in taken.items() if isinstance(o, ListOp)} if lists: try: diff --git a/libs/langgraph/langgraph/kv/memory.py b/libs/langgraph/langgraph/kv/memory.py index 265dfe77a..4b0e92a2d 100644 --- a/libs/langgraph/langgraph/kv/memory.py +++ b/libs/langgraph/langgraph/kv/memory.py @@ -1,21 +1,13 @@ from collections import defaultdict from typing import List, Optional -from langgraph.kv.base import BaseKV, V +from langgraph.kv.base import BaseMemory, V -class MemoryKV(BaseKV): +class MemoryKV(BaseMemory): def __init__(self) -> None: self.data: dict[str, dict[str, V]] = defaultdict(dict) - def get(self, pairs: List[tuple[str, str]]) -> dict[tuple[str, str], Optional[V]]: - return {pair: self.data[pair[0]].get(pair[1]) for pair in pairs} - - async def aget( - self, pairs: List[tuple[str, str]] - ) -> dict[tuple[str, str], Optional[V]]: - return self.get(pairs) - def list(self, prefixes: List[str]) -> dict[str, dict[str, V]]: return {prefix: self.data[prefix] for prefix in prefixes} diff --git a/libs/langgraph/langgraph/managed/shared_value.py b/libs/langgraph/langgraph/managed/shared_value.py index c12482c5d..a1659bb8b 100644 --- a/libs/langgraph/langgraph/managed/shared_value.py +++ b/libs/langgraph/langgraph/managed/shared_value.py @@ -14,7 +14,7 @@ from typing_extensions import NotRequired, Required, Self from langgraph.constants import CONFIG_KEY_KV from langgraph.errors import InvalidUpdateError -from langgraph.kv.base import BaseKV +from langgraph.kv.base import BaseMemory from langgraph.managed.base import ( ChannelKeyPlaceholder, ChannelTypePlaceholder, @@ -83,7 +83,7 @@ class SharedValue(WritableManagedValue[Value, Update]): self.scope = scope self.config = config self.value: Value = {} - self.kv: BaseKV = config["configurable"].get(CONFIG_KEY_KV) + self.kv: BaseMemory = config["configurable"].get(CONFIG_KEY_KV) if self.kv is None: self.ns: Optional[str] = None elif scope_value := config["configurable"].get(self.scope): diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 833453197..a5049d36f 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -73,7 +73,7 @@ from langgraph.constants import ( Interrupt, ) from langgraph.errors import GraphInterrupt, GraphRecursionError, InvalidUpdateError -from langgraph.kv.base import BaseKV +from langgraph.kv.base import BaseMemory from langgraph.managed.base import ( AsyncManagedValuesManager, ManagedValuesManager, @@ -224,7 +224,7 @@ class Pregel( checkpointer: Optional[BaseCheckpointSaver] = None """Checkpointer used to save and load graph state. Defaults to None.""" - kv: Optional[BaseKV] = None + kv: Optional[BaseMemory] = None """Key-value store to use. Defaults to None.""" retry_policy: Optional[RetryPolicy] = None diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 903a3a644..41234e9c7 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -50,7 +50,7 @@ from langgraph.constants import ( Interrupt, ) from langgraph.errors import EmptyInputError, GraphInterrupt -from langgraph.kv.base import BaseKV +from langgraph.kv.base import BaseMemory from langgraph.kv.batch import AsyncBatchedKV from langgraph.managed.base import ( AsyncManagedValuesManager, @@ -105,7 +105,7 @@ class PregelLoop: ] ] graph: "Pregel" - kv: Optional[BaseKV] + kv: Optional[BaseMemory] submit: Submit channels: Mapping[str, BaseChannel] managed: ManagedValueMapping diff --git a/libs/langgraph/tests/test_kv.py b/libs/langgraph/tests/test_kv.py index 5af46ef9c..68294f42c 100644 --- a/libs/langgraph/tests/test_kv.py +++ b/libs/langgraph/tests/test_kv.py @@ -3,7 +3,7 @@ from typing import Any from pytest_mock import MockerFixture -from langgraph.kv.base import BaseKV +from langgraph.kv.base import BaseMemory from langgraph.kv.batch import AsyncBatchedKV @@ -11,7 +11,7 @@ async def test_kv_async_batch(mocker: MockerFixture) -> None: aget = mocker.stub() alist = mocker.stub() - class MockKV(BaseKV): + class MockKV(BaseMemory): async def aget( self, pairs: list[tuple[str, str]] ) -> dict[tuple[str, str], dict[str, Any] | None]: From c9e6ee6da70c3880df5ae54fd8f54e5c546a5666 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 09:34:12 -0700 Subject: [PATCH 043/287] Rename --- libs/langgraph/langgraph/graph/state.py | 6 +++--- libs/langgraph/langgraph/managed/shared_value.py | 4 ++-- libs/langgraph/langgraph/pregel/__init__.py | 4 ++-- libs/langgraph/langgraph/pregel/loop.py | 14 +++++++------- libs/langgraph/langgraph/{kv => store}/__init__.py | 0 libs/langgraph/langgraph/{kv => store}/base.py | 2 +- libs/langgraph/langgraph/{kv => store}/batch.py | 8 ++++---- libs/langgraph/langgraph/{kv => store}/memory.py | 4 ++-- libs/langgraph/tests/test_kv.py | 8 ++++---- libs/langgraph/tests/test_pregel.py | 4 ++-- libs/langgraph/tests/test_pregel_async.py | 4 ++-- 11 files changed, 29 insertions(+), 29 deletions(-) rename libs/langgraph/langgraph/{kv => store}/__init__.py (100%) rename libs/langgraph/langgraph/{kv => store}/base.py (97%) rename libs/langgraph/langgraph/{kv => store}/batch.py (93%) rename libs/langgraph/langgraph/{kv => store}/memory.py (91%) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 26770bd20..8c85b7760 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -40,7 +40,6 @@ from langgraph.graph.graph import ( Graph, Send, ) -from langgraph.kv.base import BaseMemory from langgraph.managed.base import ( ChannelKeyPlaceholder, ChannelTypePlaceholder, @@ -52,6 +51,7 @@ from langgraph.managed.base import ( from langgraph.pregel.read import ChannelRead, PregelNode from langgraph.pregel.types import All, RetryPolicy from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry +from langgraph.store.base import BaseStore from langgraph.utils import RunnableCallable, coerce_to_runnable logger = logging.getLogger(__name__) @@ -383,7 +383,7 @@ class StateGraph(Graph): self, checkpointer: Optional[BaseCheckpointSaver] = None, *, - kv: Optional[BaseMemory] = None, + store: Optional[BaseStore] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: bool = False, @@ -452,7 +452,7 @@ class StateGraph(Graph): interrupt_after_nodes=interrupt_after, auto_validate=False, debug=debug, - kv=kv, + store=store, ) compiled.attach_node(START, None) diff --git a/libs/langgraph/langgraph/managed/shared_value.py b/libs/langgraph/langgraph/managed/shared_value.py index a1659bb8b..74a7c1ef4 100644 --- a/libs/langgraph/langgraph/managed/shared_value.py +++ b/libs/langgraph/langgraph/managed/shared_value.py @@ -14,13 +14,13 @@ from typing_extensions import NotRequired, Required, Self from langgraph.constants import CONFIG_KEY_KV from langgraph.errors import InvalidUpdateError -from langgraph.kv.base import BaseMemory from langgraph.managed.base import ( ChannelKeyPlaceholder, ChannelTypePlaceholder, ConfiguredManagedValue, WritableManagedValue, ) +from langgraph.store.base import BaseStore V = dict[str, Any] @@ -83,7 +83,7 @@ class SharedValue(WritableManagedValue[Value, Update]): self.scope = scope self.config = config self.value: Value = {} - self.kv: BaseMemory = config["configurable"].get(CONFIG_KEY_KV) + self.kv: BaseStore = config["configurable"].get(CONFIG_KEY_KV) if self.kv is None: self.ns: Optional[str] = None elif scope_value := config["configurable"].get(self.scope): diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index a5049d36f..5e058fd16 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -73,7 +73,6 @@ from langgraph.constants import ( Interrupt, ) from langgraph.errors import GraphInterrupt, GraphRecursionError, InvalidUpdateError -from langgraph.kv.base import BaseMemory from langgraph.managed.base import ( AsyncManagedValuesManager, ManagedValuesManager, @@ -109,6 +108,7 @@ from langgraph.pregel.types import ( from langgraph.pregel.utils import get_new_channel_versions from langgraph.pregel.validate import validate_graph, validate_keys from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry +from langgraph.store.base import BaseStore WriteValue = Union[ Runnable[Input, Output], @@ -224,7 +224,7 @@ class Pregel( checkpointer: Optional[BaseCheckpointSaver] = None """Checkpointer used to save and load graph state. Defaults to None.""" - kv: Optional[BaseMemory] = None + store: Optional[BaseStore] = None """Key-value store to use. Defaults to None.""" retry_policy: Optional[RetryPolicy] = None diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 41234e9c7..8079c0dc2 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -50,8 +50,6 @@ from langgraph.constants import ( Interrupt, ) from langgraph.errors import EmptyInputError, GraphInterrupt -from langgraph.kv.base import BaseMemory -from langgraph.kv.batch import AsyncBatchedKV from langgraph.managed.base import ( AsyncManagedValuesManager, ManagedValueMapping, @@ -74,6 +72,8 @@ from langgraph.pregel.executor import ( from langgraph.pregel.io import map_input, map_output_updates, map_output_values, single from langgraph.pregel.types import PregelExecutableTask from langgraph.pregel.utils import get_new_channel_versions +from langgraph.store.base import BaseStore +from langgraph.store.batch import AsyncBatchedStore if TYPE_CHECKING: from langgraph.pregel import Pregel @@ -105,7 +105,7 @@ class PregelLoop: ] ] graph: "Pregel" - kv: Optional[BaseMemory] + store: Optional[BaseStore] submit: Submit channels: Mapping[str, BaseChannel] managed: ManagedValueMapping @@ -427,7 +427,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): graph: "Pregel", ) -> None: super().__init__(input, config=config, checkpointer=checkpointer, graph=graph) - self.kv = graph.kv + self.store = graph.store self.stack = ExitStack() if checkpointer: self.checkpointer_get_next_version = checkpointer.get_next_version @@ -479,7 +479,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): self.managed = self.stack.enter_context( ManagedValuesManager( self.graph.managed_values_dict, - patch_config(self.config, configurable={CONFIG_KEY_KV: self.kv}), + patch_config(self.config, configurable={CONFIG_KEY_KV: self.store}), ) ) self.stack.push(self._suppress_interrupt) @@ -511,7 +511,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): graph: "Pregel", ) -> None: super().__init__(input, config=config, checkpointer=checkpointer, graph=graph) - self.kv = AsyncBatchedKV(graph.kv) if graph.kv else None + self.store = AsyncBatchedStore(graph.store) if graph.store else None self.stack = AsyncExitStack() if checkpointer: self.checkpointer_get_next_version = checkpointer.get_next_version @@ -567,7 +567,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): self.managed = await self.stack.enter_async_context( AsyncManagedValuesManager( self.graph.managed_values_dict, - patch_config(self.config, configurable={CONFIG_KEY_KV: self.kv}), + patch_config(self.config, configurable={CONFIG_KEY_KV: self.store}), ) ) self.stack.push(self._suppress_interrupt) diff --git a/libs/langgraph/langgraph/kv/__init__.py b/libs/langgraph/langgraph/store/__init__.py similarity index 100% rename from libs/langgraph/langgraph/kv/__init__.py rename to libs/langgraph/langgraph/store/__init__.py diff --git a/libs/langgraph/langgraph/kv/base.py b/libs/langgraph/langgraph/store/base.py similarity index 97% rename from libs/langgraph/langgraph/kv/base.py rename to libs/langgraph/langgraph/store/base.py index 0c5791eb6..7f0030f56 100644 --- a/libs/langgraph/langgraph/kv/base.py +++ b/libs/langgraph/langgraph/store/base.py @@ -3,7 +3,7 @@ from typing import Any, List, Optional V = dict[str, Any] -class BaseMemory: +class BaseStore: def list(self, prefixes: List[str]) -> dict[str, dict[str, V]]: # list[namespace] -> dict[namespace, list[value]] raise NotImplementedError diff --git a/libs/langgraph/langgraph/kv/batch.py b/libs/langgraph/langgraph/store/batch.py similarity index 93% rename from libs/langgraph/langgraph/kv/batch.py rename to libs/langgraph/langgraph/store/batch.py index 971d50df4..cfd05e548 100644 --- a/libs/langgraph/langgraph/kv/batch.py +++ b/libs/langgraph/langgraph/store/batch.py @@ -1,7 +1,7 @@ import asyncio from typing import NamedTuple, Optional, Union -from langgraph.kv.base import BaseMemory, V +from langgraph.store.base import BaseStore, V class ListOp(NamedTuple): @@ -12,8 +12,8 @@ class PutOp(NamedTuple): writes: list[tuple[str, str, Optional[V]]] -class AsyncBatchedKV(BaseMemory): - def __init__(self, kv: BaseMemory) -> None: +class AsyncBatchedStore(BaseStore): + def __init__(self, kv: BaseStore) -> None: self.kv = kv self.aqueue: dict[asyncio.Future, Union[ListOp, PutOp]] = {} self.task = asyncio.create_task(_run(self.aqueue, self.kv)) @@ -33,7 +33,7 @@ class AsyncBatchedKV(BaseMemory): async def _run( - aqueue: dict[asyncio.Future, Union[ListOp, PutOp]], kv: BaseMemory + aqueue: dict[asyncio.Future, Union[ListOp, PutOp]], kv: BaseStore ) -> None: while True: await asyncio.sleep(0) diff --git a/libs/langgraph/langgraph/kv/memory.py b/libs/langgraph/langgraph/store/memory.py similarity index 91% rename from libs/langgraph/langgraph/kv/memory.py rename to libs/langgraph/langgraph/store/memory.py index 4b0e92a2d..48fa2884f 100644 --- a/libs/langgraph/langgraph/kv/memory.py +++ b/libs/langgraph/langgraph/store/memory.py @@ -1,10 +1,10 @@ from collections import defaultdict from typing import List, Optional -from langgraph.kv.base import BaseMemory, V +from langgraph.store.base import BaseStore, V -class MemoryKV(BaseMemory): +class MemoryStore(BaseStore): def __init__(self) -> None: self.data: dict[str, dict[str, V]] = defaultdict(dict) diff --git a/libs/langgraph/tests/test_kv.py b/libs/langgraph/tests/test_kv.py index 68294f42c..0c2862b48 100644 --- a/libs/langgraph/tests/test_kv.py +++ b/libs/langgraph/tests/test_kv.py @@ -3,15 +3,15 @@ from typing import Any from pytest_mock import MockerFixture -from langgraph.kv.base import BaseMemory -from langgraph.kv.batch import AsyncBatchedKV +from langgraph.store.base import BaseStore +from langgraph.store.batch import AsyncBatchedStore async def test_kv_async_batch(mocker: MockerFixture) -> None: aget = mocker.stub() alist = mocker.stub() - class MockKV(BaseMemory): + class MockKV(BaseStore): async def aget( self, pairs: list[tuple[str, str]] ) -> dict[tuple[str, str], dict[str, Any] | None]: @@ -22,7 +22,7 @@ async def test_kv_async_batch(mocker: MockerFixture) -> None: alist(prefixes) return {prefix: {prefix: 1} for prefix in prefixes} - store = AsyncBatchedKV(MockKV()) + store = AsyncBatchedStore(MockKV()) # concurrent calls are batched results = await asyncio.gather( diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index fef936c80..8cf6aafa8 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -58,7 +58,6 @@ from langgraph.graph import END, Graph from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages from langgraph.graph.state import StateGraph -from langgraph.kv.memory import MemoryKV from langgraph.managed.shared_value import SharedValue from langgraph.prebuilt.chat_agent_executor import ( create_tool_calling_executor, @@ -67,6 +66,7 @@ from langgraph.prebuilt.tool_node import ToolNode from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask +from langgraph.store.memory import MemoryStore from tests.any_str import AnyStr, ExceptionLike from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -6250,7 +6250,7 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: with SqliteSaver.from_conn_string(":memory:") as saver: tool_two = tool_two_graph.compile( - kv=MemoryKV(), + store=MemoryStore(), checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"], ) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index b383a4d3f..d7a532489 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -52,7 +52,6 @@ from langgraph.errors import InvalidUpdateError, NodeInterrupt from langgraph.graph import END, Graph, StateGraph from langgraph.graph.graph import START from langgraph.graph.message import MessageGraph, add_messages -from langgraph.kv.memory import MemoryKV from langgraph.managed.shared_value import SharedValue from langgraph.prebuilt.chat_agent_executor import ( create_tool_calling_executor, @@ -62,6 +61,7 @@ from langgraph.prebuilt.tool_node import ToolNode from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask +from langgraph.store.memory import MemoryStore from tests.any_str import AnyStr, ExceptionLike from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -4823,7 +4823,7 @@ async def test_start_branch_then() -> None: async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: tool_two = tool_two_graph.compile( - kv=MemoryKV(), + store=MemoryStore(), checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"], ) From aa1a6be160c624aa2f3e2eb233cb10852aa05169 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 09:36:21 -0700 Subject: [PATCH 044/287] Rename more --- libs/langgraph/langgraph/constants.py | 4 ++-- .../langgraph/managed/shared_value.py | 22 +++++++++---------- libs/langgraph/langgraph/pregel/loop.py | 6 ++--- libs/langgraph/langgraph/store/batch.py | 12 +++++----- .../tests/{test_kv.py => test_store.py} | 18 +++------------ 5 files changed, 25 insertions(+), 37 deletions(-) rename libs/langgraph/tests/{test_kv.py => test_store.py} (68%) diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index cf87be337..51b6e1437 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -5,7 +5,7 @@ INPUT = "__input__" CONFIG_KEY_SEND = "__pregel_send" CONFIG_KEY_READ = "__pregel_read" CONFIG_KEY_CHECKPOINTER = "__pregel_checkpointer" -CONFIG_KEY_KV = "__pregel_kv" +CONFIG_KEY_STORE = "__pregel_store" CONFIG_KEY_RESUMING = "__pregel_resuming" CONFIG_KEY_TASK_ID = "__pregel_task_id" INTERRUPT = "__interrupt__" @@ -18,7 +18,7 @@ RESERVED = { CONFIG_KEY_SEND, CONFIG_KEY_READ, CONFIG_KEY_CHECKPOINTER, - CONFIG_KEY_KV, + CONFIG_KEY_STORE, CONFIG_KEY_RESUMING, CONFIG_KEY_TASK_ID, INPUT, diff --git a/libs/langgraph/langgraph/managed/shared_value.py b/libs/langgraph/langgraph/managed/shared_value.py index 74a7c1ef4..7f647d006 100644 --- a/libs/langgraph/langgraph/managed/shared_value.py +++ b/libs/langgraph/langgraph/managed/shared_value.py @@ -12,7 +12,7 @@ from typing import ( from langchain_core.runnables import RunnableConfig from typing_extensions import NotRequired, Required, Self -from langgraph.constants import CONFIG_KEY_KV +from langgraph.constants import CONFIG_KEY_STORE from langgraph.errors import InvalidUpdateError from langgraph.managed.base import ( ChannelKeyPlaceholder, @@ -56,8 +56,8 @@ class SharedValue(WritableManagedValue[Value, Update]): @contextmanager def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]: with super().enter(config, **kwargs) as value: - if value.kv is not None: - saved = value.kv.list([value.ns]) + if value.store is not None: + saved = value.store.list([value.ns]) value.value = saved[value.ns] yield value @@ -65,8 +65,8 @@ class SharedValue(WritableManagedValue[Value, Update]): @asynccontextmanager async def aenter(cls, config: RunnableConfig, **kwargs: Any) -> AsyncIterator[Self]: async with super().aenter(config, **kwargs) as value: - if value.kv is not None: - saved = await value.kv.alist([value.ns]) + if value.store is not None: + saved = await value.store.alist([value.ns]) value.value = saved[value.ns] yield value @@ -83,8 +83,8 @@ class SharedValue(WritableManagedValue[Value, Update]): self.scope = scope self.config = config self.value: Value = {} - self.kv: BaseStore = config["configurable"].get(CONFIG_KEY_KV) - if self.kv is None: + self.store: BaseStore = config["configurable"].get(CONFIG_KEY_STORE) + if self.store is None: self.ns: Optional[str] = None elif scope_value := config["configurable"].get(self.scope): self.ns = f"scoped:{scope}:{key}:{scope_value}" @@ -114,13 +114,13 @@ class SharedValue(WritableManagedValue[Value, Update]): return writes def update(self, values: Sequence[Update]) -> None: - if self.kv is None: + if self.store is None: self._process_update(values) else: - return self.kv.put(self._process_update(values)) + return self.store.put(self._process_update(values)) async def aupdate(self, writes: Sequence[Update]) -> None: - if self.kv is None: + if self.store is None: self._process_update(writes) else: - return await self.kv.aput(self._process_update(writes)) + return await self.store.aput(self._process_update(writes)) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 8079c0dc2..a1eadb697 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -41,9 +41,9 @@ from langgraph.checkpoint.base import ( empty_checkpoint, ) from langgraph.constants import ( - CONFIG_KEY_KV, CONFIG_KEY_READ, CONFIG_KEY_RESUMING, + CONFIG_KEY_STORE, ERROR, INPUT, INTERRUPT, @@ -479,7 +479,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): self.managed = self.stack.enter_context( ManagedValuesManager( self.graph.managed_values_dict, - patch_config(self.config, configurable={CONFIG_KEY_KV: self.store}), + patch_config(self.config, configurable={CONFIG_KEY_STORE: self.store}), ) ) self.stack.push(self._suppress_interrupt) @@ -567,7 +567,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): self.managed = await self.stack.enter_async_context( AsyncManagedValuesManager( self.graph.managed_values_dict, - patch_config(self.config, configurable={CONFIG_KEY_KV: self.store}), + patch_config(self.config, configurable={CONFIG_KEY_STORE: self.store}), ) ) self.stack.push(self._suppress_interrupt) diff --git a/libs/langgraph/langgraph/store/batch.py b/libs/langgraph/langgraph/store/batch.py index cfd05e548..54eb20d47 100644 --- a/libs/langgraph/langgraph/store/batch.py +++ b/libs/langgraph/langgraph/store/batch.py @@ -13,10 +13,10 @@ class PutOp(NamedTuple): class AsyncBatchedStore(BaseStore): - def __init__(self, kv: BaseStore) -> None: - self.kv = kv + def __init__(self, store: BaseStore) -> None: + self.store = store self.aqueue: dict[asyncio.Future, Union[ListOp, PutOp]] = {} - self.task = asyncio.create_task(_run(self.aqueue, self.kv)) + self.task = asyncio.create_task(_run(self.aqueue, self.store)) def __del__(self) -> None: self.task.cancel() @@ -33,7 +33,7 @@ class AsyncBatchedStore(BaseStore): async def _run( - aqueue: dict[asyncio.Future, Union[ListOp, PutOp]], kv: BaseStore + aqueue: dict[asyncio.Future, Union[ListOp, PutOp]], store: BaseStore ) -> None: while True: await asyncio.sleep(0) @@ -46,7 +46,7 @@ async def _run( lists = {f: o for f, o in taken.items() if isinstance(o, ListOp)} if lists: try: - results = await kv.alist( + results = await store.alist( [p for op in lists.values() for p in op.prefixes] ) for fut, op in lists.items(): @@ -57,7 +57,7 @@ async def _run( puts = {f: o for f, o in taken.items() if isinstance(o, PutOp)} if puts: try: - await kv.aput([w for op in puts.values() for w in op.writes]) + await store.aput([w for op in puts.values() for w in op.writes]) for fut in puts: fut.set_result(None) except Exception as e: diff --git a/libs/langgraph/tests/test_kv.py b/libs/langgraph/tests/test_store.py similarity index 68% rename from libs/langgraph/tests/test_kv.py rename to libs/langgraph/tests/test_store.py index 0c2862b48..a9fd0d1d4 100644 --- a/libs/langgraph/tests/test_kv.py +++ b/libs/langgraph/tests/test_store.py @@ -7,11 +7,11 @@ from langgraph.store.base import BaseStore from langgraph.store.batch import AsyncBatchedStore -async def test_kv_async_batch(mocker: MockerFixture) -> None: +async def test_async_batch_store(mocker: MockerFixture) -> None: aget = mocker.stub() alist = mocker.stub() - class MockKV(BaseStore): + class MockStore(BaseStore): async def aget( self, pairs: list[tuple[str, str]] ) -> dict[tuple[str, str], dict[str, Any] | None]: @@ -22,21 +22,9 @@ async def test_kv_async_batch(mocker: MockerFixture) -> None: alist(prefixes) return {prefix: {prefix: 1} for prefix in prefixes} - store = AsyncBatchedStore(MockKV()) + store = AsyncBatchedStore(MockStore()) # concurrent calls are batched - results = await asyncio.gather( - store.aget([("a", "b")]), - store.aget([("c", "d")]), - ) - assert results == [ - {("a", "b"): 1}, - {("c", "d"): 1}, - ] - assert [c.args for c in aget.call_args_list] == [ - ([("a", "b"), ("c", "d")],), - ] - results = await asyncio.gather( store.alist(["a", "b"]), store.alist(["c", "d"]), From f037a2e9cb6ae5dc75f22c2f0e1138521c8dca84 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 09:39:58 -0700 Subject: [PATCH 045/287] Update docstring --- libs/langgraph/langgraph/pregel/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 5e058fd16..436ed5ed0 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -225,7 +225,7 @@ class Pregel( """Checkpointer used to save and load graph state. Defaults to None.""" store: Optional[BaseStore] = None - """Key-value store to use. Defaults to None.""" + """Memory store to use for SharedValues. Defaults to None.""" retry_policy: Optional[RetryPolicy] = None """Retry policy to use when running tasks. Set to None to disable.""" From 1af0367b3459beb19008dfd0463a81d6f5c37082 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 09:41:53 -0700 Subject: [PATCH 046/287] Add error message --- libs/langgraph/langgraph/pregel/__init__.py | 4 ++-- libs/langgraph/langgraph/pregel/loop.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 436ed5ed0..429226214 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -650,7 +650,7 @@ class Pregel( # apply to checkpoint and save assert not apply_writes( checkpoint, channels, [task], self.checkpointer.get_next_version - ) + ), "Can't write to SharedValues from update_state" checkpoint = create_checkpoint(checkpoint, channels, step + 1) # check interrupt before if tasks := should_interrupt( @@ -794,7 +794,7 @@ class Pregel( # apply to checkpoint and save assert not apply_writes( checkpoint, channels, [task], self.checkpointer.get_next_version - ) + ), "Can't write to SharedValues from update_state" checkpoint = create_checkpoint(checkpoint, channels, step + 1) # check interrupt before if tasks := should_interrupt( diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index a1eadb697..672d42049 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -332,13 +332,12 @@ class PregelLoop: manager=None, ) # apply input writes - mv_writes = apply_writes( + assert not apply_writes( self.checkpoint, self.channels, discard_tasks + [PregelTaskWrites(INPUT, input_writes, [])], self.checkpointer_get_next_version, - ) - assert not mv_writes + ), "Can't write to SharedValues in graph input" # save input checkpoint self._put_checkpoint({"source": "input", "writes": self.input}) else: From 2106b5e4a6bed75c284146458a07217f5e5bbf06 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 10:48:04 -0700 Subject: [PATCH 047/287] Lint --- libs/langgraph/tests/test_pregel.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 8cf6aafa8..57df91165 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -167,9 +167,6 @@ def test_graph_validation() -> None: class State(TypedDict): hello: str - shared_things: Annotated[ - dict[str, dict[str, Any]], SharedValue.on("assistant_id") - ] def node_a(state: State) -> State: # typo From 7fd4a9ed300fac144e0c07b66e898d02d84bb069 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 10:48:52 -0700 Subject: [PATCH 048/287] Lint --- libs/langgraph/tests/test_pregel_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index d7a532489..70ce65daa 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -565,7 +565,7 @@ async def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} assert app.output_schema.schema() == {"title": "LangGraphOutput", "type": "integer"} - assert await app.ainvoke(2, debug=True) == 3 + assert await app.ainvoke(2) == 3 assert await app.ainvoke(2, output_keys=["output"]) == {"output": 3} assert await gapp.ainvoke(2) == 3 From 4250ff92b8dbbaea3fb8fff31be10a25a9adaa00 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 11:25:37 -0700 Subject: [PATCH 049/287] Fix --- libs/langgraph/langgraph/managed/shared_value.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/langgraph/managed/shared_value.py b/libs/langgraph/langgraph/managed/shared_value.py index 7f647d006..7bb6e23b7 100644 --- a/libs/langgraph/langgraph/managed/shared_value.py +++ b/libs/langgraph/langgraph/managed/shared_value.py @@ -58,7 +58,7 @@ class SharedValue(WritableManagedValue[Value, Update]): with super().enter(config, **kwargs) as value: if value.store is not None: saved = value.store.list([value.ns]) - value.value = saved[value.ns] + value.value = saved[value.ns] or {} yield value @classmethod @@ -67,7 +67,7 @@ class SharedValue(WritableManagedValue[Value, Update]): async with super().aenter(config, **kwargs) as value: if value.store is not None: saved = await value.store.alist([value.ns]) - value.value = saved[value.ns] + value.value = saved[value.ns] or {} yield value def __init__( From 5fb2c2c6f804fc55961610483ed8ed95338d767d Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 11:27:04 -0700 Subject: [PATCH 050/287] Lint --- libs/langgraph/tests/test_store.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/tests/test_store.py b/libs/langgraph/tests/test_store.py index a9fd0d1d4..cd53ee407 100644 --- a/libs/langgraph/tests/test_store.py +++ b/libs/langgraph/tests/test_store.py @@ -1,5 +1,5 @@ import asyncio -from typing import Any +from typing import Any, Optional from pytest_mock import MockerFixture @@ -14,7 +14,7 @@ async def test_async_batch_store(mocker: MockerFixture) -> None: class MockStore(BaseStore): async def aget( self, pairs: list[tuple[str, str]] - ) -> dict[tuple[str, str], dict[str, Any] | None]: + ) -> dict[tuple[str, str], Optional[dict[str, Any]]]: aget(pairs) return {pair: 1 for pair in pairs} From 2f41b2891b59f5729b5d604a828e8f143b3d7f36 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 11:33:54 -0700 Subject: [PATCH 051/287] lib0.2.7 --- 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 4dff54572..9bf6d068b 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.6" +version = "0.2.7" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From 14976d4c56c6516004769831219099483612121c Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 12:08:58 -0700 Subject: [PATCH 052/287] Interrupts shouldn't be retried (#1413) * Interrupts shouldn't be retried * Add async test * Lint --- libs/langgraph/langgraph/pregel/retry.py | 7 +++ libs/langgraph/tests/test_pregel.py | 7 ++- libs/langgraph/tests/test_pregel_async.py | 76 +++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/retry.py b/libs/langgraph/langgraph/pregel/retry.py index 88940b584..486584809 100644 --- a/libs/langgraph/langgraph/pregel/retry.py +++ b/libs/langgraph/langgraph/pregel/retry.py @@ -4,6 +4,7 @@ import random import time from typing import Optional +from langgraph.errors import GraphInterrupt from langgraph.pregel.types import PregelExecutableTask, RetryPolicy logger = logging.getLogger(__name__) @@ -25,6 +26,9 @@ def run_with_retry( task.proc.invoke(task.input, task.config) # if successful, end break + except GraphInterrupt: + # if interrupted, end + raise except Exception as exc: if retry_policy is None: raise @@ -75,6 +79,9 @@ async def arun_with_retry( await task.proc.ainvoke(task.input, task.config) # if successful, end break + except GraphInterrupt: + # if interrupted, end + raise except Exception as exc: if retry_policy is None: raise diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 57df91165..070c59476 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -6139,13 +6139,17 @@ def test_dynamic_interrupt(snapshot: SnapshotAssertion) -> None: my_key: Annotated[str, operator.add] market: str + tool_two_node_count = 0 + def tool_two_node(s: State) -> State: + nonlocal tool_two_node_count + tool_two_node_count += 1 if s["market"] == "DE": raise NodeInterrupt("Just because...") return {"my_key": " all good"} tool_two_graph = StateGraph(State) - tool_two_graph.add_node("tool_two", tool_two_node) + tool_two_graph.add_node("tool_two", tool_two_node, retry=RetryPolicy()) tool_two_graph.add_edge(START, "tool_two") tool_two = tool_two_graph.compile() @@ -6153,6 +6157,7 @@ def test_dynamic_interrupt(snapshot: SnapshotAssertion) -> None: "my_key": "value", "market": "DE", } + assert tool_two_node_count == 1, "interrupts aren't retried" assert tool_two.invoke({"my_key": "value", "market": "US"}) == { "my_key": "value all good", "market": "US", diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 70ce65daa..ebbfc7b29 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -206,6 +206,82 @@ async def test_node_cancellation_on_other_node_exception() -> None: assert inner_task_cancelled +async def test_dynamic_interrupt(snapshot: SnapshotAssertion) -> None: + class State(TypedDict): + my_key: Annotated[str, operator.add] + market: str + + tool_two_node_count = 0 + + async def tool_two_node(s: State) -> State: + nonlocal tool_two_node_count + tool_two_node_count += 1 + if s["market"] == "DE": + raise NodeInterrupt("Just because...") + return {"my_key": " all good"} + + tool_two_graph = StateGraph(State) + tool_two_graph.add_node("tool_two", tool_two_node, retry=RetryPolicy()) + tool_two_graph.add_edge(START, "tool_two") + tool_two = tool_two_graph.compile() + + assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}) == { + "my_key": "value", + "market": "DE", + } + assert tool_two_node_count == 1, "interrupts aren't retried" + assert await tool_two.ainvoke({"my_key": "value", "market": "US"}) == { + "my_key": "value all good", + "market": "US", + } + + async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: + tool_two = tool_two_graph.compile(checkpointer=saver) + + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + await tool_two.ainvoke({"my_key": "value", "market": "DE"}) + + thread1 = {"configurable": {"thread_id": "1"}} + # stop when about to enter node + assert await tool_two.ainvoke( + {"my_key": "value ⛰️", "market": "DE"}, thread1 + ) == { + "my_key": "value ⛰️", + "market": "DE", + } + assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ + { + "source": "loop", + "step": 0, + "writes": None, + }, + { + "source": "input", + "step": -1, + "writes": {"my_key": "value ⛰️", "market": "DE"}, + }, + ] + tup = await tool_two.checkpointer.aget_tuple(thread1) + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value ⛰️", "market": "DE"}, + next=("tool_two",), + tasks=( + PregelTask( + AnyStr(), + "tool_two", + interrupts=(Interrupt("during", "Just because..."),), + ), + ), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={"source": "loop", "step": 0, "writes": None}, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread1, limit=2) + ][-1].config, + ) + + @pytest.mark.parametrize( "checkpointer_name", ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], From 14ec51601c174a466f5d044e480961869f28c2a0 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 12:09:11 -0700 Subject: [PATCH 053/287] lib0.2.8 --- 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 9bf6d068b..5ba045061 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.7" +version = "0.2.8" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From f51e7ea9a407d35e7853e1182878eeec1a997d5e Mon Sep 17 00:00:00 2001 From: vbarda Date: Wed, 21 Aug 2024 15:38:30 -0400 Subject: [PATCH 054/287] pass pending writes in checkpointers --- .../langgraph/checkpoint/postgres/__init__.py | 1 + .../langgraph/checkpoint/postgres/aio.py | 1 + .../langgraph/checkpoint/sqlite/__init__.py | 16 +++++++++++++++- .../langgraph/checkpoint/sqlite/aio.py | 12 ++++++++++++ .../langgraph/checkpoint/memory/__init__.py | 4 ++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py index 02d5880a0..1da25b35f 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py @@ -150,6 +150,7 @@ class PostgresSaver(BasePostgresSaver): } if value["parent_checkpoint_id"] else None, + self._load_writes(value["pending_writes"]), ) def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py index 7ddb81237..7be1f36dc 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py @@ -135,6 +135,7 @@ class AsyncPostgresSaver(BasePostgresSaver): } if value["parent_checkpoint_id"] else None, + await asyncio.to_thread(self._load_writes, value["pending_writes"]), ) async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py index 9bf4139ad..d67454034 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py @@ -318,7 +318,9 @@ class SqliteSaver(BaseCheckpointSaver): ORDER BY checkpoint_id DESC""" if limit: query += f" LIMIT {limit}" - with self.cursor(transaction=False) as cur: + with self.cursor(transaction=False) as cur, self.cursor( + transaction=False + ) as writes_cur: cur.execute(query, param_values) for ( thread_id, @@ -329,6 +331,14 @@ class SqliteSaver(BaseCheckpointSaver): checkpoint, metadata, ) in cur: + writes_cur.execute( + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?", + ( + thread_id, + checkpoint_ns, + checkpoint_id, + ), + ) yield CheckpointTuple( { "configurable": { @@ -350,6 +360,10 @@ class SqliteSaver(BaseCheckpointSaver): if parent_checkpoint_id else None ), + [ + (task_id, channel, self.serde.loads_typed((type, value))) + for task_id, channel, type, value in writes_cur + ], ) def put( diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py index 56d14f613..364adf063 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py @@ -346,6 +346,14 @@ class AsyncSqliteSaver(BaseCheckpointSaver): checkpoint, metadata, ) in cursor: + writes_cur = await self.conn.execute( + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?", + ( + thread_id, + checkpoint_ns, + checkpoint_id, + ), + ) yield CheckpointTuple( { "configurable": { @@ -367,6 +375,10 @@ class AsyncSqliteSaver(BaseCheckpointSaver): if parent_checkpoint_id else None ), + [ + (task_id, channel, self.serde.loads_typed((type, value))) + async for task_id, channel, type, value in writes_cur + ], ) async def aput( diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 6cc1a3b14..6b3714f05 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -206,6 +206,7 @@ class MemorySaver( elif limit is not None: limit -= 1 + writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)] yield CheckpointTuple( config={ "configurable": { @@ -215,6 +216,9 @@ class MemorySaver( } }, checkpoint=self.serde.loads_typed(checkpoint), + pending_writes=[ + (id, c, self.serde.loads_typed(v)) for id, c, v in writes + ], metadata=metadata, parent_config={ "configurable": { From e76f4cc434f0d3a631222db2c4bdc00a77a9af03 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 13:30:18 -0700 Subject: [PATCH 055/287] Combine channel and managed values manager --- libs/langgraph/langgraph/channels/manager.py | 39 ------- libs/langgraph/langgraph/graph/state.py | 25 ++--- libs/langgraph/langgraph/managed/base.py | 50 +-------- libs/langgraph/langgraph/pregel/__init__.py | 108 +++++++------------ libs/langgraph/langgraph/pregel/algo.py | 23 ++-- libs/langgraph/langgraph/pregel/loop.py | 79 ++++++++------ libs/langgraph/langgraph/pregel/manager.py | 104 ++++++++++++++++++ libs/langgraph/langgraph/pregel/read.py | 3 +- libs/langgraph/pyproject.toml | 2 +- libs/langgraph/tests/test_algo.py | 7 +- 10 files changed, 213 insertions(+), 227 deletions(-) delete mode 100644 libs/langgraph/langgraph/channels/manager.py create mode 100644 libs/langgraph/langgraph/pregel/manager.py diff --git a/libs/langgraph/langgraph/channels/manager.py b/libs/langgraph/langgraph/channels/manager.py deleted file mode 100644 index 9a7c8d87e..000000000 --- a/libs/langgraph/langgraph/channels/manager.py +++ /dev/null @@ -1,39 +0,0 @@ -from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager -from typing import AsyncGenerator, Generator, Mapping - -from langchain_core.runnables import RunnableConfig - -from langgraph.channels.base import BaseChannel -from langgraph.checkpoint.base import Checkpoint - - -@contextmanager -def ChannelsManager( - channels: Mapping[str, BaseChannel], - checkpoint: Checkpoint, - config: RunnableConfig, -) -> Generator[Mapping[str, BaseChannel], None, None]: - """Manage channels for the lifetime of a Pregel invocation (multiple steps).""" - with ExitStack() as stack: - yield { - k: stack.enter_context( - v.from_checkpoint(checkpoint["channel_values"].get(k), config) - ) - for k, v in channels.items() - } - - -@asynccontextmanager -async def AsyncChannelsManager( - channels: Mapping[str, BaseChannel], - checkpoint: Checkpoint, - config: RunnableConfig, -) -> AsyncGenerator[Mapping[str, BaseChannel], None]: - """Manage channels for the lifetime of a Pregel invocation (multiple steps).""" - async with AsyncExitStack() as stack: - yield { - k: await stack.enter_async_context( - v.afrom_checkpoint(checkpoint["channel_values"].get(k), config) - ) - for k, v in channels.items() - } diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 8c85b7760..d846bc8d3 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -44,7 +44,7 @@ from langgraph.managed.base import ( ChannelKeyPlaceholder, ChannelTypePlaceholder, ConfiguredManagedValue, - ManagedValue, + ManagedValueSpec, is_managed_value, is_writable_managed_value, ) @@ -129,8 +129,8 @@ class StateGraph(Graph): nodes: dict[str, StateNodeSpec] channels: dict[str, BaseChannel] - managed: dict[str, Type[ManagedValue]] - schemas: dict[Type[Any], dict[str, Union[BaseChannel, Type[ManagedValue]]]] + managed: dict[str, ManagedValueSpec] + schemas: dict[Type[Any], dict[str, Union[BaseChannel, ManagedValueSpec]]] def __init__( self, @@ -442,7 +442,11 @@ class StateGraph(Graph): builder=self, config_type=self.config_schema, nodes={}, - channels={**self.channels, START: EphemeralValue(self.input)}, + channels={ + **self.channels, + **self.managed, + START: EphemeralValue(self.input), + }, input_channels=START, stream_mode="updates", output_channels=output_channels, @@ -497,7 +501,7 @@ class CompiledStateGraph(CompiledGraph): **{ k: (self.channels[k].UpdateType, None) for k in self.builder.schemas[self.builder.input] - if k in self.channels + if isinstance(self.channels[k], BaseChannel) and not isinstance(self.channels[k], Context) }, ) @@ -572,10 +576,7 @@ class CompiledStateGraph(CompiledGraph): ) else: input_schema = node.input if node else self.builder.schema - input_values = { - k: v if is_managed_value(v) else k - for k, v in self.builder.schemas[input_schema].items() - } + input_values = {k: k for k in self.builder.schemas[input_schema]} is_single_input = len(input_values) == 1 and "__root__" in input_values self.channels[key] = EphemeralValue(Any, guard=False) @@ -694,7 +695,7 @@ def _coerce_state(schema: Type[Any], input: dict[str, Any]) -> dict[str, Any]: def _get_channels( schema: Type[dict], -) -> tuple[dict[str, BaseChannel], dict[str, Type[ManagedValue]]]: +) -> tuple[dict[str, BaseChannel], dict[str, ManagedValueSpec]]: if not hasattr(schema, "__annotations__"): return {"__root__": _get_channel("__root__", schema, allow_managed=False)}, {} @@ -711,7 +712,7 @@ def _get_channels( def _get_channel( name: str, annotation: Any, *, allow_managed: bool = True -) -> Union[BaseChannel, Type[ManagedValue]]: +) -> Union[BaseChannel, ManagedValueSpec]: if manager := _is_field_managed_value(name, annotation): if allow_managed: return manager @@ -751,7 +752,7 @@ def _is_field_binop(typ: Type[Any]) -> Optional[BinaryOperatorAggregate]: return None -def _is_field_managed_value(name: str, typ: Type[Any]) -> Optional[Type[ManagedValue]]: +def _is_field_managed_value(name: str, typ: Type[Any]) -> Optional[ManagedValueSpec]: if hasattr(typ, "__metadata__"): meta = typ.__metadata__ if len(meta) >= 1: diff --git a/libs/langgraph/langgraph/managed/base.py b/libs/langgraph/langgraph/managed/base.py index 8dbf5c30c..bebca1be2 100644 --- a/libs/langgraph/langgraph/managed/base.py +++ b/libs/langgraph/langgraph/managed/base.py @@ -1,12 +1,9 @@ -import asyncio from abc import ABC, abstractmethod -from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager +from contextlib import asynccontextmanager, contextmanager from inspect import isclass from typing import ( Any, - AsyncGenerator, AsyncIterator, - Generator, Generic, Iterator, NamedTuple, @@ -104,50 +101,5 @@ def is_writable_managed_value(value: Any) -> TypeGuard[Type[WritableManagedValue ) -@contextmanager -def ManagedValuesManager( - values: dict[str, ManagedValueSpec], - config: RunnableConfig, -) -> Generator[ManagedValueMapping, None, None]: - if values: - with ExitStack() as stack: - yield { - key: stack.enter_context( - value.cls.enter(config, **value.kwargs) - if isinstance(value, ConfiguredManagedValue) - else value.enter(config) - ) - for key, value in values.items() - } - else: - yield {} - - -@asynccontextmanager -async def AsyncManagedValuesManager( - values: dict[str, ManagedValueSpec], - config: RunnableConfig, -) -> AsyncGenerator[ManagedValueMapping, None]: - if values: - async with AsyncExitStack() as stack: - # create enter tasks with reference to spec - tasks = { - asyncio.create_task( - stack.enter_async_context( - value.cls.aenter(config, **value.kwargs) - if isinstance(value, ConfiguredManagedValue) - else value.aenter(config) - ) - ): key - for key, value in values.items() - } - # wait for all enter tasks - done, _ = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) - # build mapping from spec to result - yield {tasks[task]: task.result() for task in done} - else: - yield {} - - ChannelKeyPlaceholder = object() ChannelTypePlaceholder = object() diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 429226214..be04d187d 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -52,11 +52,6 @@ from langgraph.channels.base import ( BaseChannel, ) from langgraph.channels.context import Context -from langgraph.channels.last_value import LastValue -from langgraph.channels.manager import ( - AsyncChannelsManager, - ChannelsManager, -) from langgraph.checkpoint.base import ( BaseCheckpointSaver, copy_checkpoint, @@ -73,12 +68,7 @@ from langgraph.constants import ( Interrupt, ) from langgraph.errors import GraphInterrupt, GraphRecursionError, InvalidUpdateError -from langgraph.managed.base import ( - AsyncManagedValuesManager, - ManagedValuesManager, - ManagedValueSpec, - is_managed_value, -) +from langgraph.managed.base import ManagedValueSpec from langgraph.pregel.algo import ( apply_writes, local_read, @@ -97,6 +87,7 @@ from langgraph.pregel.io import ( read_channels, ) from langgraph.pregel.loop import AsyncPregelLoop, SyncPregelLoop +from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager from langgraph.pregel.read import PregelNode from langgraph.pregel.retry import RetryPolicy, arun_with_retry, run_with_retry from langgraph.pregel.types import ( @@ -197,7 +188,9 @@ class Pregel( ): nodes: Mapping[str, PregelNode] - channels: Mapping[str, BaseChannel] = Field(default_factory=dict) + channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]] = Field( + default_factory=dict + ) auto_validate: bool = True @@ -350,16 +343,6 @@ class Pregel( k for k in self.channels if not isinstance(self.channels[k], Context) ] - @property - def managed_values_dict(self) -> dict[str, ManagedValueSpec]: - return { - k: v - for node in self.nodes.values() - if isinstance(node.channels, dict) - for k, v in node.channels.items() - if is_managed_value(v) - } - def get_state(self, config: RunnableConfig) -> StateSnapshot: """Get the current state of the graph.""" if not self.checkpointer: @@ -368,16 +351,10 @@ class Pregel( saved = self.checkpointer.get_tuple(config) checkpoint = saved.checkpoint if saved else empty_checkpoint() config = saved.config if saved else config - with ChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() - }, - checkpoint, - config, - ) as channels, ManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: + with ChannelsManager(self.channels, checkpoint, config, skip_context=True) as ( + channels, + managed, + ): next_tasks = prepare_next_tasks( checkpoint, self.nodes, @@ -408,15 +385,8 @@ class Pregel( config = saved.config if saved else config async with AsyncChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() - }, - checkpoint, - config, - ) as channels, AsyncManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: + self.channels, checkpoint, config, skip_context=True + ) as (channels, managed): next_tasks = prepare_next_tasks( checkpoint, self.nodes, @@ -460,15 +430,8 @@ class Pregel( pending_writes, ) in self.checkpointer.list(config, before=before, limit=limit, filter=filter): with ChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() - }, - checkpoint, - config, - ) as channels, ManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: + self.channels, checkpoint, config, skip_context=True + ) as (channels, managed): next_tasks = prepare_next_tasks( checkpoint, self.nodes, @@ -512,15 +475,8 @@ class Pregel( pending_writes, ) in self.checkpointer.alist(config, before=before, limit=limit, filter=filter): async with AsyncChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in self.channels.items() - }, - checkpoint, - config, - ) as channels, AsyncManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: + self.channels, checkpoint, config, skip_context=True + ) as (channels, managed): next_tasks = prepare_next_tasks( checkpoint, self.nodes, @@ -613,11 +569,10 @@ class Pregel( if as_node not in self.nodes: raise InvalidUpdateError(f"Node {as_node} does not exist") # update channels - with ChannelsManager( - self.channels, checkpoint, config - ) as channels, ManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: + with ChannelsManager(self.channels, checkpoint, config) as ( + channels, + managed, + ): # create task to run all writers of the chosen node writers = self.nodes[as_node].get_writers() if not writers: @@ -757,11 +712,10 @@ class Pregel( if as_node not in self.nodes: raise InvalidUpdateError(f"Node {as_node} does not exist") # update channels, acting as the chosen node - async with AsyncChannelsManager( - self.channels, checkpoint, config - ) as channels, AsyncManagedValuesManager( - self.managed_values_dict, ensure_config(config) - ) as managed: + async with AsyncChannelsManager(self.channels, checkpoint, config) as ( + channels, + managed, + ): # create task to run all writers of the chosen node writers = self.nodes[as_node].get_writers() if not writers: @@ -998,7 +952,13 @@ class Pregel( ) with SyncPregelLoop( - input, config=config, checkpointer=checkpointer, graph=self + input, + config=config, + store=self.store, + checkpointer=checkpointer, + graph=self, + nodes=self.nodes, + specs=self.channels, ) as loop: # Similarly to Bulk Synchronous Parallel / Pregel model # computation proceeds in steps, while there are channel updates @@ -1248,7 +1208,13 @@ class Pregel( debug=debug, ) async with AsyncPregelLoop( - input, config=config, checkpointer=checkpointer, graph=self + input, + config=config, + store=self.store, + checkpointer=checkpointer, + graph=self, + nodes=self.nodes, + specs=self.channels, ) as loop: aioloop = asyncio.get_event_loop() # Similarly to Bulk Synchronous Parallel / Pregel model diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 37f168bf7..28a8dcd5d 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -25,7 +25,6 @@ from langchain_core.runnables.config import ( from langgraph.channels.base import BaseChannel from langgraph.channels.context import Context -from langgraph.channels.manager import ChannelsManager from langgraph.checkpoint.base import ( BaseCheckpointSaver, Checkpoint, @@ -46,9 +45,10 @@ from langgraph.constants import ( Send, ) from langgraph.errors import EmptyChannelError, InvalidUpdateError -from langgraph.managed.base import ManagedValueMapping, is_managed_value +from langgraph.managed.base import ManagedValueMapping from langgraph.pregel.io import read_channel, read_channels from langgraph.pregel.log import logger +from langgraph.pregel.manager import ChannelsManager from langgraph.pregel.read import PregelNode from langgraph.pregel.types import All, PregelExecutableTask, PregelTask @@ -105,11 +105,10 @@ def local_read( if fresh: new_checkpoint = create_checkpoint(copy_checkpoint(checkpoint), channels, -1) context_channels = {k: v for k, v in channels.items() if isinstance(v, Context)} - with ChannelsManager( - {k: v for k, v in channels.items() if k not in context_channels}, - new_checkpoint, - config, - ) as channels: + with ChannelsManager(channels, new_checkpoint, config, skip_context=True) as ( + channels, + _, + ): all_channels = {**channels, **context_channels} apply_writes(new_checkpoint, all_channels, [task], None) return read_channels(all_channels, select) @@ -470,16 +469,10 @@ def _proc_input( chan, catch=chan not in proc.triggers, ) + if chan in channels + else managed[k](step) for k, chan in proc.channels.items() - if isinstance(chan, str) } - - managed_values = {} - for key, chan in proc.channels.items(): - if is_managed_value(chan): - managed_values[key] = managed[key](step) - - val.update(managed_values) except EmptyChannelError: return elif isinstance(proc.channels, list): diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 672d42049..bc19b39cf 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -22,14 +22,10 @@ from typing import ( ) from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager -from langchain_core.runnables import RunnableConfig, patch_config +from langchain_core.runnables import RunnableConfig from typing_extensions import Self from langgraph.channels.base import BaseChannel -from langgraph.channels.manager import ( - AsyncChannelsManager, - ChannelsManager, -) from langgraph.checkpoint.base import ( BaseCheckpointSaver, Checkpoint, @@ -43,7 +39,6 @@ from langgraph.checkpoint.base import ( from langgraph.constants import ( CONFIG_KEY_READ, CONFIG_KEY_RESUMING, - CONFIG_KEY_STORE, ERROR, INPUT, INTERRUPT, @@ -51,9 +46,8 @@ from langgraph.constants import ( ) from langgraph.errors import EmptyInputError, GraphInterrupt from langgraph.managed.base import ( - AsyncManagedValuesManager, ManagedValueMapping, - ManagedValuesManager, + ManagedValueSpec, WritableManagedValue, ) from langgraph.pregel.algo import ( @@ -70,6 +64,8 @@ from langgraph.pregel.executor import ( Submit, ) from langgraph.pregel.io import map_input, map_output_updates, map_output_values, single +from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager +from langgraph.pregel.read import PregelNode from langgraph.pregel.types import PregelExecutableTask from langgraph.pregel.utils import get_new_channel_versions from langgraph.store.base import BaseStore @@ -88,7 +84,12 @@ EMPTY_SEQ = () class PregelLoop: input: Optional[Any] config: RunnableConfig + store: Optional[BaseStore] checkpointer: Optional[BaseCheckpointSaver] + nodes: Mapping[str, PregelNode] + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]] + is_nested: bool + checkpointer_get_next_version: Callable[[Optional[V]], V] checkpointer_put_writes: Optional[ Callable[[RunnableConfig, Sequence[tuple[str, Any]], str], Any] @@ -123,7 +124,6 @@ class PregelLoop: ] tasks: Sequence[PregelExecutableTask] stream: deque[Tuple[str, Any]] - is_nested: bool # public @@ -132,16 +132,20 @@ class PregelLoop: input: Optional[Any], *, config: RunnableConfig, + store: Optional[BaseStore], checkpointer: Optional[BaseCheckpointSaver], + nodes: Mapping[str, PregelNode], + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], graph: "Pregel", ) -> None: self.stream = deque() self.input = input self.config = config + self.store = store self.checkpointer = checkpointer self.graph = graph - # TODO if managed values no longer needs graph we can replace with - # managed_specs, channel_specs + self.nodes = nodes + self.specs = specs self.is_nested = CONFIG_KEY_READ in self.config.get("configurable", {}) def mark_tasks_scheduled(self, tasks: Sequence[PregelExecutableTask]) -> None: @@ -235,7 +239,7 @@ class PregelLoop: # prepare next tasks self.tasks = prepare_next_tasks( self.checkpoint, - self.graph.nodes, + self.nodes, self.channels, self.managed, self.config, @@ -323,7 +327,7 @@ class PregelLoop: # discard any unfinished tasks from previous checkpoint discard_tasks = prepare_next_tasks( self.checkpoint, - self.graph.nodes, + self.nodes, self.channels, self.managed, self.config, @@ -422,11 +426,21 @@ class SyncPregelLoop(PregelLoop, ContextManager): input: Optional[Any], *, config: RunnableConfig, + store: Optional[BaseStore], checkpointer: Optional[BaseCheckpointSaver], + nodes: Mapping[str, PregelNode], + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], graph: "Pregel", ) -> None: - super().__init__(input, config=config, checkpointer=checkpointer, graph=graph) - self.store = graph.store + super().__init__( + input, + config=config, + checkpointer=checkpointer, + graph=graph, + store=store, + nodes=nodes, + specs=specs, + ) self.stack = ExitStack() if checkpointer: self.checkpointer_get_next_version = checkpointer.get_next_version @@ -472,14 +486,8 @@ class SyncPregelLoop(PregelLoop, ContextManager): self.checkpoint_pending_writes = saved.pending_writes or [] self.submit = self.stack.enter_context(BackgroundExecutor(self.config)) - self.channels = self.stack.enter_context( - ChannelsManager(self.graph.channels, self.checkpoint, self.config) - ) - self.managed = self.stack.enter_context( - ManagedValuesManager( - self.graph.managed_values_dict, - patch_config(self.config, configurable={CONFIG_KEY_STORE: self.store}), - ) + self.channels, self.managed = self.stack.enter_context( + ChannelsManager(self.specs, self.checkpoint, self.config, self.store) ) self.stack.push(self._suppress_interrupt) self.status = "pending" @@ -506,11 +514,22 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): input: Optional[Any], *, config: RunnableConfig, + store: Optional[BaseStore], checkpointer: Optional[BaseCheckpointSaver], + nodes: Mapping[str, PregelNode], + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], graph: "Pregel", ) -> None: - super().__init__(input, config=config, checkpointer=checkpointer, graph=graph) - self.store = AsyncBatchedStore(graph.store) if graph.store else None + super().__init__( + input, + config=config, + checkpointer=checkpointer, + graph=graph, + store=store, + nodes=nodes, + specs=specs, + ) + self.store = AsyncBatchedStore(self.store) if self.store else None self.stack = AsyncExitStack() if checkpointer: self.checkpointer_get_next_version = checkpointer.get_next_version @@ -560,14 +579,8 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): self.checkpoint_pending_writes = saved.pending_writes or [] self.submit = await self.stack.enter_async_context(AsyncBackgroundExecutor()) - self.channels = await self.stack.enter_async_context( - AsyncChannelsManager(self.graph.channels, self.checkpoint, self.config) - ) - self.managed = await self.stack.enter_async_context( - AsyncManagedValuesManager( - self.graph.managed_values_dict, - patch_config(self.config, configurable={CONFIG_KEY_STORE: self.store}), - ) + self.channels, self.managed = await self.stack.enter_async_context( + AsyncChannelsManager(self.specs, self.checkpoint, self.config, self.store) ) self.stack.push(self._suppress_interrupt) self.status = "pending" diff --git a/libs/langgraph/langgraph/pregel/manager.py b/libs/langgraph/langgraph/pregel/manager.py new file mode 100644 index 000000000..437019113 --- /dev/null +++ b/libs/langgraph/langgraph/pregel/manager.py @@ -0,0 +1,104 @@ +import asyncio +from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager +from typing import AsyncIterator, Iterator, Mapping, Optional, Union + +from langchain_core.runnables import RunnableConfig, patch_config + +from langgraph.channels.base import BaseChannel +from langgraph.channels.context import Context +from langgraph.channels.last_value import LastValue +from langgraph.checkpoint.base import Checkpoint +from langgraph.constants import CONFIG_KEY_STORE +from langgraph.managed.base import ( + ConfiguredManagedValue, + ManagedValueMapping, + ManagedValueSpec, +) +from langgraph.store.base import BaseStore + + +@contextmanager +def ChannelsManager( + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], + checkpoint: Checkpoint, + config: RunnableConfig, + store: Optional[BaseStore] = None, + *, + skip_context: bool = False, +) -> Iterator[tuple[Mapping[str, BaseChannel], ManagedValueMapping]]: + """Manage channels for the lifetime of a Pregel invocation (multiple steps).""" + config_for_managed = patch_config(config, configurable={CONFIG_KEY_STORE: store}) + channel_specs: Mapping[str, BaseChannel] = {} + managed_specs: Mapping[str, ManagedValueSpec] = {} + for k, v in specs.items(): + if skip_context and isinstance(v, Context): + channel_specs[k] = LastValue(None) + elif isinstance(v, BaseChannel): + channel_specs[k] = v + else: + managed_specs[k] = v + with ExitStack() as stack: + yield ( + { + k: stack.enter_context( + v.from_checkpoint(checkpoint["channel_values"].get(k), config) + ) + for k, v in channel_specs.items() + }, + { + key: stack.enter_context( + value.cls.enter(config_for_managed, **value.kwargs) + if isinstance(value, ConfiguredManagedValue) + else value.enter(config_for_managed) + ) + for key, value in managed_specs.items() + }, + ) + + +@asynccontextmanager +async def AsyncChannelsManager( + specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], + checkpoint: Checkpoint, + config: RunnableConfig, + store: Optional[BaseStore] = None, + *, + skip_context: bool = False, +) -> AsyncIterator[Mapping[str, BaseChannel]]: + """Manage channels for the lifetime of a Pregel invocation (multiple steps).""" + config_for_managed = patch_config(config, configurable={CONFIG_KEY_STORE: store}) + channel_specs: Mapping[str, BaseChannel] = {} + managed_specs: Mapping[str, ManagedValueSpec] = {} + for k, v in specs.items(): + if skip_context and isinstance(v, Context): + channel_specs[k] = LastValue(None) + elif isinstance(v, BaseChannel): + channel_specs[k] = v + else: + managed_specs[k] = v + async with AsyncExitStack() as stack: + # managed: create enter tasks with reference to spec, await them + if tasks := { + asyncio.create_task( + stack.enter_async_context( + value.cls.aenter(config_for_managed, **value.kwargs) + if isinstance(value, ConfiguredManagedValue) + else value.aenter(config_for_managed) + ) + ): key + for key, value in managed_specs.items() + }: + done, _ = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED) + else: + done = set() + yield ( + # channels: enter each channel with checkpoint + { + k: await stack.enter_async_context( + v.afrom_checkpoint(checkpoint["channel_values"].get(k), config) + ) + for k, v in channel_specs.items() + }, + # managed: build mapping from spec to result + {tasks[task]: task.result() for task in done}, + ) diff --git a/libs/langgraph/langgraph/pregel/read.py b/libs/langgraph/langgraph/pregel/read.py index 6e492e2ac..b5c971e4a 100644 --- a/libs/langgraph/langgraph/pregel/read.py +++ b/libs/langgraph/langgraph/pregel/read.py @@ -15,7 +15,6 @@ from langchain_core.runnables.config import merge_configs from langchain_core.runnables.utils import ConfigurableFieldSpec from langgraph.constants import CONFIG_KEY_READ -from langgraph.managed.base import ManagedValueSpec from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.write import ChannelWrite from langgraph.utils import RunnableCallable @@ -101,7 +100,7 @@ DEFAULT_BOUND: RunnablePassthrough = RunnablePassthrough() class PregelNode(RunnableBindingBase): - channels: Union[list[str], Mapping[str, Union[str, ManagedValueSpec]]] + channels: Union[list[str], Mapping[str, str]] triggers: list[str] = Field(default_factory=list) diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 5ba045061..93dc203f2 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -65,7 +65,7 @@ omit = ["tests/*"] [tool.pytest-watcher] now = true delay = 0.1 -runner_args = ["--ff", "-v", "-n", "auto", "--dist", "worksteal", "--snapshot-update", "--tb", "short"] +runner_args = ["--ff", "-v", "-x", "-n", "auto", "--dist", "worksteal", "--snapshot-update", "--tb", "short"] patterns = ["*.py"] [build-system] diff --git a/libs/langgraph/tests/test_algo.py b/libs/langgraph/tests/test_algo.py index cdf179c24..2102261ae 100644 --- a/libs/langgraph/tests/test_algo.py +++ b/libs/langgraph/tests/test_algo.py @@ -1,7 +1,6 @@ -from langgraph.channels.manager import ChannelsManager from langgraph.checkpoint.base import empty_checkpoint -from langgraph.managed.base import ManagedValuesManager from langgraph.pregel.algo import prepare_next_tasks +from langgraph.pregel.manager import ChannelsManager def test_prepare_next_tasks() -> None: @@ -9,9 +8,7 @@ def test_prepare_next_tasks() -> None: processes = {} checkpoint = empty_checkpoint() - with ManagedValuesManager({}, config) as managed, ChannelsManager( - {}, checkpoint, config - ) as channels: + with ChannelsManager({}, checkpoint, config) as (channels, managed): assert ( prepare_next_tasks( checkpoint, processes, channels, managed, config, 0, for_execution=False From 02697b5712c59d40ad39a8878d880043b0639a88 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 13:45:42 -0700 Subject: [PATCH 056/287] Remove graph arg to PregelLoop --- libs/langgraph/langgraph/pregel/__init__.py | 11 +++-- libs/langgraph/langgraph/pregel/loop.py | 34 ++++---------- libs/langgraph/tests/test_pregel.py | 52 ++++++++++++--------- libs/langgraph/tests/test_pregel_async.py | 52 ++++++++++++--------- 4 files changed, 75 insertions(+), 74 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index be04d187d..6ac6c737a 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -340,7 +340,10 @@ class Pregel( @property def stream_channels_asis(self) -> Union[str, Sequence[str]]: return self.stream_channels or [ - k for k in self.channels if not isinstance(self.channels[k], Context) + k + for k in self.channels + if isinstance(self.channels[k], BaseChannel) + and not isinstance(self.channels[k], Context) ] def get_state(self, config: RunnableConfig) -> StateSnapshot: @@ -956,7 +959,6 @@ class Pregel( config=config, store=self.store, checkpointer=checkpointer, - graph=self, nodes=self.nodes, specs=self.channels, ) as loop: @@ -966,7 +968,9 @@ class Pregel( # channels are guaranteed to be immutable for the duration of the step, # with channel updates applied only at the transition between steps while loop.tick( + input_keys=self.input_channels, output_keys=output_keys, + stream_keys=self.stream_channels_asis, interrupt_before=interrupt_before, interrupt_after=interrupt_after, manager=run_manager, @@ -1212,7 +1216,6 @@ class Pregel( config=config, store=self.store, checkpointer=checkpointer, - graph=self, nodes=self.nodes, specs=self.channels, ) as loop: @@ -1223,7 +1226,9 @@ class Pregel( # channels are guaranteed to be immutable for the duration of the step, # with channel updates applied only at the transition between steps while loop.tick( + input_keys=self.input_channels, output_keys=output_keys, + stream_keys=self.stream_channels_asis, interrupt_before=interrupt_before, interrupt_after=interrupt_after, manager=run_manager, diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index bc19b39cf..db165513b 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -4,7 +4,6 @@ from collections import deque from contextlib import AsyncExitStack, ExitStack from types import TracebackType from typing import ( - TYPE_CHECKING, Any, AsyncContextManager, Callable, @@ -71,10 +70,6 @@ from langgraph.pregel.utils import get_new_channel_versions from langgraph.store.base import BaseStore from langgraph.store.batch import AsyncBatchedStore -if TYPE_CHECKING: - from langgraph.pregel import Pregel - - V = TypeVar("V") INPUT_DONE = object() INPUT_RESUMING = object() @@ -105,8 +100,6 @@ class PregelLoop: Any, ] ] - graph: "Pregel" - store: Optional[BaseStore] submit: Submit channels: Mapping[str, BaseChannel] managed: ManagedValueMapping @@ -136,14 +129,12 @@ class PregelLoop: checkpointer: Optional[BaseCheckpointSaver], nodes: Mapping[str, PregelNode], specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], - graph: "Pregel", ) -> None: self.stream = deque() self.input = input self.config = config self.store = store self.checkpointer = checkpointer - self.graph = graph self.nodes = nodes self.specs = specs self.is_nested = CONFIG_KEY_READ in self.config.get("configurable", {}) @@ -175,7 +166,9 @@ class PregelLoop: def tick( self, *, + input_keys: Union[str, Sequence[str]], output_keys: Union[str, Sequence[str]] = EMPTY_SEQ, + stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ, interrupt_after: Sequence[str] = EMPTY_SEQ, interrupt_before: Sequence[str] = EMPTY_SEQ, manager: Union[None, AsyncParentRunManager, ParentRunManager] = None, @@ -187,7 +180,7 @@ class PregelLoop: raise RuntimeError("Cannot tick when status is no longer 'pending'") if self.input not in (INPUT_DONE, INPUT_RESUMING): - self._first() + self._first(input_keys=input_keys) elif all(task.writes for task in self.tasks): writes = [w for t in self.tasks for w in t.writes] # all tasks have finished @@ -211,11 +204,7 @@ class PregelLoop: self._put_checkpoint( { "source": "loop", - "writes": single( - map_output_updates(output_keys, self.tasks) - if self.graph.stream_mode == "updates" - else map_output_values(output_keys, writes, self.channels) - ), + "writes": single(map_output_updates(output_keys, self.tasks)), } ) # after execution, check if we should interrupt @@ -258,7 +247,7 @@ class PregelLoop: self.step - 1, # printing checkpoint for previous step self.checkpoint_config, self.channels, - self.graph.stream_channels_asis, + stream_keys, self.checkpoint_metadata, self.checkpoint, self.tasks, @@ -282,6 +271,7 @@ class PregelLoop: # if all tasks have finished, re-tick if all(task.writes for task in self.tasks): return self.tick( + input_keys=input_keys, output_keys=output_keys, interrupt_after=interrupt_after, interrupt_before=interrupt_before, @@ -306,7 +296,7 @@ class PregelLoop: # private - def _first(self) -> None: + def _first(self, *, input_keys: Union[str, Sequence[str]]) -> None: # resuming from previous checkpoint requires # - finding a previous checkpoint # - receiving None input (outer graph) or RESUMING flag (subgraph) @@ -323,7 +313,7 @@ class PregelLoop: version = self.checkpoint["channel_versions"][k] self.checkpoint["versions_seen"][INTERRUPT][k] = version # map inputs to channel updates - elif input_writes := deque(map_input(self.graph.input_channels, self.input)): + elif input_writes := deque(map_input(input_keys, self.input)): # discard any unfinished tasks from previous checkpoint discard_tasks = prepare_next_tasks( self.checkpoint, @@ -345,7 +335,7 @@ class PregelLoop: # save input checkpoint self._put_checkpoint({"source": "input", "writes": self.input}) else: - raise EmptyInputError(f"Received no input for {self.graph.input_channels}") + raise EmptyInputError(f"Received no input for {input_keys}") # done with input self.input = INPUT_RESUMING if is_resuming else INPUT_DONE @@ -430,13 +420,11 @@ class SyncPregelLoop(PregelLoop, ContextManager): checkpointer: Optional[BaseCheckpointSaver], nodes: Mapping[str, PregelNode], specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], - graph: "Pregel", ) -> None: super().__init__( input, config=config, checkpointer=checkpointer, - graph=graph, store=store, nodes=nodes, specs=specs, @@ -504,7 +492,6 @@ class SyncPregelLoop(PregelLoop, ContextManager): traceback: Optional[TracebackType], ) -> Optional[bool]: # unwind stack - del self.graph return self.stack.__exit__(exc_type, exc_value, traceback) @@ -518,13 +505,11 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): checkpointer: Optional[BaseCheckpointSaver], nodes: Mapping[str, PregelNode], specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], - graph: "Pregel", ) -> None: super().__init__( input, config=config, checkpointer=checkpointer, - graph=graph, store=store, nodes=nodes, specs=specs, @@ -598,7 +583,6 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): traceback: Optional[TracebackType], ) -> Optional[bool]: # unwind stack - del self.graph return await asyncio.shield( self.stack.__aexit__(exc_type, exc_value, traceback) ) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 070c59476..197dc6c61 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -657,7 +657,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 6, "writes": 5}, + metadata={"source": "loop", "step": 6, "writes": {"two": 5}}, created_at=AnyStr(), parent_config=history[1].config, ), @@ -672,7 +672,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 5, "writes": None}, + metadata={"source": "loop", "step": 5, "writes": {"one": None}}, created_at=AnyStr(), parent_config=history[2].config, ), @@ -702,7 +702,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 3, "writes": None}, + metadata={"source": "loop", "step": 3, "writes": {"one": None}}, created_at=AnyStr(), parent_config=history[4].config, ), @@ -732,7 +732,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 1, "writes": 4}, + metadata={"source": "loop", "step": 1, "writes": {"two": 4}}, created_at=AnyStr(), parent_config=history[6].config, ), @@ -747,7 +747,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"source": "loop", "step": 0, "writes": {"one": None}}, created_at=AnyStr(), parent_config=history[7].config, ), @@ -1995,12 +1995,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "step": 0, "writes": { "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } }, }, }, @@ -2206,12 +2208,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "step": 0, "writes": { "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } } }, }, @@ -2411,12 +2415,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "step": 0, "writes": { "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } } }, }, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index ebbfc7b29..e9987c601 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -876,7 +876,7 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 6, "writes": 5}, + metadata={"source": "loop", "step": 6, "writes": {"two": 5}}, created_at=AnyStr(), parent_config=history[1].config, ), @@ -891,7 +891,7 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 5, "writes": None}, + metadata={"source": "loop", "step": 5, "writes": {"one": None}}, created_at=AnyStr(), parent_config=history[2].config, ), @@ -921,7 +921,7 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 3, "writes": None}, + metadata={"source": "loop", "step": 3, "writes": {"one": None}}, created_at=AnyStr(), parent_config=history[4].config, ), @@ -951,7 +951,7 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 1, "writes": 4}, + metadata={"source": "loop", "step": 1, "writes": {"two": 4}}, created_at=AnyStr(), parent_config=history[6].config, ), @@ -966,7 +966,7 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"source": "loop", "step": 0, "writes": {"one": None}}, created_at=AnyStr(), parent_config=history[7].config, ), @@ -2290,12 +2290,14 @@ async def test_conditional_graph() -> None: "step": 0, "writes": { "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } } }, }, @@ -2516,12 +2518,14 @@ async def test_conditional_graph() -> None: "step": 0, "writes": { "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } } }, }, @@ -2748,12 +2752,14 @@ async def test_conditional_graph() -> None: "step": 0, "writes": { "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } } }, }, From 3a65f83ae1f08a66a84addb4e0a2f980acfd6bc9 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Wed, 21 Aug 2024 17:05:03 -0400 Subject: [PATCH 057/287] sdk-py: allow passing custom headers (#1416) --- libs/sdk-py/langgraph_sdk/client.py | 38 +++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index 13d5560db..ba8aa2f42 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -40,8 +40,14 @@ from langgraph_sdk.schema import ( logger = logging.getLogger(__name__) +RESERVED_HEADERS = ("x-api-key",) + + def get_client( - *, url: Optional[str] = None, api_key: Optional[str] = None + *, + url: Optional[str] = None, + api_key: Optional[str] = None, + headers: Optional[dict[str, str]] = None, ) -> LangGraphClient: """Get a LangGraphClient instance. @@ -53,6 +59,7 @@ def get_client( 2. LANGGRAPH_API_KEY 3. LANGSMITH_API_KEY 4. LANGCHAIN_API_KEY + headers: Optional custom headers """ transport: Optional[httpx.AsyncBaseTransport] = None if url is None: @@ -65,17 +72,12 @@ def get_client( url = "http://localhost:8123" if transport is None: transport = httpx.AsyncHTTPTransport(retries=5) - headers = { - "User-Agent": f"langgraph-sdk-py/{langgraph_sdk.__version__}", - } - api_key = _get_api_key(api_key) - if api_key: - headers["x-api-key"] = api_key + client = httpx.AsyncClient( base_url=url, transport=transport, timeout=httpx.Timeout(connect=5, read=60, write=60, pool=5), - headers=headers, + headers=_get_headers(api_key, headers), ) return LangGraphClient(client) @@ -1695,3 +1697,23 @@ def _get_api_key(api_key: Optional[str] = None) -> Optional[str]: if env := os.getenv(f"{prefix}_API_KEY"): return env.strip().strip('"').strip("'") return None # type: ignore + + +def _get_headers( + api_key: Optional[str], custom_headers: Optional[dict[str, str]] +) -> dict[str, str]: + """Combine api_key and custom user-provided headers.""" + custom_headers = custom_headers or {} + for header in RESERVED_HEADERS: + if header in custom_headers: + raise ValueError(f"Cannot set reserved header '{header}'") + + headers = { + "User-Agent": f"langgraph-sdk-py/{langgraph_sdk.__version__}", + **custom_headers, + } + api_key = _get_api_key(api_key) + if api_key: + headers["x-api-key"] = api_key + + return headers From 1907646bd472f2bf1ac99eb82ef9197e8269dbc8 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Wed, 21 Aug 2024 17:10:18 -0400 Subject: [PATCH 058/287] sdk-py: release 0.1.28 (#1417) --- libs/sdk-py/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml index 3c50594db..2755c3fc6 100644 --- a/libs/sdk-py/pyproject.toml +++ b/libs/sdk-py/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-sdk" -version = "0.1.27" +version = "0.1.28" description = "SDK for interacting with LangGraph API" authors = [] license = "MIT" From 47ed3d97e9819e7c353dea2817521256ef225ca1 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 14:25:00 -0700 Subject: [PATCH 059/287] Fix pending run when interrupt exception is used --- libs/langgraph/langgraph/pregel/__init__.py | 16 ++-- libs/langgraph/langgraph/pregel/loop.py | 30 +++++-- libs/langgraph/tests/fake_tracer.py | 91 +++++++++++++++++++++ libs/langgraph/tests/test_pregel.py | 12 ++- libs/langgraph/tests/test_pregel_async.py | 12 ++- 5 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 libs/langgraph/tests/fake_tracer.py diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 6ac6c737a..6e4d3e002 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -961,6 +961,7 @@ class Pregel( checkpointer=checkpointer, nodes=self.nodes, specs=self.channels, + output_keys=output_keys, ) as loop: # Similarly to Bulk Synchronous Parallel / Pregel model # computation proceeds in steps, while there are channel updates @@ -969,7 +970,6 @@ class Pregel( # with channel updates applied only at the transition between steps while loop.tick( input_keys=self.input_channels, - output_keys=output_keys, stream_keys=self.stream_channels_asis, interrupt_before=interrupt_before, interrupt_after=interrupt_after, @@ -1088,8 +1088,8 @@ class Pregel( "without hitting a stop condition. You can increase the " "limit by setting the `recursion_limit` config key." ) - # set final channel values as run output - run_manager.on_chain_end(read_channels(loop.channels, output_keys)) + # set final channel values as run output + run_manager.on_chain_end(loop.output) except BaseException as e: run_manager.on_chain_error(e) raise @@ -1218,6 +1218,7 @@ class Pregel( checkpointer=checkpointer, nodes=self.nodes, specs=self.channels, + output_keys=output_keys, ) as loop: aioloop = asyncio.get_event_loop() # Similarly to Bulk Synchronous Parallel / Pregel model @@ -1227,7 +1228,6 @@ class Pregel( # with channel updates applied only at the transition between steps while loop.tick( input_keys=self.input_channels, - output_keys=output_keys, stream_keys=self.stream_channels_asis, interrupt_before=interrupt_before, interrupt_after=interrupt_after, @@ -1349,13 +1349,9 @@ class Pregel( "without hitting a stop condition. You can increase the " "limit by setting the `recursion_limit` config key." ) - - # set final channel values as run output - await run_manager.on_chain_end( - read_channels(loop.channels, output_keys) - ) + # set final channel values as run output + await run_manager.on_chain_end(loop.output) except BaseException as e: - # TODO use on_chain_end if exc is GraphInterrupt await asyncio.shield(run_manager.on_chain_error(e)) raise diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index db165513b..aa256b2f7 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -62,7 +62,13 @@ from langgraph.pregel.executor import ( BackgroundExecutor, Submit, ) -from langgraph.pregel.io import map_input, map_output_updates, map_output_values, single +from langgraph.pregel.io import ( + map_input, + map_output_updates, + map_output_values, + read_channels, + single, +) from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager from langgraph.pregel.read import PregelNode from langgraph.pregel.types import PregelExecutableTask @@ -83,6 +89,7 @@ class PregelLoop: checkpointer: Optional[BaseCheckpointSaver] nodes: Mapping[str, PregelNode] specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]] + output_keys: Union[str, Sequence[str]] is_nested: bool checkpointer_get_next_version: Callable[[Optional[V]], V] @@ -117,6 +124,7 @@ class PregelLoop: ] tasks: Sequence[PregelExecutableTask] stream: deque[Tuple[str, Any]] + output: Union[None, dict[str, Any], Any] = None # public @@ -129,6 +137,7 @@ class PregelLoop: checkpointer: Optional[BaseCheckpointSaver], nodes: Mapping[str, PregelNode], specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], + output_keys: Union[str, Sequence[str]], ) -> None: self.stream = deque() self.input = input @@ -137,6 +146,7 @@ class PregelLoop: self.checkpointer = checkpointer self.nodes = nodes self.specs = specs + self.output_keys = output_keys self.is_nested = CONFIG_KEY_READ in self.config.get("configurable", {}) def mark_tasks_scheduled(self, tasks: Sequence[PregelExecutableTask]) -> None: @@ -167,7 +177,6 @@ class PregelLoop: self, *, input_keys: Union[str, Sequence[str]], - output_keys: Union[str, Sequence[str]] = EMPTY_SEQ, stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ, interrupt_after: Sequence[str] = EMPTY_SEQ, interrupt_before: Sequence[str] = EMPTY_SEQ, @@ -196,7 +205,7 @@ class PregelLoop: # produce values output self.stream.extend( ("values", v) - for v in map_output_values(output_keys, writes, self.channels) + for v in map_output_values(self.output_keys, writes, self.channels) ) # clear pending writes self.checkpoint_pending_writes.clear() @@ -204,7 +213,7 @@ class PregelLoop: self._put_checkpoint( { "source": "loop", - "writes": single(map_output_updates(output_keys, self.tasks)), + "writes": single(map_output_updates(self.output_keys, self.tasks)), } ) # after execution, check if we should interrupt @@ -272,7 +281,7 @@ class PregelLoop: if all(task.writes for task in self.tasks): return self.tick( input_keys=input_keys, - output_keys=output_keys, + stream_keys=stream_keys, interrupt_after=interrupt_after, interrupt_before=interrupt_before, manager=manager, @@ -406,7 +415,12 @@ class PregelLoop: exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> Optional[bool]: - if isinstance(exc_value, GraphInterrupt) and not self.is_nested: + suppress = isinstance(exc_value, GraphInterrupt) and not self.is_nested + if suppress or exc_type is None: + # save final output + self.output = read_channels(self.channels, self.output_keys) + if suppress: + # suppress interrupt return True @@ -420,6 +434,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): checkpointer: Optional[BaseCheckpointSaver], nodes: Mapping[str, PregelNode], specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], + output_keys: Union[str, Sequence[str]] = EMPTY_SEQ, ) -> None: super().__init__( input, @@ -428,6 +443,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): store=store, nodes=nodes, specs=specs, + output_keys=output_keys, ) self.stack = ExitStack() if checkpointer: @@ -505,6 +521,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): checkpointer: Optional[BaseCheckpointSaver], nodes: Mapping[str, PregelNode], specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], + output_keys: Union[str, Sequence[str]] = EMPTY_SEQ, ) -> None: super().__init__( input, @@ -513,6 +530,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): store=store, nodes=nodes, specs=specs, + output_keys=output_keys, ) self.store = AsyncBatchedStore(self.store) if self.store else None self.stack = AsyncExitStack() diff --git a/libs/langgraph/tests/fake_tracer.py b/libs/langgraph/tests/fake_tracer.py new file mode 100644 index 000000000..28ecc88db --- /dev/null +++ b/libs/langgraph/tests/fake_tracer.py @@ -0,0 +1,91 @@ +from typing import Any, Optional +from uuid import UUID + +from langchain_core.messages.base import BaseMessage +from langchain_core.outputs.chat_generation import ChatGeneration +from langchain_core.outputs.llm_result import LLMResult +from langchain_core.tracers import BaseTracer, Run + + +class FakeTracer(BaseTracer): + """Fake tracer that records LangChain execution. + It replaces run ids with deterministic UUIDs for snapshotting.""" + + def __init__(self) -> None: + """Initialize the tracer.""" + super().__init__() + self.runs: list[Run] = [] + self.uuids_map: dict[UUID, UUID] = {} + self.uuids_generator = ( + UUID(f"00000000-0000-4000-8000-{i:012}", version=4) for i in range(10000) + ) + + def _replace_uuid(self, uuid: UUID) -> UUID: + if uuid not in self.uuids_map: + self.uuids_map[uuid] = next(self.uuids_generator) + return self.uuids_map[uuid] + + def _replace_message_id(self, maybe_message: Any) -> Any: + if isinstance(maybe_message, BaseMessage): + maybe_message.id = str(next(self.uuids_generator)) + if isinstance(maybe_message, ChatGeneration): + maybe_message.message.id = str(next(self.uuids_generator)) + if isinstance(maybe_message, LLMResult): + for i, gen_list in enumerate(maybe_message.generations): + for j, gen in enumerate(gen_list): + maybe_message.generations[i][j] = self._replace_message_id(gen) + if isinstance(maybe_message, dict): + for k, v in maybe_message.items(): + maybe_message[k] = self._replace_message_id(v) + if isinstance(maybe_message, list): + for i, v in enumerate(maybe_message): + maybe_message[i] = self._replace_message_id(v) + + return maybe_message + + def _copy_run(self, run: Run) -> Run: + if run.dotted_order: + levels = run.dotted_order.split(".") + processed_levels = [] + for level in levels: + timestamp, run_id = level.split("Z") + new_run_id = self._replace_uuid(UUID(run_id)) + processed_level = f"{timestamp}Z{new_run_id}" + processed_levels.append(processed_level) + new_dotted_order = ".".join(processed_levels) + else: + new_dotted_order = None + return run.copy( + update={ + "id": self._replace_uuid(run.id), + "parent_run_id": ( + self.uuids_map[run.parent_run_id] if run.parent_run_id else None + ), + "child_runs": [self._copy_run(child) for child in run.child_runs], + "trace_id": self._replace_uuid(run.trace_id) if run.trace_id else None, + "dotted_order": new_dotted_order, + "inputs": self._replace_message_id(run.inputs), + "outputs": self._replace_message_id(run.outputs), + } + ) + + def _persist_run(self, run: Run) -> None: + """Persist a run.""" + + self.runs.append(self._copy_run(run)) + + def flattened_runs(self) -> list[Run]: + q = [] + self.runs + result = [] + while q: + parent = q.pop() + result.append(parent) + if parent.child_runs: + q.extend(parent.child_runs) + return result + + @property + def run_ids(self) -> list[Optional[UUID]]: + runs = self.flattened_runs() + uuids_map = {v: k for k, v in self.uuids_map.items()} + return [uuids_map.get(r.id) for r in runs] diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 197dc6c61..74897efcc 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -68,6 +68,7 @@ from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore from tests.any_str import AnyStr, ExceptionLike +from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, MemorySaverAssertImmutable, @@ -6159,11 +6160,20 @@ def test_dynamic_interrupt(snapshot: SnapshotAssertion) -> None: tool_two_graph.add_edge(START, "tool_two") tool_two = tool_two_graph.compile() - assert tool_two.invoke({"my_key": "value", "market": "DE"}) == { + tracer = FakeTracer() + assert tool_two.invoke( + {"my_key": "value", "market": "DE"}, {"callbacks": [tracer]} + ) == { "my_key": "value", "market": "DE", } assert tool_two_node_count == 1, "interrupts aren't retried" + assert len(tracer.runs) == 1 + run = tracer.runs[0] + assert run.end_time is not None + assert run.error is None + assert run.outputs == {"market": "DE", "my_key": "value"} + assert tool_two.invoke({"my_key": "value", "market": "US"}) == { "my_key": "value all good", "market": "US", diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index e9987c601..ef78c8bdb 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -63,6 +63,7 @@ from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore from tests.any_str import AnyStr, ExceptionLike +from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, MemorySaverAssertImmutable, @@ -225,11 +226,20 @@ async def test_dynamic_interrupt(snapshot: SnapshotAssertion) -> None: tool_two_graph.add_edge(START, "tool_two") tool_two = tool_two_graph.compile() - assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}) == { + tracer = FakeTracer() + assert await tool_two.ainvoke( + {"my_key": "value", "market": "DE"}, {"callbacks": [tracer]} + ) == { "my_key": "value", "market": "DE", } assert tool_two_node_count == 1, "interrupts aren't retried" + assert len(tracer.runs) == 1 + run = tracer.runs[0] + assert run.end_time is not None + assert run.error is None + assert run.outputs == {"market": "DE", "my_key": "value"} + assert await tool_two.ainvoke({"my_key": "value", "market": "US"}) == { "my_key": "value all good", "market": "US", From 9147d05cc47b8380cd8f6d76ad5f7351bb538d60 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 14:32:15 -0700 Subject: [PATCH 060/287] lib0.2.9 --- libs/cli/langgraph_cli/docker.py | 17 +++++++++++++++-- libs/langgraph/pyproject.toml | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/libs/cli/langgraph_cli/docker.py b/libs/cli/langgraph_cli/docker.py index 79e0dd25a..d2cffc26b 100644 --- a/libs/cli/langgraph_cli/docker.py +++ b/libs/cli/langgraph_cli/docker.py @@ -12,6 +12,15 @@ DEFAULT_POSTGRES_URI = ( "postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable" ) +REDIS = """ + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 +""" DB = """ langgraph-postgres: @@ -166,18 +175,22 @@ def compose( compose_str = f"""{volumes}services: {db} +{REDIS} {debugger_compose(port=debugger_port, base_url=debugger_base_url)} langgraph-api: ports: - - "{port}:8000\"""" + - "{port}:8000\" + depends_on: + langgraph-redis: + condition: service_healthy""" if include_db: compose_str += """ - depends_on: langgraph-postgres: condition: service_healthy""" compose_str += f""" environment: POSTGRES_URI: {postgres_uri} + REDIS_URI: redis://langgraph-redis:6379 """ if capabilities.healthcheck_start_interval: compose_str += """ healthcheck: diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 93dc203f2..1df951ee4 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.8" +version = "0.2.9" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From 37848a536185731f227b50dc7c896c2d90a1be00 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 14:37:36 -0700 Subject: [PATCH 061/287] cli: Add redis container to `langgraph up` --- libs/cli/langgraph_cli/docker.py | 4 +-- libs/cli/tests/unit_tests/test_cli.py | 10 ++++++ libs/cli/tests/unit_tests/test_docker.py | 43 ++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/libs/cli/langgraph_cli/docker.py b/libs/cli/langgraph_cli/docker.py index d2cffc26b..f28274f4d 100644 --- a/libs/cli/langgraph_cli/docker.py +++ b/libs/cli/langgraph_cli/docker.py @@ -174,8 +174,8 @@ def compose( interval: 5s""" compose_str = f"""{volumes}services: -{db} {REDIS} +{db} {debugger_compose(port=debugger_port, base_url=debugger_base_url)} langgraph-api: ports: @@ -189,8 +189,8 @@ def compose( condition: service_healthy""" compose_str += f""" environment: - POSTGRES_URI: {postgres_uri} REDIS_URI: redis://langgraph-redis:6379 + POSTGRES_URI: {postgres_uri} """ if capabilities.healthcheck_start_interval: compose_str += """ healthcheck: diff --git a/libs/cli/tests/unit_tests/test_cli.py b/libs/cli/tests/unit_tests/test_cli.py index da9354c07..a4972f393 100644 --- a/libs/cli/tests/unit_tests/test_cli.py +++ b/libs/cli/tests/unit_tests/test_cli.py @@ -45,6 +45,13 @@ def test_prepare_args_and_stdin(): langgraph-data: driver: local services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 langgraph-postgres: image: postgres:16 ports: @@ -76,9 +83,12 @@ services: ports: - "8000:8000" depends_on: + langgraph-redis: + condition: service_healthy langgraph-postgres: condition: service_healthy environment: + REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {DEFAULT_POSTGRES_URI} healthcheck: test: python /api/healthcheck.py diff --git a/libs/cli/tests/unit_tests/test_docker.py b/libs/cli/tests/unit_tests/test_docker.py index 4d5e09b94..781c049c1 100644 --- a/libs/cli/tests/unit_tests/test_docker.py +++ b/libs/cli/tests/unit_tests/test_docker.py @@ -20,10 +20,21 @@ def test_compose_with_no_debugger_and_custom_db(): DEFAULT_DOCKER_CAPABILITIES, port=port, postgres_uri=custom_postgres_uri ) expected_compose_str = f"""services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 langgraph-api: ports: - "{port}:8000" + depends_on: + langgraph-redis: + condition: service_healthy environment: + REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {custom_postgres_uri}""" assert clean_empty_lines(actual_compose_str) == expected_compose_str @@ -37,10 +48,21 @@ def test_compose_with_no_debugger_and_custom_db_with_healthcheck(): postgres_uri=custom_postgres_uri, ) expected_compose_str = f"""services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 langgraph-api: ports: - "{port}:8000" + depends_on: + langgraph-redis: + condition: service_healthy environment: + REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {custom_postgres_uri} healthcheck: test: python /api/healthcheck.py @@ -59,10 +81,21 @@ def test_compose_with_debugger_and_custom_db(): postgres_uri=custom_postgres_uri, ) expected_compose_str = f"""services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 langgraph-api: ports: - "{port}:8000" + depends_on: + langgraph-redis: + condition: service_healthy environment: + REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {custom_postgres_uri}""" assert clean_empty_lines(actual_compose_str) == expected_compose_str @@ -74,6 +107,13 @@ def test_compose_with_debugger_and_default_db(): langgraph-data: driver: local services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 langgraph-postgres: image: postgres:16 ports: @@ -94,8 +134,11 @@ services: ports: - "{port}:8000" depends_on: + langgraph-redis: + condition: service_healthy langgraph-postgres: condition: service_healthy environment: + REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {DEFAULT_POSTGRES_URI}""" assert clean_empty_lines(actual_compose_str) == expected_compose_str From 0a87b9fa1c8e6385c088f665311a0fe0fb286e5f Mon Sep 17 00:00:00 2001 From: vbarda Date: Wed, 21 Aug 2024 15:49:56 -0400 Subject: [PATCH 062/287] update more tests --- libs/langgraph/langgraph/pregel/__init__.py | 7 +- libs/langgraph/tests/test_pregel.py | 148 +++++++++++++--- libs/langgraph/tests/test_pregel_async.py | 183 ++++++++++++++++++-- 3 files changed, 296 insertions(+), 42 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 488334f63..cb8cb29fb 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -558,7 +558,12 @@ class Pregel( if not checkpoint_ns_to_state_snapshots: return StateSnapshot( - values={}, next=(), config=config, metadata=None, created_at=None, tasks=() + values={}, + next=(), + config=config, + metadata=None, + created_at=None, + tasks=(), ) state_snapshot = _assemble_state_snapshot_hierarchy( diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 8016f3911..6058ecf1d 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -7986,7 +7986,13 @@ def test_nested_graph_interrupts( assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner",), config={ "configurable": { @@ -8014,7 +8020,13 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=(PregelTask(AnyStr(), "inner_2"),), + tasks=( + PregelTask( + AnyStr(), + "inner_2", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner_2",), config={ "configurable": { @@ -8288,7 +8300,13 @@ def test_nested_graph_interrupts( assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner",), config={ "configurable": { @@ -8357,7 +8375,13 @@ def test_nested_graph_interrupts( assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner",), config={ "configurable": { @@ -8385,7 +8409,13 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=(PregelTask(AnyStr(), "inner_2"),), + tasks=( + PregelTask( + AnyStr(), + "inner_2", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner_2",), config={ "configurable": { @@ -8635,7 +8665,13 @@ def test_nested_graph_interrupts( assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner",), config={ "configurable": { @@ -8663,7 +8699,13 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=(PregelTask(AnyStr(), "inner_2"),), + tasks=( + PregelTask( + AnyStr(), + name="inner_2", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner_2",), config={ "configurable": { @@ -9040,7 +9082,9 @@ def test_nested_graph_interrupts( assert state_history == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), + ), next=("inner",), config={ "configurable": { @@ -9068,7 +9112,11 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=(PregelTask(AnyStr(), "inner_2"),), + tasks=( + PregelTask( + AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) + ), + ), next=("inner_2",), config={ "configurable": { @@ -9152,7 +9200,9 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=(PregelTask(AnyStr(), "inner_2"),), + tasks=( + PregelTask(AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),)), + ), next=("inner_2",), config={ "configurable": { @@ -9201,7 +9251,9 @@ def test_nested_graph_interrupts( assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), + ), next=("inner",), config={ "configurable": { @@ -9229,7 +9281,11 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=(PregelTask(AnyStr(), "inner_2"),), + tasks=( + PregelTask( + AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) + ), + ), next=("inner_2",), config={ "configurable": { @@ -9261,7 +9317,9 @@ def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), + ), next=("inner",), config={ "configurable": { @@ -9289,7 +9347,11 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=(PregelTask(AnyStr(), "inner_2"),), + tasks=( + PregelTask( + AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) + ), + ), next=("inner_2",), config={ "configurable": { @@ -9420,7 +9482,9 @@ def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), + ), next=("inner",), config={ "configurable": { @@ -9448,7 +9512,11 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=(PregelTask(AnyStr(), "inner_2"),), + tasks=( + PregelTask( + AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) + ), + ), next=("inner_2",), config={ "configurable": { @@ -9869,7 +9937,11 @@ def test_nested_graph_state( # test state w/ nested subgraph state (right after interrupt) assert app.get_state(config) == StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), "inner", interrupts=(Interrupt(when="before", value=None),) + ), + ), next=("inner",), config={ "configurable": { @@ -9894,7 +9966,14 @@ def test_nested_graph_state( subgraph_state_snapshots={ "inner": StateSnapshot( values={"my_key": "hi my value here", "my_other_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner_2"),), + tasks=( + PregelTask( + AnyStr(), + name="inner_2", + error=None, + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner_2",), config={ "configurable": { @@ -9928,7 +10007,13 @@ def test_nested_graph_state( assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner",), config={ "configurable": { @@ -9956,7 +10041,14 @@ def test_nested_graph_state( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=(PregelTask(AnyStr(), "inner_2"),), + tasks=( + PregelTask( + AnyStr(), + name="inner_2", + error=None, + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner_2",), config={ "configurable": { @@ -10357,7 +10449,7 @@ def test_doubly_nested_graph_state( app.invoke({"my_key": "my value"}, config, debug=True) assert app.get_state(config) == StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "child"),), + tasks=(PregelTask(AnyStr(), "child", interrupts=(Interrupt(when="before"),)),), next=("child",), config={ "configurable": { @@ -10382,7 +10474,11 @@ def test_doubly_nested_graph_state( subgraph_state_snapshots={ "child": StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "child_1"),), + tasks=( + PregelTask( + AnyStr(), "child_1", interrupts=(Interrupt(when="before"),) + ), + ), next=("child_1",), config={ "configurable": { @@ -10403,7 +10499,13 @@ def test_doubly_nested_graph_state( subgraph_state_snapshots={ "child_1": StateSnapshot( values={"my_key": "hi my value here"}, - tasks=(PregelTask(AnyStr(), "grandchild_2"),), + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + interrupts=(Interrupt(when="before"),), + ), + ), next=("grandchild_2",), config={ "configurable": { diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index eb6a1abe8..22178f103 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -6456,7 +6456,13 @@ async def test_nested_graph_interrupts( assert [s async for s in app.aget_state_history(config)] == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner",), config={ "configurable": { @@ -6484,6 +6490,13 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, + tasks=( + PregelTask( + AnyStr(), + "inner_2", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner_2",), config={ "configurable": { @@ -6642,6 +6655,7 @@ async def test_nested_graph_interrupts( "my_other_key": "hi my value here", }, next=(), + tasks=(), config={ "configurable": { "thread_id": "1", @@ -6761,7 +6775,13 @@ async def test_nested_graph_interrupts( assert [s async for s in app.aget_state_history(config)] == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner",), config={ "configurable": { @@ -6830,7 +6850,13 @@ async def test_nested_graph_interrupts( assert [s async for s in app.aget_state_history(config)] == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner",), config={ "configurable": { @@ -6858,6 +6884,13 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, + tasks=( + PregelTask( + AnyStr(), + "inner_2", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner_2",), config={ "configurable": { @@ -7020,6 +7053,7 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here and there", "my_other_key": "hi my value here", }, + tasks=(), next=(), config={ "configurable": { @@ -7109,7 +7143,13 @@ async def test_nested_graph_interrupts( assert [s async for s in app.aget_state_history(config)] == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner",), config={ "configurable": { @@ -7137,6 +7177,13 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, + tasks=( + PregelTask( + AnyStr(), + name="inner_2", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner_2",), config={ "configurable": { @@ -7270,6 +7317,7 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here and there", "my_other_key": "hi my value here", }, + tasks=(), next=(), config={ "configurable": { @@ -7429,6 +7477,7 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here and there", "my_other_key": "hi my value here", }, + tasks=(), next=(), config={ "configurable": { @@ -7510,7 +7559,9 @@ async def test_nested_graph_interrupts( assert state_history == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), + ), next=("inner",), config={ "configurable": { @@ -7538,6 +7589,11 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, + tasks=( + PregelTask( + AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) + ), + ), next=("inner_2",), config={ "configurable": { @@ -7618,9 +7674,14 @@ async def test_nested_graph_interrupts( ] assert child_state_history == [ StateSnapshot( - values={"my_key": "hi my value here"}, - tasks=(), - next=(), + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + tasks=( + PregelTask(AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),)), + ), + next=("inner_2",), config={ "configurable": { "thread_id": "6", @@ -7660,7 +7721,7 @@ async def test_nested_graph_interrupts( # check resuming from interrupt w/ checkpoint_id interrupt_state_snapshot, before_interrupt_state_snapshot = state_history[:2] before_interrupt_config = before_interrupt_state_snapshot.config - # going to get to interrupt again here + # going to get to interrupt again here, so the output is None assert await app.ainvoke(None, before_interrupt_config, debug=True) == { "my_key": "hi my value" } @@ -7668,7 +7729,9 @@ async def test_nested_graph_interrupts( assert [s async for s in app.aget_state_history(config)] == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), + ), next=("inner",), config={ "configurable": { @@ -7696,6 +7759,11 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, + tasks=( + PregelTask( + AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) + ), + ), next=("inner_2",), config={ "configurable": { @@ -7727,7 +7795,9 @@ async def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), + ), next=("inner",), config={ "configurable": { @@ -7755,6 +7825,11 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, + tasks=( + PregelTask( + AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) + ), + ), next=("inner_2",), config={ "configurable": { @@ -7825,9 +7900,9 @@ async def test_nested_graph_interrupts( parent_config=None, ), ] - # going to resume from interrupt + # going to restart from interrupt interrupt_config = interrupt_state_snapshot.config - assert (await app.ainvoke(None, interrupt_config, debug=True)) == { + assert await app.ainvoke(None, interrupt_config, debug=True) == { "my_key": "hi my value here and there and back again", } assert [s async for s in app.aget_state_history(config)] == [ @@ -7885,7 +7960,9 @@ async def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), + ), next=("inner",), config={ "configurable": { @@ -7913,6 +7990,11 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, + tasks=( + PregelTask( + AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) + ), + ), next=("inner_2",), config={ "configurable": { @@ -7973,6 +8055,7 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here and there", "my_other_key": "hi my value here", }, + tasks=(), next=(), config={ "configurable": { @@ -8337,6 +8420,11 @@ async def test_nested_graph_state( # test state w/ nested subgraph state (right after interrupt) assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), "inner", interrupts=(Interrupt(when="before", value=None),) + ), + ), next=("inner",), config={ "configurable": { @@ -8360,10 +8448,15 @@ async def test_nested_graph_state( }, subgraph_state_snapshots={ "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, + values={"my_key": "hi my value here", "my_other_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + name="inner_2", + error=None, + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner_2",), config={ "configurable": { @@ -8397,6 +8490,13 @@ async def test_nested_graph_state( assert [s async for s in app.aget_state_history(config)] == [ StateSnapshot( values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner",), config={ "configurable": { @@ -8424,6 +8524,14 @@ async def test_nested_graph_state( "my_key": "hi my value here", "my_other_key": "hi my value", }, + tasks=( + PregelTask( + AnyStr(), + name="inner_2", + error=None, + interrupts=(Interrupt(when="before", value=None),), + ), + ), next=("inner_2",), config={ "configurable": { @@ -8456,6 +8564,7 @@ async def test_nested_graph_state( ), StateSnapshot( values={"my_key": "my value"}, + tasks=(PregelTask(AnyStr(), "outer_1"),), next=("outer_1",), config={ "configurable": { @@ -8477,6 +8586,7 @@ async def test_nested_graph_state( ), StateSnapshot( values={}, + tasks=(PregelTask(AnyStr(), "__start__"),), next=("__start__",), config={ "configurable": { @@ -8499,6 +8609,7 @@ async def test_nested_graph_state( # test state w/ nested subgraph state (after resuming from interrupt) assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value here and there and back again"}, + tasks=(), next=(), config={ "configurable": { @@ -8532,6 +8643,7 @@ async def test_nested_graph_state( "my_key": "hi my value here and there", "my_other_key": "hi my value here", }, + tasks=(), next=(), config={ "configurable": { @@ -8571,6 +8683,7 @@ async def test_nested_graph_state( }, ) == StateSnapshot( values={"my_key": "hi my value"}, + tasks=(PregelTask(AnyStr(), "inner"),), next=("inner",), config={ "configurable": { @@ -8598,6 +8711,7 @@ async def test_nested_graph_state( assert [s async for s in app.aget_state_history(config)] == [ StateSnapshot( values={"my_key": "hi my value here and there and back again"}, + tasks=(), next=(), config={ "configurable": { @@ -8625,6 +8739,7 @@ async def test_nested_graph_state( ), StateSnapshot( values={"my_key": "hi my value here and there"}, + tasks=(PregelTask(AnyStr(), "outer_2"),), next=("outer_2",), config={ "configurable": { @@ -8650,6 +8765,7 @@ async def test_nested_graph_state( ), StateSnapshot( values={"my_key": "hi my value"}, + tasks=(PregelTask(AnyStr(), "inner"),), next=("inner",), config={ "configurable": { @@ -8677,6 +8793,7 @@ async def test_nested_graph_state( "my_key": "hi my value here and there", "my_other_key": "hi my value here", }, + tasks=(), next=(), config={ "configurable": { @@ -8709,6 +8826,7 @@ async def test_nested_graph_state( ), StateSnapshot( values={"my_key": "my value"}, + tasks=(PregelTask(AnyStr(), "outer_1"),), next=("outer_1",), config={ "configurable": { @@ -8730,6 +8848,7 @@ async def test_nested_graph_state( ), StateSnapshot( values={}, + tasks=(PregelTask(AnyStr(), "__start__"),), next=("__start__",), config={ "configurable": { @@ -8813,6 +8932,7 @@ async def test_doubly_nested_graph_state( await app.ainvoke({"my_key": "my value"}, config, debug=True) assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value"}, + tasks=(PregelTask(AnyStr(), "child", interrupts=(Interrupt(when="before"),)),), next=("child",), config={ "configurable": { @@ -8837,6 +8957,11 @@ async def test_doubly_nested_graph_state( subgraph_state_snapshots={ "child": StateSnapshot( values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), "child_1", interrupts=(Interrupt(when="before"),) + ), + ), next=("child_1",), config={ "configurable": { @@ -8857,6 +8982,13 @@ async def test_doubly_nested_graph_state( subgraph_state_snapshots={ "child_1": StateSnapshot( values={"my_key": "hi my value here"}, + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + interrupts=(Interrupt(when="before"),), + ), + ), next=("grandchild_2",), config={ "configurable": { @@ -8887,6 +9019,7 @@ async def test_doubly_nested_graph_state( await app.ainvoke(None, config, debug=True) assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value here and there and back again"}, + tasks=(), next=(), config={ "configurable": { @@ -8917,6 +9050,7 @@ async def test_doubly_nested_graph_state( ) assert grandchild_snapshot == StateSnapshot( values={"my_key": "hi my value here and there"}, + tasks=(), next=(), config={ "configurable": { @@ -8946,6 +9080,7 @@ async def test_doubly_nested_graph_state( ) assert child_snapshot == StateSnapshot( values={"my_key": "hi my value here and there"}, + tasks=(), next=(), config={ "configurable": { @@ -8979,6 +9114,7 @@ async def test_doubly_nested_graph_state( }, ) == StateSnapshot( values={"my_key": "hi my value"}, + tasks=(PregelTask(AnyStr(), "child"),), next=("child",), config={ "configurable": { @@ -9069,6 +9205,10 @@ async def test_send_to_nested_graphs( } expected_snapshot = StateSnapshot( values={"subjects": ["cats", "dogs"], "jokes": []}, + tasks=( + PregelTask(AnyStr(), "generate_joke"), + PregelTask(AnyStr(), "generate_joke"), + ), next=("generate_joke", "generate_joke"), config={ "configurable": { @@ -9102,6 +9242,7 @@ async def test_send_to_nested_graphs( "subjects": ["cats", "dogs"], "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], }, + tasks=(), next=(), config={ "configurable": { @@ -9146,6 +9287,7 @@ async def test_send_to_nested_graphs( "subjects": ["cats", "dogs"], "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], }, + tasks=(), next=(), config={ "configurable": { @@ -9177,6 +9319,10 @@ async def test_send_to_nested_graphs( StateSnapshot( values={"subjects": ["cats", "dogs"], "jokes": []}, next=("generate_joke", "generate_joke"), + tasks=( + PregelTask(AnyStr(), "generate_joke"), + PregelTask(AnyStr(), "generate_joke"), + ), config={ "configurable": { "thread_id": "1", @@ -9197,6 +9343,7 @@ async def test_send_to_nested_graphs( ), StateSnapshot( values={"jokes": []}, + tasks=(PregelTask(AnyStr(), "__start__"),), next=("__start__",), config={ "configurable": { From e7bc74e9186ca4d74bdcb14c9bf426f1d1acb723 Mon Sep 17 00:00:00 2001 From: vbarda Date: Wed, 21 Aug 2024 18:06:18 -0400 Subject: [PATCH 063/287] remove refactors --- libs/langgraph/langgraph/pregel/__init__.py | 163 +++++++++----------- 1 file changed, 70 insertions(+), 93 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index cb8cb29fb..5c54d5b7e 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -258,73 +258,6 @@ def _assemble_state_snapshot_hierarchy( return state_snapshot -def _prepare_state_snapshot( - saved: CheckpointTuple, - graph: Pregel, -) -> StateSnapshot: - with ChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in graph.channels.items() - }, - saved.checkpoint, - saved.config, - ) as channels, ManagedValuesManager( - graph.managed_values_dict, ensure_config(saved.config) - ) as managed: - next_tasks = prepare_next_tasks( - saved.checkpoint, - graph.nodes, - channels, - managed, - saved.config, - saved.metadata.get("step", -1) + 1, - for_execution=False, - ) - return StateSnapshot( - values=read_channels(channels, graph.stream_channels_asis), - next=tuple(t.name for t in next_tasks), - config=saved.config, - metadata=saved.metadata, - created_at=saved.checkpoint["ts"], - parent_config=saved.parent_config, - tasks=tasks_w_writes(next_tasks, saved.pending_writes), - ) - - -async def _prepare_state_snapshot_async( - saved: CheckpointTuple, graph: Pregel -) -> StateSnapshot: - async with AsyncChannelsManager( - { - k: LastValue(None) if isinstance(c, Context) else c - for k, c in graph.channels.items() - }, - saved.checkpoint, - saved.config, - ) as channels, AsyncManagedValuesManager( - graph.managed_values_dict, ensure_config(saved.config) - ) as managed: - next_tasks = prepare_next_tasks( - saved.checkpoint, - graph.nodes, - channels, - managed, - saved.config, - saved.metadata.get("step", -1) + 1, - for_execution=False, - ) - return StateSnapshot( - values=read_channels(channels, graph.stream_channels_asis), - next=tuple(t.name for t in next_tasks), - config=saved.config, - metadata=saved.metadata, - created_at=saved.checkpoint["ts"], - parent_config=saved.parent_config, - tasks=tasks_w_writes(next_tasks, saved.pending_writes), - ) - - def _has_nested_interrupts( graph: Pregel, ) -> bool: @@ -517,20 +450,16 @@ class Pregel( if not self.checkpointer: raise ValueError("No checkpointer set") - checkpoint_tuple = self.checkpointer.get_tuple(config) - checkpoint_config = checkpoint_tuple.config if checkpoint_tuple else config + saved = self.checkpointer.get_tuple(config) + checkpoint_config = saved.config if saved else config checkpoint_ns = checkpoint_config["configurable"].get("checkpoint_ns", "") checkpoint_id = checkpoint_config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} checkpoint_ns_to_graph: dict[str, Pregel] = {} - for checkpoint_tuple in self.checkpointer.list(config): - saved_checkpoint_ns = checkpoint_tuple.config["configurable"][ - "checkpoint_ns" - ] - saved_checkpoint_id = checkpoint_tuple.config["configurable"][ - "checkpoint_id" - ] + for saved in self.checkpointer.list(config): + saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] + saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] if checkpoint_id != saved_checkpoint_id: continue @@ -547,10 +476,36 @@ class Pregel( self, saved_checkpoint_ns ) - state_snapshot = _prepare_state_snapshot( - checkpoint_tuple, - checkpoint_ns_to_graph[saved_checkpoint_ns], - ) + graph = checkpoint_ns_to_graph[saved_checkpoint_ns] + with ChannelsManager( + { + k: LastValue(None) if isinstance(c, Context) else c + for k, c in graph.channels.items() + }, + saved.checkpoint, + saved.config, + ) as channels, ManagedValuesManager( + graph.managed_values_dict, ensure_config(saved.config) + ) as managed: + next_tasks = prepare_next_tasks( + saved.checkpoint, + graph.nodes, + channels, + managed, + saved.config, + saved.metadata.get("step", -1) + 1, + for_execution=False, + ) + state_snapshot = StateSnapshot( + read_channels(channels, graph.stream_channels_asis), + tuple(t.name for t in next_tasks), + saved.config, + saved.metadata, + saved.checkpoint["ts"], + saved.parent_config, + tasks_w_writes(next_tasks, saved.pending_writes), + ) + checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot checkpoint_ns_to_checkpoint_id[ saved_checkpoint_ns @@ -576,20 +531,16 @@ class Pregel( if not self.checkpointer: raise ValueError("No checkpointer set") - checkpoint_tuple = await self.checkpointer.aget_tuple(config) - checkpoint_config = checkpoint_tuple.config if checkpoint_tuple else config + saved = await self.checkpointer.aget_tuple(config) + checkpoint_config = saved.config if saved else config checkpoint_ns = checkpoint_config["configurable"].get("checkpoint_ns", "") checkpoint_id = checkpoint_config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} checkpoint_ns_to_graph: dict[str, Pregel] = {} - async for checkpoint_tuple in self.checkpointer.alist(config): - saved_checkpoint_ns = checkpoint_tuple.config["configurable"][ - "checkpoint_ns" - ] - saved_checkpoint_id = checkpoint_tuple.config["configurable"][ - "checkpoint_id" - ] + async for saved in self.checkpointer.alist(config): + saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] + saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] if checkpoint_id != saved_checkpoint_id: continue @@ -606,10 +557,36 @@ class Pregel( self, saved_checkpoint_ns ) - state_snapshot = await _prepare_state_snapshot_async( - checkpoint_tuple, - checkpoint_ns_to_graph[saved_checkpoint_ns], - ) + graph = checkpoint_ns_to_graph[saved_checkpoint_ns] + async with AsyncChannelsManager( + { + k: LastValue(None) if isinstance(c, Context) else c + for k, c in graph.channels.items() + }, + saved.checkpoint, + saved.config, + ) as channels, AsyncManagedValuesManager( + graph.managed_values_dict, ensure_config(saved.config) + ) as managed: + next_tasks = prepare_next_tasks( + saved.checkpoint, + graph.nodes, + channels, + managed, + saved.config, + saved.metadata.get("step", -1) + 1, + for_execution=False, + ) + state_snapshot = StateSnapshot( + read_channels(channels, graph.stream_channels_asis), + tuple(t.name for t in next_tasks), + saved.config, + saved.metadata, + saved.checkpoint["ts"], + saved.parent_config, + tasks_w_writes(next_tasks, saved.pending_writes), + ) + checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot checkpoint_ns_to_checkpoint_id[ saved_checkpoint_ns From 0b6088f91386d885cb661a5bbcf68218a253bc4c Mon Sep 17 00:00:00 2001 From: vbarda Date: Wed, 21 Aug 2024 18:26:54 -0400 Subject: [PATCH 064/287] remove futures.clear --- libs/langgraph/langgraph/pregel/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index ed3ec185a..4ef53b40f 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -1185,8 +1185,6 @@ class Pregel( else: loop.put_writes(task.id, [(ERROR, exc)]) - futures.clear() - else: # save task writes to checkpointer loop.put_writes(task.id, task.writes) @@ -1447,7 +1445,6 @@ class Pregel( else: loop.put_writes(task.id, [(ERROR, exc)]) - futures.clear() else: # save task writes to checkpointer loop.put_writes(task.id, task.writes) From d85e267a83bec2e080eae26e1e3486c39f8020d1 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 16:23:48 -0700 Subject: [PATCH 065/287] Add interrupts and errors to debug stream mode - move remaining output code from Pregel.stream to PregelLoop --- libs/langgraph/langgraph/pregel/__init__.py | 122 +++++++--------- libs/langgraph/langgraph/pregel/debug.py | 16 +- libs/langgraph/langgraph/pregel/io.py | 19 ++- libs/langgraph/langgraph/pregel/loop.py | 36 ++++- libs/langgraph/tests/test_pregel.py | 23 ++- libs/langgraph/tests/test_pregel_async.py | 153 +++++++++++++++++++- 6 files changed, 272 insertions(+), 97 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 6e4d3e002..563f097f8 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -76,16 +76,12 @@ from langgraph.pregel.algo import ( should_interrupt, ) from langgraph.pregel.debug import ( - map_debug_task_results, print_step_checkpoint, print_step_tasks, print_step_writes, tasks_w_writes, ) -from langgraph.pregel.io import ( - map_output_updates, - read_channels, -) +from langgraph.pregel.io import read_channels from langgraph.pregel.loop import AsyncPregelLoop, SyncPregelLoop from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager from langgraph.pregel.read import PregelNode @@ -962,6 +958,7 @@ class Pregel( nodes=self.nodes, specs=self.channels, output_keys=output_keys, + stream_keys=self.stream_channels_asis, ) as loop: # Similarly to Bulk Synchronous Parallel / Pregel model # computation proceeds in steps, while there are channel updates @@ -970,7 +967,6 @@ class Pregel( # with channel updates applied only at the transition between steps while loop.tick( input_keys=self.input_channels, - stream_keys=self.stream_channels_asis, interrupt_before=interrupt_before, interrupt_after=interrupt_after, manager=run_manager, @@ -1039,26 +1035,17 @@ class Pregel( else: # save task writes to checkpointer loop.put_writes(task.id, task.writes) - # yield updates output for the finished task - if "updates" in stream_modes: - yield from _with_mode( - "updates", - isinstance(stream_mode, list), - map_output_updates(output_keys, [task]), - ) - if "debug" in stream_modes: - yield from _with_mode( - "debug", - isinstance(stream_mode, list), - map_debug_task_results( - loop.step, - [task], - self.stream_channels_list, - ), - ) else: # remove references to loop vars del fut, task + # emit output + while loop.stream: + mode, payload = loop.stream.popleft() + if mode in stream_modes: + if isinstance(stream_mode, list): + yield (mode, payload) + else: + yield payload if _should_stop_others(done): break @@ -1073,21 +1060,21 @@ class Pregel( [w for t in loop.tasks for w in t.writes], self.stream_channels_list, ) - # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload - # handle exit - if loop.status == "out_of_steps": - raise GraphRecursionError( - f"Recursion limit of {config['recursion_limit']} reached " - "without hitting a stop condition. You can increase the " - "limit by setting the `recursion_limit` config key." - ) + # emit output + while loop.stream: + mode, payload = loop.stream.popleft() + if mode in stream_modes: + if isinstance(stream_mode, list): + yield (mode, payload) + else: + yield payload + # handle exit + if loop.status == "out_of_steps": + raise GraphRecursionError( + f"Recursion limit of {config['recursion_limit']} reached " + "without hitting a stop condition. You can increase the " + "limit by setting the `recursion_limit` config key." + ) # set final channel values as run output run_manager.on_chain_end(loop.output) except BaseException as e: @@ -1219,6 +1206,7 @@ class Pregel( nodes=self.nodes, specs=self.channels, output_keys=output_keys, + stream_keys=self.stream_channels_asis, ) as loop: aioloop = asyncio.get_event_loop() # Similarly to Bulk Synchronous Parallel / Pregel model @@ -1228,7 +1216,6 @@ class Pregel( # with channel updates applied only at the transition between steps while loop.tick( input_keys=self.input_channels, - stream_keys=self.stream_channels_asis, interrupt_before=interrupt_before, interrupt_after=interrupt_after, manager=run_manager, @@ -1298,28 +1285,17 @@ class Pregel( else: # save task writes to checkpointer loop.put_writes(task.id, task.writes) - # yield updates output for the finished task - if "updates" in stream_modes: - for chunk in _with_mode( - "updates", - isinstance(stream_mode, list), - map_output_updates(output_keys, [task]), - ): - yield chunk - if "debug" in stream_modes: - for chunk in _with_mode( - "debug", - isinstance(stream_mode, list), - map_debug_task_results( - loop.step, - [task], - self.stream_channels_list, - ), - ): - yield chunk else: # remove references to loop vars del fut, task + # emit output + while loop.stream: + mode, payload = loop.stream.popleft() + if mode in stream_modes: + if isinstance(stream_mode, list): + yield (mode, payload) + else: + yield payload if _should_stop_others(done): break @@ -1334,21 +1310,21 @@ class Pregel( [w for t in loop.tasks for w in t.writes], self.stream_channels_list, ) - # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload - # handle exit - if loop.status == "out_of_steps": - raise GraphRecursionError( - f"Recursion limit of {config['recursion_limit']} reached " - "without hitting a stop condition. You can increase the " - "limit by setting the `recursion_limit` config key." - ) + # emit output + while loop.stream: + mode, payload = loop.stream.popleft() + if mode in stream_modes: + if isinstance(stream_mode, list): + yield (mode, payload) + else: + yield payload + # handle exit + if loop.status == "out_of_steps": + raise GraphRecursionError( + f"Recursion limit of {config['recursion_limit']} reached " + "without hitting a stop condition. You can increase the " + "limit by setting the `recursion_limit` config key." + ) # set final channel values as run output await run_manager.on_chain_end(loop.output) except BaseException as e: diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index 5102cd8c5..72b6cdbe5 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -1,5 +1,6 @@ import json from collections import defaultdict +from dataclasses import asdict from datetime import datetime, timezone from pprint import pformat from typing import Any, Iterator, Literal, Mapping, Optional, Sequence, TypedDict, Union @@ -25,6 +26,8 @@ class TaskPayload(TypedDict): class TaskResultPayload(TypedDict): id: str name: str + error: Optional[str] + interrupts: list[dict] result: list[tuple[str, Any]] @@ -97,11 +100,14 @@ def map_debug_tasks( def map_debug_task_results( step: int, - tasks: list[PregelExecutableTask], - stream_channels_list: Sequence[str], + tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]], + stream_keys: Union[str, Sequence[str]], ) -> Iterator[DebugOutputTaskResult]: + stream_channels_list = ( + [stream_keys] if isinstance(stream_keys, str) else stream_keys + ) ts = datetime.now(timezone.utc).isoformat() - for name, _, _, writes, config, _, _, _ in tasks: + for (name, _, _, _, config, _, _, _), writes in tasks: if config is not None and TAG_HIDDEN in config.get("tags", []): continue @@ -116,7 +122,9 @@ def map_debug_task_results( "payload": { "id": str(uuid5(TASK_NAMESPACE, json.dumps((name, step, metadata)))), "name": name, + "error": next((w[1] for w in writes if w[0] == ERROR), None), "result": [w for w in writes if w[0] in stream_channels_list], + "interrupts": [asdict(w[1]) for w in writes if w[0] == INTERRUPT], }, } @@ -150,7 +158,7 @@ def map_debug_checkpoint( else { "id": t.id, "name": t.name, - "interrupts": t.interrupts, + "interrupts": tuple(asdict(i) for i in t.interrupts), } for t in tasks_w_writes(tasks, pending_writes) ], diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/io.py index 53e77557e..6c9205248 100644 --- a/libs/langgraph/langgraph/pregel/io.py +++ b/libs/langgraph/langgraph/pregel/io.py @@ -3,7 +3,7 @@ from typing import Any, Iterator, Mapping, Optional, Sequence, TypeVar, Union from langchain_core.runnables.utils import AddableDict from langgraph.channels.base import BaseChannel, EmptyChannelError -from langgraph.constants import TAG_HIDDEN +from langgraph.constants import ERROR, INTERRUPT, TAG_HIDDEN from langgraph.pregel.log import logger from langgraph.pregel.types import PregelExecutableTask @@ -95,19 +95,22 @@ class AddableUpdatesDict(AddableDict): def map_output_updates( output_channels: Union[str, Sequence[str]], - tasks: list[PregelExecutableTask], + tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]], ) -> Iterator[dict[str, Union[Any, dict[str, Any]]]]: """Map pending writes (a sequence of tuples (channel, value)) to output chunk.""" output_tasks = [ - t for t in tasks if not t.config or TAG_HIDDEN not in t.config.get("tags") + (t, ww) + for t, ww in tasks + if (not t.config or TAG_HIDDEN not in t.config.get("tags")) + and all(k not in (ERROR, INTERRUPT) for k, _ in ww) ] if not output_tasks: return if isinstance(output_channels, str): updated = [ (task.name, value) - for task in output_tasks - for chan, value in task.writes + for task, writes in output_tasks + for chan, value in writes if chan == output_channels ] else: @@ -116,10 +119,10 @@ def map_output_updates( task.name, {chan: value for chan, value in task.writes if chan in output_channels}, ) - for task in output_tasks - if any(chan in output_channels for chan, _ in task.writes) + for task, writes in output_tasks + if any(chan in output_channels for chan, _ in writes) ] - grouped = {t.name: [] for t in output_tasks} + grouped = {t.name: [] for t, _ in output_tasks} for node, value in updated: grouped[node].append(value) for node, value in grouped.items(): diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index aa256b2f7..11b937eea 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -56,7 +56,11 @@ from langgraph.pregel.algo import ( prepare_next_tasks, should_interrupt, ) -from langgraph.pregel.debug import map_debug_checkpoint, map_debug_tasks +from langgraph.pregel.debug import ( + map_debug_checkpoint, + map_debug_task_results, + map_debug_tasks, +) from langgraph.pregel.executor import ( AsyncBackgroundExecutor, BackgroundExecutor, @@ -90,6 +94,7 @@ class PregelLoop: nodes: Mapping[str, PregelNode] specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]] output_keys: Union[str, Sequence[str]] + stream_keys: Union[str, Sequence[str]] is_nested: bool checkpointer_get_next_version: Callable[[Optional[V]], V] @@ -138,6 +143,7 @@ class PregelLoop: nodes: Mapping[str, PregelNode], specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], output_keys: Union[str, Sequence[str]], + stream_keys: Union[str, Sequence[str]], ) -> None: self.stream = deque() self.input = input @@ -147,6 +153,7 @@ class PregelLoop: self.nodes = nodes self.specs = specs self.output_keys = output_keys + self.stream_keys = stream_keys self.is_nested = CONFIG_KEY_READ in self.config.get("configurable", {}) def mark_tasks_scheduled(self, tasks: Sequence[PregelExecutableTask]) -> None: @@ -172,12 +179,24 @@ class PregelLoop: writes, task_id, ) + if task := next((t for t in self.tasks if t.id == task_id), None): + self.stream.extend( + ("updates", v) + for v in map_output_updates(self.output_keys, [(task, writes)]) + ) + self.stream.extend( + ("debug", v) + for v in map_debug_task_results( + self.step, [(task, writes)], self.stream_keys + ) + ) + else: + raise RuntimeError def tick( self, *, input_keys: Union[str, Sequence[str]], - stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ, interrupt_after: Sequence[str] = EMPTY_SEQ, interrupt_before: Sequence[str] = EMPTY_SEQ, manager: Union[None, AsyncParentRunManager, ParentRunManager] = None, @@ -213,7 +232,11 @@ class PregelLoop: self._put_checkpoint( { "source": "loop", - "writes": single(map_output_updates(self.output_keys, self.tasks)), + "writes": single( + map_output_updates( + self.output_keys, [(t, t.writes) for t in self.tasks] + ) + ), } ) # after execution, check if we should interrupt @@ -256,7 +279,7 @@ class PregelLoop: self.step - 1, # printing checkpoint for previous step self.checkpoint_config, self.channels, - stream_keys, + self.stream_keys, self.checkpoint_metadata, self.checkpoint, self.tasks, @@ -281,7 +304,6 @@ class PregelLoop: if all(task.writes for task in self.tasks): return self.tick( input_keys=input_keys, - stream_keys=stream_keys, interrupt_after=interrupt_after, interrupt_before=interrupt_before, manager=manager, @@ -435,6 +457,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): nodes: Mapping[str, PregelNode], specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], output_keys: Union[str, Sequence[str]] = EMPTY_SEQ, + stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ, ) -> None: super().__init__( input, @@ -444,6 +467,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): nodes=nodes, specs=specs, output_keys=output_keys, + stream_keys=stream_keys, ) self.stack = ExitStack() if checkpointer: @@ -522,6 +546,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): nodes: Mapping[str, PregelNode], specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], output_keys: Union[str, Sequence[str]] = EMPTY_SEQ, + stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ, ) -> None: super().__init__( input, @@ -531,6 +556,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): nodes=nodes, specs=specs, output_keys=output_keys, + stream_keys=stream_keys, ) self.store = AsyncBatchedStore(self.store) if self.store else None self.stack = AsyncExitStack() diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 74897efcc..c1fc68c2a 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -769,7 +769,7 @@ def test_invoke_two_processes_in_out_interrupt( ), ] - # forking from any previous checkpoint w/out forking should do nothing + # re-running from any previous checkpoint w/out forking should do nothing assert [c for c in app.stream(None, history[0].config, stream_mode="updates")] == [] assert [c for c in app.stream(None, history[1].config, stream_mode="updates")] == [] assert [c for c in app.stream(None, history[2].config, stream_mode="updates")] == [] @@ -1033,6 +1033,8 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", "name": "one", "result": [("inbox", 3)], + "error": None, + "interrupts": [], }, }, { @@ -1043,6 +1045,8 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", "name": "two", "result": [("output", 13)], + "error": None, + "interrupts": [], }, }, { @@ -1064,6 +1068,8 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", "name": "two", "result": [("output", 4)], + "error": None, + "interrupts": [], }, }, ] @@ -1744,7 +1750,6 @@ def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: assert cleanup.call_count == 0 for i, chunk in enumerate(app.stream(2)): assert setup.call_count == 1, "Expected setup to be called once" - assert cleanup.call_count == 0, "Expected cleanup to not be called yet" if i == 0: assert chunk == {"inbox": [3]} elif i == 1: @@ -6017,6 +6022,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", "name": "rewrite_query", "result": [("query", "query: what is weather in sf")], + "error": None, + "interrupts": [], }, }, ), @@ -6071,6 +6078,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", "name": "retriever_two", "result": [("docs", ["doc3", "doc4"])], + "error": None, + "interrupts": [], }, }, ), @@ -6088,6 +6097,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", "name": "retriever_one", "result": [("docs", ["doc1", "doc2"])], + "error": None, + "interrupts": [], }, }, ), @@ -6127,6 +6138,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", "name": "qa", "result": [("answer", "doc1,doc2,doc3,doc4")], + "error": None, + "interrupts": [], }, }, ), @@ -6544,6 +6557,8 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", "name": "prepare", "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], }, }, { @@ -6596,6 +6611,8 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", "name": "tool_two_slow", "result": [("my_key", " slow")], + "error": None, + "interrupts": [], }, }, { @@ -6646,6 +6663,8 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", "name": "finish", "result": [("my_key", " finished")], + "error": None, + "interrupts": [], }, }, { diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index ef78c8bdb..692d101e0 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1278,6 +1278,8 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", "name": "one", "result": [("inbox", 3)], + "error": None, + "interrupts": [], }, }, { @@ -1288,6 +1290,8 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", "name": "two", "result": [("output", 13)], + "error": None, + "interrupts": [], }, }, { @@ -1309,6 +1313,8 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", "name": "two", "result": [("output", 4)], + "error": None, + "interrupts": [], }, }, ] @@ -1978,7 +1984,6 @@ async def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: assert setup_sync.call_count == 0, "Sync context manager should not be used" assert cleanup_sync.call_count == 0, "Sync context manager should not be used" assert setup_async.call_count == 1, "Expected setup to be called once" - assert cleanup_async.call_count == 0, "Expected cleanup to not be called yet" if i == 0: assert chunk == {"inbox": [3]} elif i == 1: @@ -4744,6 +4749,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", "name": "rewrite_query", "result": [("query", "query: what is weather in sf")], + "error": None, + "interrupts": [], }, }, ), @@ -4798,6 +4805,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", "name": "retriever_two", "result": [("docs", ["doc3", "doc4"])], + "error": None, + "interrupts": [], }, }, ), @@ -4815,6 +4824,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", "name": "retriever_one", "result": [("docs", ["doc1", "doc2"])], + "error": None, + "interrupts": [], }, }, ), @@ -4854,6 +4865,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", "name": "qa", "result": [("answer", "doc1,doc2,doc3,doc4")], + "error": None, + "interrupts": [], }, }, ), @@ -5218,6 +5231,8 @@ async def test_branch_then() -> None: "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", "name": "prepare", "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], }, }, { @@ -5270,6 +5285,8 @@ async def test_branch_then() -> None: "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", "name": "tool_two_slow", "result": [("my_key", " slow")], + "error": None, + "interrupts": [], }, }, { @@ -5320,6 +5337,8 @@ async def test_branch_then() -> None: "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", "name": "finish", "result": [("my_key", " finished")], + "error": None, + "interrupts": [], }, }, { @@ -5363,10 +5382,134 @@ async def test_branch_then() -> None: thread1 = {"configurable": {"thread_id": "1"}} # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { - "my_key": "value prepared", - "market": "DE", - } + assert [ + c + async for c in tool_two.astream( + {"my_key": "value", "market": "DE"}, thread1, stream_mode="debug" + ) + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": -1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "1"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": {"my_key": ""}, + "metadata": { + "source": "input", + "step": -1, + "writes": {"my_key": "value", "market": "DE"}, + }, + "next": ["__start__"], + "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "1"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value", + "market": "DE", + }, + "metadata": { + "source": "loop", + "step": 0, + "writes": None, + }, + "next": ["prepare"], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "ca572c3b-b805-5fc6-a19e-3d79f52dde70", + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "ca572c3b-b805-5fc6-a19e-3d79f52dde70", + "name": "prepare", + "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "1"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared", + "market": "DE", + }, + "metadata": { + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + "next": ["tool_two_slow"], + "tasks": [ + {"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()} + ], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "054b0ced-4546-58f4-bee5-548f029e1a8e", + "name": "tool_two_slow", + "result": [], + "error": None, + "interrupts": [{"when": "before", "value": None}], + }, + }, + ] assert await tool_two.aget_state(thread1) == StateSnapshot( values={"my_key": "value prepared", "market": "DE"}, tasks=( From d67419522dfa5350115198b8576f198cd06155fe Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 16:27:23 -0700 Subject: [PATCH 066/287] Remove error --- libs/langgraph/langgraph/pregel/loop.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 11b937eea..061b932c0 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -190,8 +190,6 @@ class PregelLoop: self.step, [(task, writes)], self.stream_keys ) ) - else: - raise RuntimeError def tick( self, From a545a70afb08f7458411bdc53284ed640e77494b Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 21 Aug 2024 16:33:30 -0700 Subject: [PATCH 067/287] lib0.2.10 --- 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 1df951ee4..9b38e6049 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.9" +version = "0.2.10" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From a9a59dd4e44d22f0c48301c06553e6ae92eece8f Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Thu, 22 Aug 2024 06:29:23 -0700 Subject: [PATCH 068/287] fix notebook (#1422) --- .../information-gather-prompting.ipynb | 144 ++++++++++++------ 1 file changed, 97 insertions(+), 47 deletions(-) diff --git a/examples/chatbots/information-gather-prompting.ipynb b/examples/chatbots/information-gather-prompting.ipynb index bc1248cda..ad4cdfe35 100644 --- a/examples/chatbots/information-gather-prompting.ipynb +++ b/examples/chatbots/information-gather-prompting.ipynb @@ -42,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 21, "id": "5f795b78-004d-40ca-95d6-069f67e4f9c9", "metadata": {}, "outputs": [], @@ -77,7 +77,11 @@ "llm = ChatOpenAI(temperature=0)\n", "llm_with_tool = llm.bind_tools([PromptInstructions])\n", "\n", - "chain = get_messages_info | llm_with_tool" + "\n", + "def info_chain(state):\n", + " messages = get_messages_info(state['messages'])\n", + " response = llm_with_tool.invoke(messages)\n", + " return {\"messages\": [response]}" ] }, { @@ -93,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "ca9a0234-bbeb-4bff-8276-8dde499c3390", "metadata": {}, "outputs": [], @@ -121,7 +125,10 @@ " return [SystemMessage(content=prompt_system.format(reqs=tool_call))] + other_msgs\n", "\n", "\n", - "prompt_gen_chain = get_prompt_messages | llm" + "def prompt_gen_chain(state):\n", + " messages = get_prompt_messages(state['messages'])\n", + " response = llm.invoke(messages)\n", + " return {\"messages\": [response]}" ] }, { @@ -140,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "id": "74f29e15-20e2-420c-a450-84e929f16e4e", "metadata": {}, "outputs": [], @@ -150,7 +157,8 @@ "from langgraph.graph import END\n", "\n", "\n", - "def get_state(messages) -> Literal[\"add_tool_message\", \"info\", \"__end__\"]:\n", + "def get_state(state) -> Literal[\"add_tool_message\", \"info\", \"__end__\"]:\n", + " messages = state['messages']\n", " if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:\n", " return \"add_tool_message\"\n", " elif not isinstance(messages[-1], HumanMessage):\n", @@ -171,7 +179,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "id": "59d9d6b4-dce4-43cc-9a1a-61a7912ed5b8", "metadata": {}, "outputs": [], @@ -187,15 +195,15 @@ "\n", "memory = MemorySaver()\n", "workflow = StateGraph(State)\n", - "workflow.add_node(\"info\", chain)\n", + "workflow.add_node(\"info\", info_chain)\n", "workflow.add_node(\"prompt\", prompt_gen_chain)\n", "\n", "\n", "@workflow.add_node\n", - "def add_tool_message(state: list):\n", - " return ToolMessage(\n", - " content=\"Prompt generated!\", tool_call_id=state[-1].tool_calls[0][\"id\"]\n", - " )\n", + "def add_tool_message(state: State):\n", + " return {\"messages\": [ToolMessage(\n", + " content=\"Prompt generated!\", tool_call_id=state['messages'][-1].tool_calls[0][\"id\"]\n", + " )]}\n", "\n", "\n", "workflow.add_conditional_edges(\"info\", get_state)\n", @@ -207,13 +215,13 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 29, "id": "1b1613e0", "metadata": {}, "outputs": [ { "data": { - "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAGCANIDASIAAhEBAxEB/8QAHQABAAIDAQEBAQAAAAAAAAAAAAYHBAUIAwIJAf/EAFMQAAEEAQIDBAYFAw8KBQUAAAEAAgMEBQYRBxIhEzFV0QgVFiJBlBQXMlGTQmFxIzM1NjdSVHR2gZGys9LhCSRDVnJ1kqGxtERFV4KVU2JzhcH/xAAbAQEAAgMBAQAAAAAAAAAAAAAAAgMBBAUGB//EADcRAAIBAgIHBgMHBQEAAAAAAAABAgMRBBMSFSExUVKRFEFTcaHwImHBBTI0YoHR4TNCY3Kxsv/aAAwDAQACEQMRAD8A/VNERAEREAREQBERAEREAREQGJeytLGcn0y5Xqc+/J28rWc23ftueveP6Vi+1WE8YofNM81C+I9Ovd11pxliCOwwY3IODZWBwB7Wp12KwPZ/F+G0/wABvktTE4yjhXGM4ttq+y3Fr6HSoYPOgp6Viw/arCeMUPmmeae1WE8YofNM81Xns/i/Daf4DfJPZ/F+G0/wG+S1Na4fkl1Rfq783oWH7VYTxih80zzT2qwnjFD5pnmq89n8X4bT/Ab5J7P4vw2n+A3yTWuH5JdUNXfm9Cw/arCeMUPmmeae1WE8YofNM81Xns/i/Daf4DfJPZ/F+G0/wG+Sa1w/JLqhq783oWH7VYTxih80zzT2qwnjFD5pnmq89n8X4bT/AAG+Sez+L8Np/gN8k1rh+SXVDV35vQsWPU+Hle1jMtRe9xAa1tlhJP3DqtmqS1PhsfWxbZIqNaKRtqsWvZC0Efq8fcQFdq6VGtDEUlVgmtrW35JP6mjiKGRJK97hERWGoEREAREQBERAEREAREQBERAEREBXevf2+6c/3ZkP7WmvNemvf2+6c/3ZkP7Wmo/qbXWm9FfRvaHUOKwP0nm7D1ndirdry7c3LzuHNtzN327tx96859rJutBLl+rPSYNpUE2bxRjiFxBx3DbBRZPIw27ZsWoaNWnQiEk9mxK7ljjYCQNyfvIHTvWv+vPhvsT9YOlth039dVv760WuNW6V4n6TvYjAwYTik8ujfZwmOzVYStj5h+qtfz+65p2IO7eu2zgVyI03pLSTsbUpqz0XtNZr7jnmdPjQE2O0XnHDPZaSnaoWq8LLcbWRyns2h07WiRxYHNO5aWNcdwS3eRa340QaBY2fI6T1RNQjptvXb9OiyWCjGd+btXCTqWBpLhGH7Ab9xCriHQPEWpoXRl61QmzeX07qiTKQYa3k45LYxzmTRRwusuPJJKxsoO5dsQNubcLC4ocMdW8Q81qSfJaHbnhmcLDWwv0/Kwivp6cwubNzs5jzP7Qh4kja8nYDdoC2lCk2k7W29/z8+BrudRJtXv5fItTUPG/F4XVtTTdHC5rUmWt4tuYrx4eCJ7ZK5eWc3O+RgbsRv7xAPM0AknZa3h1xczOruJuttOXdM5CrQw+QFWvfDIRFG0QMf+rETFxc8uLm8rduVzN+U7hYXDjRGocZxDwOayWKdRqwaGqYecunieY7bJi58XuuJOw68w90/fv0X3iGZjhlxS1zkspjIG6Nz1qDJO1HLkYIIaHJVZC5kzJHB324m7FoI2f1I2VejBJxW12499yd57G91y4UUIHHPhue7iDpY/8A7qt/fXtQ4y6Ayt6vSpa503cuWJGww14MvXfJK9x2a1rQ/ckkgADqSVr5c+DL9OPE3Grf2GH8arf28auFU9q39hh/Gq39vGrhXq/sz8Gv9pf8icT7Q+/HyCIi6JygiIgCIiAIiIAiIgCIiAIiIAiIgK717+33Tn+7Mh/a01jy14p9u0jZJt3c7QdlK9T6IpaquUrVixcq2KkcsUclObszyyFhcD069Y2f0LVfVTR8Yzfzv+C0MXgVi5Rmp2srbnxf7nXw+LhSpqEkzS+r6v8ABofwwvuKtDASY4mRk9CWtAW3+qmj4xm/nf8ABPqpo+MZv53/AAWhqh+KujNjt9LgzWotl9VNHxjN/O/4KouOlW7oHVvCnH4nN5RlbUepY8XfEtjnLoDG5xDTt7p3A6pqf/KujM6wpcGWWvl7GyNLXtDmnvBG4K2n1U0fGM387/gn1U0fGM387/gmp/8AKujMawpcGaX6BV/g0P8AwBf1tKuxwc2vE1wO4IYNwtz9VNHxjN/O/wCCfVTR8Yzfzv8AgmqH4q6MdvpcGRTVv7DD+NVv7eNXCoQeEuMkdH22Sy9hjJGSdnLc3a4tcHDcbdRuApuuxh6Cw1BUtK7u31SX0Obiq8a8k4hERXGkEREAREQBERAEREAREQBERAEREAREQBERAFzv6VH7oXAL+WsP9k9dELnf0qP3QuAX8tYf7J6A6IREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBc7+lR+6FwC/lrD/ZPXRC5E9JPjVw8zuuuCNjG680zkIMfq+Kzclq5ivK2tEIngySFryGN3IHMdggOu0Ud0jxH0nxA+l+y+qMNqT6JyfSPVGQhtdjz83Jz9m48vNyu237+U/cVIkAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAERRnUOu6mFtPo1q82VybRu6vX2DYtxuO0kPus3BBA+0QQQCOqnGLm7IlGLk7RRJl+HHpScFZuAvGjO6YDHjFF/0zFSOO/aVJCTH1PUluzmE/FzHL9f5Nb6qldzR47D1m/vH2JZSP5wxv8A0VUcZODcHHTUOlMzqehjn29O2O3gbWke1llnM1xhnDmuL492joCD1dsRuVZlcZLqbPZK3A2PoH8C/qV4G0Zb9Ywak1Dy5PIc7dnxtI/UYT8RysO5B7nPeF0cq2Gs9Wg9a2FI+4OmCzKfEm5UeBmsMYYOgNvGymy1v53MLWvA/wBkO+/p12ZTf3Wn+ph4arFXcSeovGndr5GpDaqTx2a0zQ+OaF4ex7T3EEdCPzr2VLVtjNUIo7LxE0xDraHR7s7R9qJoHWWYkTA2OyA3LiwdQNuo323AO3coPj+I2u+JPD3P39IaRdo/Uta8K2Nr6+ryRwWog5nPM5sLudrS0yBuxPVrT1BWAW0irl2o+IOO1lw8xNvAUL+NyOOsnUuVxpf2NC7HFE5nZc7geye8ytAIc47t+zynexkAREQBERAEREAREQBERAEREAREQBERARnXeoZ8Lj69ai4Myd+XsIHkA9k3beSXY9DytB2BBBcWg9CVD6FCHG1mwQNIaCXOc5xc97id3Pc49XOJJJcepJJPVZ+uXufr/Fsd9iPGTuj3HxdLGHf8mt/pXgrKvwxjFd+1+/L6newUFGnpd7NXhdTY3UNnK16FgzzYu0aVtpjezs5gxr+XdwHN7r2ndu469/eszI5CtiaFm9dnjq060Tppp5XBrI2NBLnOJ7gACSVy5Pkc5UpZfD1tU56GP61K+HZbdkZJbEdSStCXQtfIXe7u9xDe4E7gbr54nUbVbRfHzSUuczV3E4XFVMlRNvJTSzxmaCYyROlLud8RMQPI4ke8R3LXsXZrtu97f2OktH6zxOvMOMphZprFEvLGyzVJq/P0B3aJWtLmkEbOA2PwJW7VCZLE25tW8POHMWos7jNPy4a5lJ7UWTlF69Kx8QbD9JJ7QNb2znbB3cGjuGylfALOZHI4nVmKyGSsZluntRW8PVyFt3PNPAwRvb2j/wAt7TIWF3x5OvXdYLIzu7MsaDK5HTNiaHFSV4xlA+GBt3mdWrXnNcYZHNaQ7kfJs17WnclzSNiXE+lrhHmOI3DrD4XiRqSzPlq9w3Lc+k7EuMhsjd/LA7ldzOjAe34gkxtPQ776vWT3RabuSs/XYeSWL7+0a9rmbfn5gFdC25fFTjN79q6W/f0OPjYKM013mC3B41mYkyzcfVblJIhA+8IWid0YJIYX7cxaCTsN9upWciKk55BuM+kcjrTQNujitXT6HuwzQ3Bm4SeWFkUjXvEgD2czCwOBBcB169BspTp/UGN1VhamWxF6DJ4y2wSQW6rw+KVv75pHQhZs0MdiJ8UrGyRPaWuY8bhwPQgj4hVnwAyWGOmsxp7Tek7mktO6Xy1jC0oLgc36SGEPfMwP97kc6RxBcST1P5kBZ6IiAIiIAiIgCIiAIiIAiIgCIiAIiICE8S8e+JmPzsTXOGOL2WWtP/h5OXnf/wCwtY8//a138+ma4PaHNILSNwR8VINd8VdO8OcjpvH5mxO2/qG83H46rVrSTyTSHbmOzAdmtBBc49AFrspoK7h3uk06IZaPU+qp3Fgi/NC/qGt+6MjYb7NLQA1XWVWKjezR08LiY01oT3EGfwr0vJNLK7F7vlzTNQvP0iXrfY1rWzfa+DWNHL9np3d6+8twy01nTqY3sb251JVipZX9Xlb9Jhja9rG9HDk2Ej+rNid+p6BbuSxlq7uWfTGXY/49nHHKP6WPK+PWGQ/1bzfyo/vKPZ6vcvVHSzKPFGo1fw003ruhQqZrHfSY6DuepLFPLBNXPLyksljc17dx0Ozuvx3Wx0rpLEaIwdfD4KhFjcbBvyQRb95JLnEncucSSS4kkk9SvcX8g47DTea3/PWA/wCrlm08LqTMyBkWMbhYDtzWci9r3gfHlijcdz8Pec37+vxx2ep/ds82jDrUY/FdHgMe7UWdx+JjG8LJY7t1wP63Cx3M0H/8j2Bu3xaJD+SQqG1D/lT+G2LdLDR0xqq5bjdyuZZrwV2gjvBJlc4EfcWro3K6y0nwXu6ZwuVs2obWprxp178tZ8gsWthyiaVjeVjnbhrQdh02aA1vTmL06PQg+sZlviBoCkBqljTJk8TC3b1i0Akyxgf6b72/l/D3vt5m1ZQjuXu5xK9bOnpdxZXoqemLb9KLU+oaVXQj8BhcNWZLLlJMsydxlkftFEYeyYRzNZM7mBcB2ex+0F0suOfQf4Aao4ZejlmshRlk0xxD1RK6XkzuMez6AIJHxwxyQuIc7cdo/mO3SZuwPLu6/ruu9WaLi0Jj83pS1qfIZeRlPMZTS8W9PGzOLG9o5kju07Hd7iXfktYSdugVZrEr1wMvLpHMQaetVKWorFSWHGT3nFsLLLmERuds1xIDtjsAd9u5fWiamaoaPwlbUl6PJ6hipxNyNyGNrGTWAwdo5rWgANLt9gAOm3RVpreLB8ZuI9TQ2d0zrKpFpbJVc/Xy5oGLDX5omh7IxY94SAdps5hDd3NIBPKrmQBERAEREAREQBERAEREAREQBEXjYtwVBGZ5o4BI9sbDI8N5nk7NaN+8k9wQHsoNk+IX0riJLoGljc5BekxT7r9QQ0QaVIuJbEO0f7rnkhxDQHDdmx+O0bu1Mxx6xuvdIal09ndDadhuMo08tVyLIrOUha7eVzQ3csieGhvXcOZIe4ggWni8bBh8ZUoVu0+jVYWQRdrK6V/K1oaOZ7iXOOw6lxJPeSUBGOFmhchoLRWOw+a1Le1nla7pJZcxk2t7WSR7nOdygb8rRzENaSSG9N9gApiiIAiIgCIiA+JImSgB7GvDSHDmG+xHcf0qpLbMtwCxOu9W5PM6o4iYi1fbfr4SCrHPYxkTnbTNh2LS6NvMXcvQNbH0G/M428iAxsbfjyuOq3YWSxxWYmTMZPE6KRrXAEBzHAOaevVpAIPQrJUGyfDarU4hWeIdCbLT51uJfQOJjyLmUroaS6IOjd7rXAlwa7oB2jiQT1Xtww1vlNX6NxOQ1Pp6bReftukikwt2dj5BJG5wcYyD7zSGlwOwPL17tiQJmiIgCIiAIiIAiIgCIiAIiIAiIgNVn9QV8LCyL6RSGVtNezH0rdpsBuTBpIjaTuevTcgHYddiq8wHDq1xVwGjM7xd03jYtX4S5JkquPoWpJK1ORzj2XMOblkkY0M3J5gHNJadivri7a0TBxD4WR6np3bOcly0rcBLWJEcNnsvedLs4bt5fvB6/BWsgCIiAIiIAiIgCIiAIiIAonrbhbpniJkdPZDO40W7+n7rb+MtMlfHJXlBBOzmkEtdyt5mncHYbjoFLEQFe6U1dqqjqLUNLX1XBYPHOyja2mr1bIDfJxSBxZGY37ETNAAIH2iTyt2bzOsJVTx2taJrZDhqNZU7tueTVlOPBmmSBFkSH9i+TZzfcA5t99/h0KtZAEREAREQBF5z2IqsZkmlZDGO90jg0D+crX+1WFH/AJxQ+aZ5qSjKW5A2iLVe1WE8YofNM809qsJ4xQ+aZ5qWXPlZmzNqi1XtVhPGKHzTPNParCeMUPmmeaZc+VizNqtbqR2Vbp3KHBCq7NirKaAvBxgNjkPZ9oGkHk5uXfYg7b7EL49qsJ4xQ+aZ5p7VYTxih80zzTLnysWZ+YuZ/wAp1xexmWnpZLSOjIMhQnfDJFPjrfaQStJa5vWz0IIIK7e9D3jZqnj/AMJpdW6pxFHETSZGStTbj2SNingZHEDIOd7id5jO3v6coHUgk8demr6L02q/SGwuY0WILOM1hMxuRnqkSQ46yHNbLPMWnZjHNIfuSN3CT47b/oJoSrpHh1o3DaZw2ToQ4zFVWVYGm1GXENG3M479XE7kn4kkplz5WLMmSLVe1WE8YofNM809qsJ4xQ+aZ5plz5WLM2qLVe1WE8YofNM809qsJ4xQ+aZ5plz5WLM2qLVe1WE8YofNM809qsJ4xQ+aZ5plz5WLM2qLEp5ejkSRVu17JHwhla//AKFZag01sZgIiLACIiAg/E3K6oxlrR7dNaeq5+Kzna8GVfZ23o0iHdpZZu4e83Zu3f39xU4Vf8WMVeylzRDqetGaPbW1DWnnifLyet4wHb0R7zeYybjp1+z3FWAgCIiAKI6u1dPUt+qcTyHIFofPZkHNHUYe7p+VI78lvcBu53Tla+VWJ2Va8s0h2jjaXuP5gNyqh00+S3imZGfY28kfps7hv1c8Agdf3reVo/M0K2NoxdR927z9/TuN3C0VVn8W5B+mqNubt8jGcvbI2NnI7TPPXfoCOVo/M0AfmXr6gxg/8tqfgN8lga01Y7R2LjuR4PL6gkkmELaeFrtmm3IJ5jzOa1rRy/ac4Dcgd5Ci7OPWmfYSbVEseRrshvHFPxMlU/T/AKdz8gqiEE7ykkbAEjY777blVutUlvkzuXhDZuJx6gxnh1T8BvknqDGeHVPwG+SpnT3H17dYcRbupYcnpvTeAxmNsMx2WpxssQySunDtuzLjIZC2INAc7r0Gx3C2GjOLmX1lxwbhH4nNadw400++cbm6UUMj5vpLGNlDmlx25HEcvMNj3tBUcyfMyKqQdi1vUGM8OqfgN8k9QYzw6p+A3yXtkbT6OPs2Y60118MTpG1q/L2kpAJDGcxDeY7bDcgbnqQqb4aekO7K8GoNZ6vw17Enlja18MDHMyEskrmRx1I2Sve47hrdnhp3PxG5DMnzMm5Ri7Mt71BjPDqn4DfJPUGM8OqfgN8lAq/pAadhx2obGcp5XStrBV47dvH5is1lgwyOLYnxiNz2yBzwWDlcTzdCASFrcvxyZkNLayr1sZmdJ6pxun7OXqVc5UjZI+NsbuWZgDnscGvDQWnqCQHN6pmT5mY04Fn+oMZ4dU/Ab5J6gxnh1T8BvksDQeTs5rQ2nchck7a5bx1aeaTlDed7omucdgABuSegGy1OuuKVLQucweGfictmcrmo7MlKrioGSOf2HZl4Je9rW9JAQXEN907kHYFmT5mSukrskvqDGeHVPwG+SeoMZ4dU/Ab5KssL6SuBzJx8pwOo6GPt5IYZ+Qu0mRwVbxf2f0eX9ULg7n2bzNa5gJALt99s7UHH7CafvZUHEZ7IYjESugyedo0hJSpPbt2ge7mD3cm/vljHBux3O4ITMnzMjpwte5P/AFBjPDqn4DfJPUGM8OqfgN8lD8dxkxua1va05i8Pmso2nOyrazFSsx1GvK6MSBjn84d9lzTuGlvUdV5aI41Y/X9e5dxen9QnExQTWK2UlpN7C82J/I4Qcry4uJB5Wua0u2Ow6JmT5mZ0o7ia+oMZ4dU/Ab5J6gxnh1T8BvkoLgeO2GyV3KU8tis1pC1j8c/LyRagqNhL6bDtJMwse8ENJG4JDhuOixtLekDiNTairYmTBZzBCzjJcxDczMEVeGSows3lB7QnY84OxAcB1cAOqZk+LMacOJPZ9K4az1kxdQuHc9sLWvb+hwG4/mK2uK1He0i8GxYsZPCE7SNmPaT1B+/a77T2D4tdu4DqCdg01rp7jviNQZbE1ThM/i6OZeY8Vl8jREVS87kL2hjuYubzMa5ze0azmA6KyVONaa2Sd1wfvYQnTp1o2ZZkUrJo2SRvbJG8BzXtO4cD3EFfShfCy0W4e9idxyYm2asIG/uwljJI29fg1sgYPzMU0UqkdCTS9ruPOTi4ScX3BERVkCqeO1rRNbIcNRrKndtzyaspx4M0yQIsiQ/sXybOb7gHNvvv8OhVrKD8TcrqjGWtHt01p6rn4rOdrwZV9nbejSId2llm7h7zdm7d/f3FThAEREBjZGoMhj7VUnYTxOjJ+7cEf/1VNpV7n6cxzZGuZLFA2GVjhsWyMHK8H9DmkK4lXWqsDLpzI2crUhdNirb+1uRxDd9aXYAyhvxjdt723Vrve2Ic4tuitODprfvX7e+FjoYOqqc3GXeVJx20pm9URaZ+g4ubUmDqX3S5fT8FxlV1+IxPEYLnua1zWSFrjG5wDtvjtsquw3BrWOD0zYmx2lKOOvYnW7dVY3BRXo/o9ms6uInVmyDYRyNDnjdzQ0OaNiQd10/Wsw3YGT15WTwyDmZJE4Oa4feCOhXotXatjOvKkpPSOZ9ScNdd8RctrnOyaaZp27YiwdrFU79+GZtmajYllfFIYnODebcDfqPeB36ECUYnIZ6Pi27XutMBDoLTtbTTsZJaymXqvY2d1uN4DnMfsAe4E9/d0JAV4L5kjZK0te0Pae8OG4WAqSW1MiuK4t6GzuRgoY3WmnsjendyxVamVgllkPfs1rXkk/oVLYbhnrmHhHg9JyacjhyuisrWymOtSX4jVzHYWXPDG8pL4uaMnq9o2dt+ldHsp143BzII2uHcQwAheyEnDS+8zm7WnCrWnF/Jaj1LcwsWlb0WMpUsPirtyOd08le62450zoi5jWucxsbQCSASTt3LcZTQ+seLOpMxmMxgG6OhZpW9gaVWzdisyz2LXLzSOMJc0Rt5GgAnmJO+w7lfKJcjlLiVdobiRhNE6I0/hNb5nB6Q1JRoxVrOKvZmt2jORvI149/q14aHj8zgD1BXnc7HiFxW0BqnTF6hn9PYeHK1b2Qx12GaOGWWOv2bDyuJJPKeg326b7bje0JKkEzuZ8Mb3fe5oJX3FDHC3ljY2Nu++zRsEJaLtZvYc/y8K9UO4ZX8SMXvkJdenNMh+kRdafrUT9rvzbfrQ5uXfm+G2/RaehwPk05qrUNTI8JMHr2rk8zPkauorM1VjooZ5Od0c7ZQZC6MufsWBwcNvsrppEuQyY7Cic9ofUjuMWKyWl9JP0xHBfrtyGoK2UibVyWOZHs+KaoDzOk/IYSw8vKDzgdFoKvDviE7KaqfpXBScNKmQw92N9U5eOzVnyTyOxsVo4yewP2+Z+zPtD3SRuulUQy6Sfecq4zg7dqZubJT8OItN4CfSWSw2UFjNV3WLEsjY3mWeYOd7ruzc0SbucC7mc1oC1PDqN+sslV0trC3kZ8/lNKXcHhb7bGOsVoK7omiZ+1SZ7i8gR7SP2DuXYbEnfr6WJk8T45GNkjeC1zHjcOB7wR8QtJp/QemdJWJ7GD07icNPP0mlx9GKB0nXf3ixoJ/nWbkMmzVmU1wZ4Wu03fwFTMcG8DiMliogybVdSaq5ssjGcrZoWtHa8zyNzzhvLzHqV0EixIH2M/edjcO5kllruWza+1HTHxL/vfsfdZ3npvs3cicISqOyLPhoxu3sJBwurucNQ39nCOzkCyMkbbtiiZG4/8AG2QfzKcrCwuIrYDE1MdTaWVq0bYmBx3cQB3uPxJ7yT1JJKzVdUkpSut279FsR5upLTm5cQiIqisr/ixir2UuaIdT1ozR7a2oa088T5eT1vGA7eiPebzGTcdOv2e4qwFVPHa1omtkOGo1lTu255NWU48GaZIEWRIf2L5NnN9wDm333+HQq1kAREQBERARjJ8N8Dk7MlkVpaNmQ7vmx9iSuXnfclwYQHHf4kFa/wCqfH+L5r50+Sm6K9V6i2aRYqs4qykyEfVPj/F8386fJPqnx/i+b+dPkpuiZ9TiSzqnMyEfVPj/ABfN/OnyT6p8f4vm/nT5KbomfU4jOqczIR9U+P8AF8386fJPqnx/i+b+dPkpuiZ9TiM6pzM5q4zUreiOJPCTC4vN5RlHUuZmpZBstnmc6NsJeA07e6d/ire+qfH+L5v50+Sq70lP3avR6/lJZ/7YrohM+pxGdU5mQj6p8f4vm/nT5J9U+P8AF8386fJTdEz6nEZ1TmZCPqnx/i+b+dPkn1T4/wAXzfzp8lN0TPqcRnVOZkI+qfH+L5v50+SfVPj/ABfN/OnyU3RM+pxGdU5mQyLhRhd/86nyd9nxjsZCXkP6WtIB/Qd1K6GPq4upHVp14qtaMbMihYGNb+gBZCKEqk5q0nsISnKX3ncIiKsgEREBB+JuV1RjLWj26a09Vz8VnO14Mq+ztvRpEO7Syzdw95uzdu/v7ipwq/4sYq9lLmiHU9aM0e2tqGtPPE+Xk9bxgO3oj3m8xk3HTr9nuKsBAEREAREQBERAEREAREQBERAc7+kp+7V6PX8pLP8A2xXRC539JT92r0ev5SWf+2K6IQBERAEREAREQBERAEREAREQFU8drWia2Q4ajWVO7bnk1ZTjwZpkgRZEh/Yvk2c33AObfff4dCrWUH4m5XVGMtaPbprT1XPxWc7Xgyr7O29GkQ7tLLN3D3m7N27+/uKnCAIiIAiIgCIiAIiIAiIgCxslkqmGx1q/ftQ0aFWJ89i1ZkEcUMbQXOe9xIDWgAkk9AAslY2SxtXMY61QvQR2qVqJ8E8Eo3ZJG4FrmuHxBBIP6UByb6QXGrh5muLvAu5j9eaZvU8dn55rtitmK8kdWM1yA+VzXkMaT03dsN11LprV2C1nRfd0/msdnabHiN9jG2o7EbXljXhpcwkA8j2O2+5zT3EL8Q/SR4N2eBXGPP6Ska80oZu3x0zzuZqknWJ2/wASB7rj++a5fq16FHBebgfwDw+LvNfHmco85jIwv6GGaVjAI9vgWxsjaR++DvvQF8IiIAiIgCIiAIiIAiIgCIiAr/ixir2UuaIdT1ozR7a2oa088T5eT1vGA7eiPebzGTcdOv2e4qwFVPHa1omtkOGo1lTu255NWU48GaZIEWRIf2L5NnN9wDm333+HQq1kAREQBERAEREAREQGFl8zSwNF9y/YbXrtIHM7clzj0DWgdXOJ6BoBJPcFCrPEXMXXE4vBRwQfkzZWz2b3fnEbGu2Hx95wP3gfDU+tXauvnNSOLqm5GOi5t2Nh6gTAfvpB13+DS0dPe3yldJxovRtd99zsUMHFx0qh6nWWrdztXwu35zMv57Zau/g+F/pmXmijnvlXQ2uyUeBVvFjg1Bxm1tpHVOoqGOdktNTdrA2vI9sdpoe17YpwWkvYHNJDQW/ad8CVa3tlq7+D4X+mZeaJnvlXQdko8D09stXfwfC/0zL7j1zqiA80uLxVto72RWpIXH9G7HD+nb+bvXgsNmaoSZeXFMuQOycULbMlRsgMrInEta8t7wCWuAPx5T9yZ/GK6B4SjwJzpzW1HUExqOjlx2Ta0uNK2AHuaNt3MIJa9o3HVpO243AJ2UhVU3aYuxNAkkrzRu54bEJ2khfsQHtP39SPuIJBBBIM40XqJ+pMKJrDGRX68rq1uOM+62VvxH5nNLXgd4Dxv13WWozjpxVuK99xy8Th8nbHcb5ERVGiEREAREQBERAQfibldUYy1o9umtPVc/FZzteDKvs7b0aRDu0ss3cPebs3bv7+4qcKv+LGKvZS5oh1PWjNHtrahrTzxPl5PW8YDt6I95vMZNx06/Z7irAQBERAEREAREQBaLXlmanofUU9ckTxY6w+Mjv5hE4j/mt6vOxXjt15YJmCSKRpY9h7nNI2IU4SUZKT7jK2Mq3Gwx18dVih2ETImtZsNhygABVTxhht5fihwwwTMzlsVjslJkvpjMVekqunEdYPaHOYQehH6R8CFZ2HrTYmJ+GtuLrmN2gc55BdLGOkcv8A72jf/aDh+SVBOKnCFnE/WGibl3s5MLhn3H3IRZmgncZYQyMxOi2IIcASeZvT7+5QqRcZtM9NJ6cE4/L/AKU5nc9qSpHltH0dW5h1XGa/xGJqZz6UX2xBYZG+WB8n+l7Nz3D3+bfoHb7KcZLTdnIcXMdw7bqfUlDT1PASZoyQZicXbth9ox7PslxkLIxtswED3279AArEx3B7R+JwGNwtPCR18dj8jHloImSyB30tjudsz383NI7m2J5yd9hvuFk634Yaa4imk7PY42pqfP8AR7EFmWtNEHAB7WyROa8NdsN277HYbhVEFTlb30OftKZ7Pa81HoLTmQ1TmjQZc1LjLF7H3XVpMpDUmhbBI98ex5tvy27O+11952+PibGewWjYNTjWGo72QxeuvULI7uRfJBNRGR+i9lLH9mRxY4ntHAv322cAAF0XjOGemMLNp2XH4iGkdPQT1sY2BzmMrxzBvajlB2cXcjSS4E77nfcnfzdwr0u7Cy4k4vfHy5T10+H6RL1udv2/a782/wCujm5d+X4bbdFkxlS737sihMuMq/RXGTWY1pqHH5XTOcyTsY0ZST6JGIY43xwGAkxvY5x5eVwP2umykWkdPR6h9JfIZq5Zy1O8/S2JyTqkOTnjiEjpJ2ujdGHhroxyj3CC3cuO27iTvdN+jVgJM/qbL6rx1bL2r+oZ8tVYy3OYBE7kMQmh3bG97S1x95rgNx1Kn+f4Zab1PqjGaiyGOMmbxoa2tchsywvDWvDwx/ZuaJGBw5uV4cN9+nUoI05b373koWbw2e5updUxD9a/zWU7f/ULHtd/PysZ/wAlrrFiKpXlnnkbDDE0vfI87Na0Dckn4ABSXhxh56OJtX7kT4LeTnNkwyfaijDQyJh+48jQ4j4Oe4fnWxS2Qm33q363T+hTjZJU7cSWoiKs4QREQBERAEREBVPHa1omtkOGo1lTu255NWU48GaZIEWRIf2L5NnN9wDm333+HQq1lB+JuV1RjLWj26a09Vz8VnO14Mq+ztvRpEO7Syzdw95uzdu/v7ipwgCIiAIiIAiIgCIiA0Op9Jw6iZHLHKaOSgBENxjA4gfFjgftMPxbuPvBBAIhFmjqTFOLLeBffaO6zipmPY79LHua9p/MA4D7z8bVRWqataSv7+Rs0sRUpbIvYVEb+QBI9nM0dvuqj+8v56wyH+reb+VH95W8izpUuT1Njt1TgiiM5xDqaaymGxuUxuUpXszO6tj4JavvWZA3mLW9e8Dqtz6wyH+reb+VH95RT0lf3a/R6/lJZ/7YrolNKlyeo7dU4IqH1hkP9W838qP7y9I5cxa92tpjKveR07YRQt/nL3j/AJAq2kTSp8nqx26pwRBsFoW1YsxXNQPhcInc8OMrnnha4fZdI4gGRw7wNg0Hrs4hrhOURQlNy8jSnUlUelJhERQKwiIgCIiAIiICv+LGKvZS5oh1PWjNHtrahrTzxPl5PW8YDt6I95vMZNx06/Z7irAVU8drWia2Q4ajWVO7bnk1ZTjwZpkgRZEh/Yvk2c33AObfff4dCrWQBERAEREAREQBERAEREAREQHO3pK/u1+j1/KSz/2xXRK5s9KrKU8FxY4AZHJWoaGPg1LOJbVl4jij5q+zeZx6Dc/eukgQQCDuCgP6iIgCIiAIiIAiIgCIiAIiICD8TcrqjGWtHt01p6rn4rOdrwZV9nbejSId2llm7h7zdm7d/f3FThVjxVmGZ1joDT+O19DpXNsy8eVkxLZf1fL04WvM1fkDgeRw6kkEe4eh2VnIAiIgCIiAIiIAiIgCIiAIiIDSay0VguIWnLmA1Hi6+YxFxnJNVss5mu+4j4tcO8OGxB6ggrnQ1te+h4S+qMjxI4MxdTW37XMaejHfyH/TwNHw72gfkhpLupkQGg0Lr3T/ABL0zU1BpjK18xiLTd47Nd243+LXA9WuHxa4Aj4hb9c8669HjM6I1Nc19wSuV9O6jnPaZPTFgbYjN7ddnMGwhlPXaRu3U9duZzj64z00tFVdEalymrq9/R+pNMQtfmNK3Iua9G9zmsYIB0EzHvexrZBs332lxYDugOgUVYejnx2xvpE8MKersfV9WzOlkrXMaZxM6pMw9WF4a3m3aWOB2HR46Kz0AREQBERAERUPwn9MnQXFziZqnROOsvq5PFTyNoPlIczMQxt/VZK/L3ua4SfqfUujDXjf32xgXwq0zPE9mtXa30lw3zWMm1/gYY2SesYZXU6s0hIDZHNGznANdu1pOx23HeFp4s3k/SN0hgczovUOc0FiIcz2lqW1ixFZyVWFxIEXafZjkcGHmIO7eZrm97TbdXHVKU1qWvVhry2pO2sPijDXTP5Q3meR9o8rWjc9dmgfBARrAcPqUN3Dahz9XG5jXVPGsx82oY6TYpJB3v5BuTG1zi48oP5RHcpaiIAiIgCIiAIiIAiIgCIiAIiIAiIgP45wa0ucQABuSfgvzJ/yi2nNdcUuLFWxhtMHJaXw2PZBTvYt0FqS29/vyvIjHajZxEYjduB2Ze3btDv+lWb/AGFv/wAXk/qlVDpfBY2TTOIc7HVHOdThJJgaSTyD8yxOdOjT053e22w5+MxiwcVJxvc4T/ye3E3McGOMUuldR0b2LwOp2CB5uwviZXtM3MMh5gNg7d0Z/O9pJ2av1C9rMJ4zj/mmearz2fxfhtP8Bvkns/i/Daf4DfJavbaHK/Q5eu4eG+v8Fh+1mE8Zx/zTPNPazCeM4/5pnmq89n8X4bT/AAG+Sez+L8Np/gN8k7bQ5X6DXcPDfX+Cw/azCeM4/wCaZ5p7WYTxnH/NM81Xns/i/Daf4DfJPZ/F+G0/wG+Sdtocr9BruHhvr/BDfTP46jhxwLyx0zY+n6hzLhiqZx57Z0Bka7nlPJvykRtfyn98W/cV+XHCvQ3FbDazxWc0Zhc1jM5RmE1XICuYGRu22Ic+QBhaQS1zXbhzSQQQSF+vPs/i/Daf4DfJPZ/F+G0/wG+Sdtocr9BruHhvr/BLOF2qMnq3QuGvZ6nBjNRurMGUoV54pmQWQNpOR0UsreQuBcz3yeUt5tnbgSxVzwxqwU9TapjrwxwR8tQ8kbQ0b8snwCsZbsrbHHc0n1SZ36VRVacaiVrq4REUS0IiIAiIgCIiAIiIAiIgCIiAIiIDCzf7C3/4vJ/VKq/Sn7VsP/E4f6gVoZv9hb/8Xk/qlVdpVwbpXDkkAClCST8PcC1cb+HXn9Dzn21/Th5s2yKD/Xpw2/8AULSv/wA1W/vp9enDb/1C0r/81W/vrh6MuB5fKqcr6Gr1Lx6w2nMjloW4XP5fH4ZxZlcvi6ImqUHBoc9r3cwc4saQ5wja/lHfsei8c76QuFw+SzlSrhM9nm4WtDevWcVVjkhirSxdq2XmdI3mHLv7o3ceU7NIG6rRnBh2H1hqmaxwowXEmhn8tLl6OfnsVWOgjnIc+GbtQXlrHcxaYw/cEdAp5T4c5XH6j4uPrYtlfGZnFUqeIbHJG1khiqSxFgaD7gaXNb7wA+7or9GmvfkbjhQj89nH5r+eBvdQccMJibeGp43H5bVeQytFuUgp4Ks2aRtM7cs7+dzGtYSdhudydwAdl8+jzrDKa+4Q4PPZmw+1kbb7XaSSQtids2zKxgLGgAENa0d3w69VXui9Fa54VZPTWYpaWGoxa0ji8JlKMWQghnoWqrD1Dnu5HxntHA8pJ3buNwt9wb1JhuD/AAywmmtd5/BaV1HCbM82Nv5es2RjZLUz2Ee/1Ba4bEf8iCFiUY6No7fbMVKcFTap7XdfN999nQupFCPry4cBod9YGluUnYH11W23/wCP84W/03rLAaygmn0/nMbnIYXBksmNtx2GscRuA4sJAO3wKpcWt6NJwnFXaN/w5/bVqn/Yqf1ZFYKr7hz+2rVP+xU/qyKwV6f+2H+sf/KPoOD/AA9PyQREWDcCIiAIiIAiIgCIiAIiIAiIgCIiAws3+wt/+Lyf1Sqv0p+1bD/xOH+oFbNiBlqvLDJuWSNLHbfcRsVCa/CHGVYI4Icrmo4o2hjGC6dmtA2A7lGrSjXpaDlbbc5ePwksZCMYu1jU+r6v8Gh/DCer6v8ABofwwt19VFDxjN/PHyT6qKHjGb+ePktHV68T0ZxtS1edeprAAAABsAv6tl9VFDxjN/PHyT6qKHjGb+ePkmr4+IujMalq869TWrylqQTO5pIY5Hd27mglbf6qKHjGb+ePkn1UUPGM388fJNXrxF0Y1LV516ml9X1f4ND+GF6RQRwAiONkYPeGNAW2+qih4xm/nj5J9VFDxjN/PHyTV68RdGNS1edepicOf21ap/2Kn9WRWCtBpfRlLSklyStPbszWyztZLc3aOPKCGgH4d5W/XTlZWSd7JLokj09Cm6VKNN9ySCIigXhERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQH//2Q==", + "image/jpeg": "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", "text/plain": [ "" ] @@ -240,63 +248,105 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 30, "id": "25793988-45a2-4e65-b33c-64e72aadb10e", "metadata": {}, "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): hi\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Hello! How can I assist you today?\n", + "Hello! How can I assist you today?\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): rag prompt\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Sure! I can help you with that. To create an extraction prompt, I need some information from you. Could you please provide the following details:\n", + "Sure! I can help you create a prompt template. To get started, could you please provide me with the following information:\n", "\n", "1. What is the objective of the prompt?\n", "2. What variables will be passed into the prompt template?\n", "3. Any constraints for what the output should NOT do?\n", "4. Any requirements that the output MUST adhere to?\n", "\n", - "Once I have this information, I can create the extraction prompt for you.\n", + "Once I have this information, I can assist you in creating the prompt template.\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): 1 rag, 2 none, 3 no, 4 no\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "Great! To create an extraction prompt for filling out a CSAT (Customer Satisfaction) survey, I will need the following information:\n", - "\n", - "1. Objective: To gather feedback on customer satisfaction.\n", - "2. Variables: Customer name, Date of interaction, Service provided, Rating (scale of 1-5), Comments.\n", - "3. Constraints: The output should not include any personally identifiable information (PII) of the customer.\n", - "4. Requirements: The output must include a structured format with fields for each variable mentioned above.\n", - "\n", - "With this information, I will proceed to create the extraction prompt template for filling out a CSAT survey. Let's get started!\n", "Tool Calls:\n", - " PromptInstructions (call_aU48Bjo7X29tXfRtCcrXkrqq)\n", - " Call ID: call_aU48Bjo7X29tXfRtCcrXkrqq\n", + " PromptInstructions (call_7qkSORledsemoCnK8A3RKvAb)\n", + " Call ID: call_7qkSORledsemoCnK8A3RKvAb\n", " Args:\n", - " objective: To gather feedback on customer satisfaction.\n", - " variables: ['Customer name', 'Date of interaction', 'Service provided', 'Rating (scale of 1-5)', 'Comments']\n", - " constraints: ['The output should not include any personally identifiable information (PII) of the customer.']\n", - " requirements: ['The output must include a structured format with fields for each variable mentioned above.']\n", + " objective: rag\n", + " variables: ['none']\n", + " constraints: ['no']\n", + " requirements: ['no']\n", "=================================\u001b[1m Tool Message \u001b[0m=================================\n", "\n", "Prompt generated!\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Please provide feedback on your recent interaction with our service. Your input is valuable to us in improving our services.\n", - "\n", - "Customer name: \n", - "Date of interaction: \n", - "Service provided: \n", - "Rating (scale of 1-5): \n", - "Comments: \n", - "\n", - "Please note that the output should not include any personally identifiable information (PII) of the customer. Your feedback will be kept confidential and used for internal evaluation purposes only. Thank you for taking the time to share your thoughts with us.\n", - "Done!\n", + "Please write a response using the RAG (Red, Amber, Green) rating system.\n", + "Done!\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): red\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "I'm glad you found it helpful! If you need any more assistance or have any other requests, feel free to let me know. Have a great day!\n", + "Thank you for providing the response. If you need any more assistance, feel free to ask!\n" + ] + }, + { + "name": "stdin", + "output_type": "stream", + "text": [ + "User (q/Q to quit): q\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "AI: Byebye\n" ] } @@ -312,9 +362,9 @@ " break\n", " output = None\n", " for output in graph.stream(\n", - " [HumanMessage(content=user)], config=config, stream_mode=\"updates\"\n", + " {\"messages\": [HumanMessage(content=user)]}, config=config, stream_mode=\"updates\"\n", " ):\n", - " last_message = next(iter(output.values()))\n", + " last_message = next(iter(output.values()))['messages'][-1]\n", " last_message.pretty_print()\n", "\n", " if output and \"prompt\" in output:\n", @@ -344,7 +394,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.11.1" } }, "nbformat": 4, From 9a306ce9319d6ae599a99d7046a310579c91e41e Mon Sep 17 00:00:00 2001 From: vbarda Date: Thu, 22 Aug 2024 11:47:36 -0400 Subject: [PATCH 069/287] langgraph: check for empty checkpoint when creating snapshot tasks --- libs/langgraph/langgraph/pregel/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 563f097f8..270a3c7ba 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -371,7 +371,7 @@ class Pregel( saved.metadata if saved else None, saved.checkpoint["ts"] if saved else None, saved.parent_config if saved else None, - tasks_w_writes(next_tasks, saved.pending_writes), + tasks_w_writes(next_tasks, saved.pending_writes if saved else None), ) async def aget_state(self, config: RunnableConfig) -> StateSnapshot: @@ -402,7 +402,7 @@ class Pregel( saved.metadata if saved else None, saved.checkpoint["ts"] if saved else None, saved.parent_config if saved else None, - tasks_w_writes(next_tasks, saved.pending_writes), + tasks_w_writes(next_tasks, saved.pending_writes if saved else None), ) def get_state_history( From 7ec3c0e827a6bfa574bde1d2b09f203296dbac68 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Thu, 22 Aug 2024 11:54:47 -0400 Subject: [PATCH 070/287] langgraph: release 0.2.11 (#1432) --- 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 9b38e6049..1c3b20e9b 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.10" +version = "0.2.11" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From 4e2b508ebb91bf517625c89c0d0aaa6e5aef722c Mon Sep 17 00:00:00 2001 From: Hassan Memon Date: Thu, 22 Aug 2024 21:27:23 +0500 Subject: [PATCH 071/287] docs: remove extra line from generate function (#1392) --- examples/rag/langgraph_agentic_rag.ipynb | 316 ++++++++++++++++++++++- 1 file changed, 306 insertions(+), 10 deletions(-) diff --git a/examples/rag/langgraph_agentic_rag.ipynb b/examples/rag/langgraph_agentic_rag.ipynb index 486c6caed..73d02f14a 100644 --- a/examples/rag/langgraph_agentic_rag.ipynb +++ b/examples/rag/langgraph_agentic_rag.ipynb @@ -20,7 +20,10 @@ "id": "969fb438", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters"] + "source": [ + "%%capture --no-stderr\n", + "%pip install -U --quiet langchain-community tiktoken langchain-openai langchainhub chromadb langchain langgraph langchain-text-splitters" + ] }, { "cell_type": "code", @@ -28,7 +31,22 @@ "id": "e4958a8c", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_env(key: str):\n if key not in os.environ:\n os.environ[key] = getpass.getpass(f\"{key}:\")\n\n\n_set_env(\"OPENAI_API_KEY\")\n\n# (Optional) For tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(key: str):\n", + " if key not in os.environ:\n", + " os.environ[key] = getpass.getpass(f\"{key}:\")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")\n", + "\n", + "# (Optional) For tracing\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] }, { "cell_type": "markdown", @@ -46,7 +64,34 @@ "id": "e50c9efe-4abe-42fa-b35a-05eeeede9ec6", "metadata": {}, "outputs": [], - "source": ["from langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.vectorstores import Chroma\nfrom langchain_openai import OpenAIEmbeddings\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter\n\nurls = [\n \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n]\n\ndocs = [WebBaseLoader(url).load() for url in urls]\ndocs_list = [item for sublist in docs for item in sublist]\n\ntext_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n chunk_size=100, chunk_overlap=50\n)\ndoc_splits = text_splitter.split_documents(docs_list)\n\n# Add to vectorDB\nvectorstore = Chroma.from_documents(\n documents=doc_splits,\n collection_name=\"rag-chroma\",\n embedding=OpenAIEmbeddings(),\n)\nretriever = vectorstore.as_retriever()"] + "source": [ + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.vectorstores import Chroma\n", + "from langchain_openai import OpenAIEmbeddings\n", + "from langchain_text_splitters import RecursiveCharacterTextSplitter\n", + "\n", + "urls = [\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", + " \"https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/\",\n", + " \"https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/\",\n", + "]\n", + "\n", + "docs = [WebBaseLoader(url).load() for url in urls]\n", + "docs_list = [item for sublist in docs for item in sublist]\n", + "\n", + "text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(\n", + " chunk_size=100, chunk_overlap=50\n", + ")\n", + "doc_splits = text_splitter.split_documents(docs_list)\n", + "\n", + "# Add to vectorDB\n", + "vectorstore = Chroma.from_documents(\n", + " documents=doc_splits,\n", + " collection_name=\"rag-chroma\",\n", + " embedding=OpenAIEmbeddings(),\n", + ")\n", + "retriever = vectorstore.as_retriever()" + ] }, { "cell_type": "markdown", @@ -62,7 +107,17 @@ "id": "0b97bdd8-d7e3-444d-ac96-5ef4725f9048", "metadata": {}, "outputs": [], - "source": ["from langchain.tools.retriever import create_retriever_tool\n\nretriever_tool = create_retriever_tool(\n retriever,\n \"retrieve_blog_posts\",\n \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n)\n\ntools = [retriever_tool]"] + "source": [ + "from langchain.tools.retriever import create_retriever_tool\n", + "\n", + "retriever_tool = create_retriever_tool(\n", + " retriever,\n", + " \"retrieve_blog_posts\",\n", + " \"Search and return information about Lilian Weng blog posts on LLM agents, prompt engineering, and adversarial attacks on LLMs.\",\n", + ")\n", + "\n", + "tools = [retriever_tool]" + ] }, { "cell_type": "markdown", @@ -86,7 +141,19 @@ "id": "0e378706-47d5-425a-8ba0-57b9acffbd0c", "metadata": {}, "outputs": [], - "source": ["from typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\nfrom langgraph.graph.message import add_messages\n\n\nclass AgentState(TypedDict):\n # The add_messages function defines how an update should be processed\n # Default is to replace. add_messages says \"append\"\n messages: Annotated[Sequence[BaseMessage], add_messages]"] + "source": [ + "from typing import Annotated, Sequence, TypedDict\n", + "\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "from langgraph.graph.message import add_messages\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " # The add_messages function defines how an update should be processed\n", + " # Default is to replace. add_messages says \"append\"\n", + " messages: Annotated[Sequence[BaseMessage], add_messages]" + ] }, { "attachments": { @@ -129,7 +196,173 @@ ] } ], - "source": ["from typing import Annotated, Literal, Sequence, TypedDict\n\nfrom langchain import hub\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import PromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.prebuilt import tools_condition\n\n### Edges\n\n\ndef grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n \"\"\"\n Determines whether the retrieved documents are relevant to the question.\n\n Args:\n state (messages): The current state\n\n Returns:\n str: A decision for whether the documents are relevant or not\n \"\"\"\n\n print(\"---CHECK RELEVANCE---\")\n\n # Data model\n class grade(BaseModel):\n \"\"\"Binary score for relevance check.\"\"\"\n\n binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n\n # LLM\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n\n # LLM with tool and validation\n llm_with_tool = model.with_structured_output(grade)\n\n # Prompt\n prompt = PromptTemplate(\n template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n Here is the retrieved document: \\n\\n {context} \\n\\n\n Here is the user question: {question} \\n\n If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n input_variables=[\"context\", \"question\"],\n )\n\n # Chain\n chain = prompt | llm_with_tool\n\n messages = state[\"messages\"]\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n scored_result = chain.invoke({\"question\": question, \"context\": docs})\n\n score = scored_result.binary_score\n\n if score == \"yes\":\n print(\"---DECISION: DOCS RELEVANT---\")\n return \"generate\"\n\n else:\n print(\"---DECISION: DOCS NOT RELEVANT---\")\n print(score)\n return \"rewrite\"\n\n\n### Nodes\n\n\ndef agent(state):\n \"\"\"\n Invokes the agent model to generate a response based on the current state. Given\n the question, it will decide to retrieve using the retriever tool, or simply end.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with the agent response appended to messages\n \"\"\"\n print(\"---CALL AGENT---\")\n messages = state[\"messages\"]\n model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n model = model.bind_tools(tools)\n response = model.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\ndef rewrite(state):\n \"\"\"\n Transform the query to produce a better question.\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n\n print(\"---TRANSFORM QUERY---\")\n messages = state[\"messages\"]\n question = messages[0].content\n\n msg = [\n HumanMessage(\n content=f\"\"\" \\n \n Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n Here is the initial question:\n \\n ------- \\n\n {question} \n \\n ------- \\n\n Formulate an improved question: \"\"\",\n )\n ]\n\n # Grader\n model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n response = model.invoke(msg)\n return {\"messages\": [response]}\n\n\ndef generate(state):\n \"\"\"\n Generate answer\n\n Args:\n state (messages): The current state\n\n Returns:\n dict: The updated state with re-phrased question\n \"\"\"\n print(\"---GENERATE---\")\n messages = state[\"messages\"]\n question = messages[0].content\n last_message = messages[-1]\n\n question = messages[0].content\n docs = last_message.content\n\n # Prompt\n prompt = hub.pull(\"rlm/rag-prompt\")\n\n # LLM\n llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n\n # Post-processing\n def format_docs(docs):\n return \"\\n\\n\".join(doc.page_content for doc in docs)\n\n # Chain\n rag_chain = prompt | llm | StrOutputParser()\n\n # Run\n response = rag_chain.invoke({\"context\": docs, \"question\": question})\n return {\"messages\": [response]}\n\n\nprint(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\nprompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like"] + "source": [ + "from typing import Annotated, Literal, Sequence, TypedDict\n", + "\n", + "from langchain import hub\n", + "from langchain_core.messages import BaseMessage, HumanMessage\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.prompts import PromptTemplate\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "from langgraph.prebuilt import tools_condition\n", + "\n", + "### Edges\n", + "\n", + "\n", + "def grade_documents(state) -> Literal[\"generate\", \"rewrite\"]:\n", + " \"\"\"\n", + " Determines whether the retrieved documents are relevant to the question.\n", + "\n", + " Args:\n", + " state (messages): The current state\n", + "\n", + " Returns:\n", + " str: A decision for whether the documents are relevant or not\n", + " \"\"\"\n", + "\n", + " print(\"---CHECK RELEVANCE---\")\n", + "\n", + " # Data model\n", + " class grade(BaseModel):\n", + " \"\"\"Binary score for relevance check.\"\"\"\n", + "\n", + " binary_score: str = Field(description=\"Relevance score 'yes' or 'no'\")\n", + "\n", + " # LLM\n", + " model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n", + "\n", + " # LLM with tool and validation\n", + " llm_with_tool = model.with_structured_output(grade)\n", + "\n", + " # Prompt\n", + " prompt = PromptTemplate(\n", + " template=\"\"\"You are a grader assessing relevance of a retrieved document to a user question. \\n \n", + " Here is the retrieved document: \\n\\n {context} \\n\\n\n", + " Here is the user question: {question} \\n\n", + " If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \\n\n", + " Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.\"\"\",\n", + " input_variables=[\"context\", \"question\"],\n", + " )\n", + "\n", + " # Chain\n", + " chain = prompt | llm_with_tool\n", + "\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + "\n", + " question = messages[0].content\n", + " docs = last_message.content\n", + "\n", + " scored_result = chain.invoke({\"question\": question, \"context\": docs})\n", + "\n", + " score = scored_result.binary_score\n", + "\n", + " if score == \"yes\":\n", + " print(\"---DECISION: DOCS RELEVANT---\")\n", + " return \"generate\"\n", + "\n", + " else:\n", + " print(\"---DECISION: DOCS NOT RELEVANT---\")\n", + " print(score)\n", + " return \"rewrite\"\n", + "\n", + "\n", + "### Nodes\n", + "\n", + "\n", + "def agent(state):\n", + " \"\"\"\n", + " Invokes the agent model to generate a response based on the current state. Given\n", + " the question, it will decide to retrieve using the retriever tool, or simply end.\n", + "\n", + " Args:\n", + " state (messages): The current state\n", + "\n", + " Returns:\n", + " dict: The updated state with the agent response appended to messages\n", + " \"\"\"\n", + " print(\"---CALL AGENT---\")\n", + " messages = state[\"messages\"]\n", + " model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4-turbo\")\n", + " model = model.bind_tools(tools)\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "def rewrite(state):\n", + " \"\"\"\n", + " Transform the query to produce a better question.\n", + "\n", + " Args:\n", + " state (messages): The current state\n", + "\n", + " Returns:\n", + " dict: The updated state with re-phrased question\n", + " \"\"\"\n", + "\n", + " print(\"---TRANSFORM QUERY---\")\n", + " messages = state[\"messages\"]\n", + " question = messages[0].content\n", + "\n", + " msg = [\n", + " HumanMessage(\n", + " content=f\"\"\" \\n \n", + " Look at the input and try to reason about the underlying semantic intent / meaning. \\n \n", + " Here is the initial question:\n", + " \\n ------- \\n\n", + " {question} \n", + " \\n ------- \\n\n", + " Formulate an improved question: \"\"\",\n", + " )\n", + " ]\n", + "\n", + " # Grader\n", + " model = ChatOpenAI(temperature=0, model=\"gpt-4-0125-preview\", streaming=True)\n", + " response = model.invoke(msg)\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "def generate(state):\n", + " \"\"\"\n", + " Generate answer\n", + "\n", + " Args:\n", + " state (messages): The current state\n", + "\n", + " Returns:\n", + " dict: The updated state with re-phrased question\n", + " \"\"\"\n", + " print(\"---GENERATE---\")\n", + " messages = state[\"messages\"]\n", + " question = messages[0].content\n", + " last_message = messages[-1]\n", + "\n", + " docs = last_message.content\n", + "\n", + " # Prompt\n", + " prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + " # LLM\n", + " llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0, streaming=True)\n", + "\n", + " # Post-processing\n", + " def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + " # Chain\n", + " rag_chain = prompt | llm | StrOutputParser()\n", + "\n", + " # Run\n", + " response = rag_chain.invoke({\"context\": docs, \"question\": question})\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "print(\"*\" * 20 + \"Prompt[rlm/rag-prompt]\" + \"*\" * 20)\n", + "prompt = hub.pull(\"rlm/rag-prompt\").pretty_print() # Show what the prompt looks like" + ] }, { "cell_type": "markdown", @@ -150,7 +383,48 @@ "id": "8718a37f-83c2-4f16-9850-e61e0f49c3d4", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, StateGraph, START\nfrom langgraph.prebuilt import ToolNode\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the nodes we will cycle between\nworkflow.add_node(\"agent\", agent) # agent\nretrieve = ToolNode([retriever_tool])\nworkflow.add_node(\"retrieve\", retrieve) # retrieval\nworkflow.add_node(\"rewrite\", rewrite) # Re-writing the question\nworkflow.add_node(\n \"generate\", generate\n) # Generating a response after we know the documents are relevant\n# Call agent node to decide to retrieve or not\nworkflow.add_edge(START, \"agent\")\n\n# Decide whether to retrieve\nworkflow.add_conditional_edges(\n \"agent\",\n # Assess agent decision\n tools_condition,\n {\n # Translate the condition outputs to nodes in our graph\n \"tools\": \"retrieve\",\n END: END,\n },\n)\n\n# Edges taken after the `action` node is called.\nworkflow.add_conditional_edges(\n \"retrieve\",\n # Assess agent decision\n grade_documents,\n)\nworkflow.add_edge(\"generate\", END)\nworkflow.add_edge(\"rewrite\", \"agent\")\n\n# Compile\ngraph = workflow.compile()"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "from langgraph.prebuilt import ToolNode\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the nodes we will cycle between\n", + "workflow.add_node(\"agent\", agent) # agent\n", + "retrieve = ToolNode([retriever_tool])\n", + "workflow.add_node(\"retrieve\", retrieve) # retrieval\n", + "workflow.add_node(\"rewrite\", rewrite) # Re-writing the question\n", + "workflow.add_node(\n", + " \"generate\", generate\n", + ") # Generating a response after we know the documents are relevant\n", + "# Call agent node to decide to retrieve or not\n", + "workflow.add_edge(START, \"agent\")\n", + "\n", + "# Decide whether to retrieve\n", + "workflow.add_conditional_edges(\n", + " \"agent\",\n", + " # Assess agent decision\n", + " tools_condition,\n", + " {\n", + " # Translate the condition outputs to nodes in our graph\n", + " \"tools\": \"retrieve\",\n", + " END: END,\n", + " },\n", + ")\n", + "\n", + "# Edges taken after the `action` node is called.\n", + "workflow.add_conditional_edges(\n", + " \"retrieve\",\n", + " # Assess agent decision\n", + " grade_documents,\n", + ")\n", + "workflow.add_edge(\"generate\", END)\n", + "workflow.add_edge(\"rewrite\", \"agent\")\n", + "\n", + "# Compile\n", + "graph = workflow.compile()" + ] }, { "cell_type": "code", @@ -169,7 +443,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(graph.get_graph(xray=True).draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "code", @@ -203,7 +485,21 @@ ] } ], - "source": ["import pprint\n\ninputs = {\n \"messages\": [\n (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n ]\n}\nfor output in graph.stream(inputs):\n for key, value in output.items():\n pprint.pprint(f\"Output from node '{key}':\")\n pprint.pprint(\"---\")\n pprint.pprint(value, indent=2, width=80, depth=None)\n pprint.pprint(\"\\n---\\n\")"] + "source": [ + "import pprint\n", + "\n", + "inputs = {\n", + " \"messages\": [\n", + " (\"user\", \"What does Lilian Weng say about the types of agent memory?\"),\n", + " ]\n", + "}\n", + "for output in graph.stream(inputs):\n", + " for key, value in output.items():\n", + " pprint.pprint(f\"Output from node '{key}':\")\n", + " pprint.pprint(\"---\")\n", + " pprint.pprint(value, indent=2, width=80, depth=None)\n", + " pprint.pprint(\"\\n---\\n\")" + ] }, { "cell_type": "code", @@ -211,7 +507,7 @@ "id": "189333cc-5d34-4869-9f9b-741210e1096f", "metadata": {}, "outputs": [], - "source": [""] + "source": [] } ], "metadata": { From 22f5367af73fa5aa79ba0c378564d1d23bb08d9e Mon Sep 17 00:00:00 2001 From: gbaian10 <34255899+gbaian10@users.noreply.github.com> Date: Fri, 23 Aug 2024 00:32:21 +0800 Subject: [PATCH 072/287] langgraph: fix add_node input schema error (#1332) --- libs/langgraph/langgraph/graph/state.py | 12 ++++--- libs/langgraph/tests/test_state.py | 42 ++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index d846bc8d3..3ff07b15c 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -1,3 +1,4 @@ +import inspect import logging import typing import warnings @@ -330,10 +331,13 @@ class StateGraph(Graph): hints := get_type_hints(action.__call__) or get_type_hints(action) ): if input is None: - input_hint = hints[list(hints.keys())[0]] - if isinstance(input_hint, type) and get_type_hints(input_hint): - input = input_hint - except TypeError: + first_parameter_name = next( + iter(inspect.signature(action).parameters.keys()) + ) + if input_hint := hints.get(first_parameter_name): + if isinstance(input_hint, type) and get_type_hints(input_hint): + input = input_hint + except (TypeError, StopIteration): pass if input is not None: self._add_schema(input) diff --git a/libs/langgraph/tests/test_state.py b/libs/langgraph/tests/test_state.py index ecf35eb2a..4088e38c9 100644 --- a/libs/langgraph/tests/test_state.py +++ b/libs/langgraph/tests/test_state.py @@ -2,10 +2,11 @@ from typing import Annotated as Annotated2 from typing import Any import pytest +from langchain_core.runnables import RunnableConfig from pydantic.v1 import BaseModel from typing_extensions import Annotated, TypedDict -from langgraph.graph.state import _warn_invalid_state_schema +from langgraph.graph.state import StateGraph, _warn_invalid_state_schema class State(BaseModel): @@ -46,3 +47,42 @@ def test_doesnt_warn_valid_schema(schema: Any): # Assert the function does not raise a warning with pytest.warns(None): _warn_invalid_state_schema(schema) + + +def test_state_schema_with_type_hint(): + class InputState(TypedDict): + question: str + + class OutputState(TypedDict): + input_state: InputState + + def complete_hint(state: InputState) -> OutputState: + return {"input_state": state} + + def miss_first_hint(state, config: RunnableConfig) -> OutputState: + return {"input_state": state} + + def only_return_hint(state, config) -> OutputState: + return {"input_state": state} + + def miss_all_hint(state, config): + return {"input_state": state} + + graph = StateGraph(input=InputState, output=OutputState) + actions = [complete_hint, miss_first_hint, only_return_hint, miss_all_hint] + + for action in actions: + graph.add_node(action) + + graph.set_entry_point(actions[0].__name__) + for i in range(len(actions) - 1): + graph.add_edge(actions[i].__name__, actions[i + 1].__name__) + graph.set_finish_point(actions[-1].__name__) + + graph = graph.compile() + + input_state = InputState(question="Hello World!") + output_state = OutputState(input_state=input_state) + for i, c in enumerate(graph.stream(input_state, stream_mode="updates")): + node_name = actions[i].__name__ + assert c[node_name] == output_state From 078f9f7275be0951447beb83a6213f6c8cbbaf6c Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Thu, 22 Aug 2024 14:56:34 -0400 Subject: [PATCH 073/287] docs: fix manage conversation history how-to (#1438) --- examples/memory/manage-conversation-history.ipynb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/memory/manage-conversation-history.ipynb b/examples/memory/manage-conversation-history.ipynb index 066d38ad6..559112a4a 100644 --- a/examples/memory/manage-conversation-history.ipynb +++ b/examples/memory/manage-conversation-history.ipynb @@ -268,8 +268,8 @@ "\n", "\n", "def filter_messages(messages: list):\n", - " # This is very simple helper function which only ever uses the last two messages\n", - " return messages[-2:]\n", + " # This is very simple helper function which only ever uses the last message\n", + " return messages[-1:]\n", "\n", "\n", "# Define the function that calls the model\n", @@ -372,9 +372,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "langgraph-example-dev", "language": "python", - "name": "python3" + "name": "langgraph-example-dev" }, "language_info": { "codemirror_mode": { @@ -386,7 +386,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, From 38daba5259de8d80700a8d669b1c73a0ef783547 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 11:59:29 -0700 Subject: [PATCH 074/287] Better error messages for invalid update in all channel types (#1437) * langgraph: support multiple edges for Topic channel annotations * remove support_multiple_edges * Better error messages for invalid update in all channel types --------- Co-authored-by: vbarda --- libs/langgraph/langgraph/channels/base.py | 26 +++++++++++++--- libs/langgraph/langgraph/channels/context.py | 4 ++- .../channels/dynamic_barrier_value.py | 2 +- .../langgraph/channels/ephemeral_value.py | 2 +- .../langgraph/channels/last_value.py | 4 ++- .../langgraph/channels/named_barrier_value.py | 4 ++- .../langgraph/channels/untracked_value.py | 2 +- libs/langgraph/langgraph/graph/graph.py | 6 ++-- libs/langgraph/langgraph/graph/state.py | 11 +++---- libs/langgraph/langgraph/pregel/algo.py | 7 +---- libs/langgraph/langgraph/pregel/manager.py | 6 ++-- libs/langgraph/tests/test_pregel.py | 30 +++++++++++++++++++ 12 files changed, 79 insertions(+), 25 deletions(-) diff --git a/libs/langgraph/langgraph/channels/base.py b/libs/langgraph/langgraph/channels/base.py index fe47f0d8f..885698743 100644 --- a/libs/langgraph/langgraph/channels/base.py +++ b/libs/langgraph/langgraph/channels/base.py @@ -2,9 +2,9 @@ from abc import ABC, abstractmethod from contextlib import asynccontextmanager, contextmanager from typing import ( Any, - AsyncGenerator, - Generator, + AsyncIterator, Generic, + Iterator, Optional, Sequence, TypeVar, @@ -21,6 +21,8 @@ C = TypeVar("C") class BaseChannel(Generic[Value, Update, C], ABC): + key: str = "" + @property @abstractmethod def ValueType(self) -> Any: @@ -43,19 +45,35 @@ class BaseChannel(Generic[Value, Update, C], ABC): @abstractmethod def from_checkpoint( self, checkpoint: Optional[C], config: RunnableConfig - ) -> Generator[Self, None, None]: + ) -> Iterator[Self]: """Return a new identical channel, optionally initialized from a checkpoint. If the checkpoint contains complex data structures, they should be copied.""" + @contextmanager + def from_checkpoint_named( + self, checkpoint: Optional[C], config: RunnableConfig + ) -> Iterator[Self]: + with self.from_checkpoint(checkpoint, config) as value: + value.key = self.key + yield value + @asynccontextmanager async def afrom_checkpoint( self, checkpoint: Optional[C], config: RunnableConfig - ) -> AsyncGenerator[Self, None]: + ) -> AsyncIterator[Self]: """Return a new identical channel, optionally initialized from a checkpoint. If the checkpoint contains complex data structures, they should be copied.""" with self.from_checkpoint(checkpoint, config) as value: yield value + @asynccontextmanager + async def afrom_checkpoint_named( + self, checkpoint: Optional[C], config: RunnableConfig + ) -> AsyncIterator[Self]: + async with self.afrom_checkpoint(checkpoint, config) as value: + value.key = self.key + yield value + # state methods @abstractmethod diff --git a/libs/langgraph/langgraph/channels/context.py b/libs/langgraph/langgraph/channels/context.py index 914de9348..b48260b40 100644 --- a/libs/langgraph/langgraph/channels/context.py +++ b/libs/langgraph/langgraph/channels/context.py @@ -112,7 +112,9 @@ class Context(Generic[Value], BaseChannel[Value, None, None]): def update(self, values: Sequence[None]) -> bool: if values: - raise InvalidUpdateError("Context channel does not accept writes.") + raise InvalidUpdateError( + f"At key '{self.key}': Context channel does not accept writes." + ) return False def get(self) -> Value: diff --git a/libs/langgraph/langgraph/channels/dynamic_barrier_value.py b/libs/langgraph/langgraph/channels/dynamic_barrier_value.py index bb0d447fa..64406b8f8 100644 --- a/libs/langgraph/langgraph/channels/dynamic_barrier_value.py +++ b/libs/langgraph/langgraph/channels/dynamic_barrier_value.py @@ -69,7 +69,7 @@ class DynamicBarrierValue( if wait_for_names := [v for v in values if isinstance(v, WaitForNames)]: if len(wait_for_names) > 1: raise InvalidUpdateError( - "Received multiple WaitForNames updates in the same step." + f"At key '{self.key}': Received multiple WaitForNames updates in the same step." ) self.names = wait_for_names[0].names return True diff --git a/libs/langgraph/langgraph/channels/ephemeral_value.py b/libs/langgraph/langgraph/channels/ephemeral_value.py index 15e11550d..4e7f2ed63 100644 --- a/libs/langgraph/langgraph/channels/ephemeral_value.py +++ b/libs/langgraph/langgraph/channels/ephemeral_value.py @@ -58,7 +58,7 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]): return False if len(values) != 1 and self.guard: raise InvalidUpdateError( - "EphemeralValue can only receive one value per step." + f"At key '{self.key}': EphemeralValue(guard=True) can receive only one value per step. Use guard=False if you want to store any one of multiple values." ) self.value = values[-1] diff --git a/libs/langgraph/langgraph/channels/last_value.py b/libs/langgraph/langgraph/channels/last_value.py index a207ebce3..e74580d6a 100644 --- a/libs/langgraph/langgraph/channels/last_value.py +++ b/libs/langgraph/langgraph/channels/last_value.py @@ -52,7 +52,9 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]): if len(values) == 0: return False if len(values) != 1: - raise InvalidUpdateError("LastValue can only receive one value per step.") + raise InvalidUpdateError( + f"At key '{self.key}': Can receive only one value per step. Use an Annotated key to handle multiple values." + ) self.value = values[-1] return True diff --git a/libs/langgraph/langgraph/channels/named_barrier_value.py b/libs/langgraph/langgraph/channels/named_barrier_value.py index bdfd4660b..023f54e6c 100644 --- a/libs/langgraph/langgraph/channels/named_barrier_value.py +++ b/libs/langgraph/langgraph/channels/named_barrier_value.py @@ -53,7 +53,9 @@ class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]): self.seen.add(value) updated = True else: - raise InvalidUpdateError(f"Value {value} not in {self.names}") + raise InvalidUpdateError( + f"At key '{self.key}': Value {value} not in {self.names}" + ) return updated def get(self) -> Value: diff --git a/libs/langgraph/langgraph/channels/untracked_value.py b/libs/langgraph/langgraph/channels/untracked_value.py index 989bba35e..a112b0e81 100644 --- a/libs/langgraph/langgraph/channels/untracked_value.py +++ b/libs/langgraph/langgraph/channels/untracked_value.py @@ -49,7 +49,7 @@ class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]): return False if len(values) != 1 and self.guard: raise InvalidUpdateError( - "UntrackedValue can only receive one value per step." + f"At key '{self.key}': UntrackedValue(guard=True) can receive only one value per step. Use guard=False if you want to store any one of multiple values." ) self.value = values[-1] diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index dc927446d..d5eeb88e2 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -192,12 +192,14 @@ class Graph: raise ValueError("END cannot be a start node") if end_key == START: raise ValueError("START cannot be an end node") - if not self.support_multiple_edges and start_key in set( + + # run this validation only for non-StateGraph graphs + if not hasattr(self, "channels") and start_key in set( start for start, _ in self.edges ): raise ValueError( f"Already found path for node '{start_key}'.\n" - "For multiple edges, use StateGraph with an annotated state key." + "For multiple edges, use StateGraph with an Annotated state key." ) self.edges.add((start_key, end_key)) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 3ff07b15c..7228c5fa2 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -193,10 +193,6 @@ class StateGraph(Graph): ) else: self.managed[key] = managed - if any( - isinstance(c, BinaryOperatorAggregate) for c in self.channels.values() - ): - self.support_multiple_edges = True @overload def add_node( @@ -723,10 +719,15 @@ def _get_channel( else: raise ValueError(f"This {annotation} not allowed in this position") elif channel := _is_field_channel(annotation): + channel.key = name return channel elif channel := _is_field_binop(annotation): + channel.key = name return channel - return LastValue(annotation) + + fallback = LastValue(annotation) + fallback.key = name + return fallback def _is_field_channel(typ: Type[Any]) -> Optional[BaseChannel]: diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 28a8dcd5d..1d3f57040 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -198,12 +198,7 @@ def apply_writes( updated_channels: set[str] = set() for chan, vals in pending_writes_by_channel.items(): if chan in channels: - try: - updated = channels[chan].update(vals) - except InvalidUpdateError as e: - raise InvalidUpdateError( - f"Invalid update for channel {chan} with values {vals}" - ) from e + updated = channels[chan].update(vals) if updated and get_next_version is not None: checkpoint["channel_versions"][chan] = get_next_version( max_version, channels[chan] diff --git a/libs/langgraph/langgraph/pregel/manager.py b/libs/langgraph/langgraph/pregel/manager.py index 437019113..849395c50 100644 --- a/libs/langgraph/langgraph/pregel/manager.py +++ b/libs/langgraph/langgraph/pregel/manager.py @@ -41,7 +41,7 @@ def ChannelsManager( yield ( { k: stack.enter_context( - v.from_checkpoint(checkpoint["channel_values"].get(k), config) + v.from_checkpoint_named(checkpoint["channel_values"].get(k), config) ) for k, v in channel_specs.items() }, @@ -95,7 +95,9 @@ async def AsyncChannelsManager( # channels: enter each channel with checkpoint { k: await stack.enter_async_context( - v.afrom_checkpoint(checkpoint["channel_values"].get(k), config) + v.afrom_checkpoint_named( + checkpoint["channel_values"].get(k), config + ) ) for k, v in channel_specs.items() }, diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index c1fc68c2a..3e40f31fc 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -198,6 +198,21 @@ def test_graph_validation() -> None: with pytest.raises(ValueError, match="Invalid reducer"): StateGraph(BadReducerState) + def node_b(state: State) -> State: + return {"hello": "world"} + + builder = StateGraph(State) + builder.add_node("a", node_b) + builder.add_node("b", node_b) + builder.add_node("c", node_b) + builder.set_entry_point("a") + builder.add_edge("a", "b") + builder.add_edge("a", "c") + graph = builder.compile() + + with pytest.raises(InvalidUpdateError, match="At key 'hello'"): + graph.invoke({"hello": "there"}) + def test_checkpoint_errors() -> None: class FaultyGetCheckpointer(MemorySaver): @@ -1197,6 +1212,21 @@ def test_invoke_two_processes_two_in_two_out_invalid(mocker: MockerFixture) -> N # LastValue channels can only be updated once per iteration app.invoke(2) + class State(TypedDict): + hello: str + + def my_node(input: State) -> State: + return {"hello": "world"} + + builder = StateGraph(State) + builder.add_node("one", my_node) + builder.add_node("two", my_node) + builder.set_conditional_entry_point(lambda _: ["one", "two"]) + + graph = builder.compile() + with pytest.raises(InvalidUpdateError, match="At key 'hello'"): + graph.invoke({"hello": "there"}, debug=True) + def test_invoke_two_processes_two_in_two_out_valid(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) From a261e1a49761d976d783bcc0d96b3f9b6e105e52 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 12:04:42 -0700 Subject: [PATCH 075/287] Fix semantics of put_writes/list (#1436) * Fix semantics of put_writes/list - put_writes(error) should not prevent saving future successful if task is retried successfully - put_writes(writes) should be a no-op if non-error writes already exist for that task (this prevents tasks executed more than once from modifying writes previously saved / acted on) - checkpoints should not include channel default values (ie those without a version) - list() should fetch and return writes for each checkpoint * Lint * Rm print * Fix import * Lint --- .../langgraph/checkpoint/postgres/__init__.py | 11 +- .../langgraph/checkpoint/postgres/aio.py | 11 +- .../langgraph/checkpoint/postgres/base.py | 13 +- .../langgraph/checkpoint/sqlite/__init__.py | 27 +- .../langgraph/checkpoint/sqlite/aio.py | 29 +- .../langgraph/checkpoint/base/__init__.py | 14 + .../langgraph/checkpoint/memory/__init__.py | 24 +- .../langgraph/checkpoint/serde/types.py | 2 + libs/langgraph/langgraph/errors.py | 4 +- libs/langgraph/langgraph/pregel/__init__.py | 66 +--- libs/langgraph/langgraph/pregel/loop.py | 17 +- libs/langgraph/tests/any_str.py | 35 ++ libs/langgraph/tests/test_pregel.py | 237 +++++++++---- libs/langgraph/tests/test_pregel_async.py | 333 +++++++++++------- 14 files changed, 480 insertions(+), 343 deletions(-) diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py index 02d5880a0..433bc231d 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py @@ -150,6 +150,7 @@ class PostgresSaver(BasePostgresSaver): } if value["parent_checkpoint_id"] else None, + self._load_writes(value["pending_writes"]), ) def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: @@ -317,16 +318,6 @@ class PostgresSaver(BasePostgresSaver): task_id (str): Identifier for the task creating the writes. """ with self._cursor(pipeline=True) as cur: - cur.execute( - self.DELETE_WRITES_SQL, - ( - config["configurable"]["thread_id"], - config["configurable"]["checkpoint_ns"], - config["configurable"]["checkpoint_id"], - task_id, - len(writes), - ), - ) cur.executemany( self.UPSERT_CHECKPOINT_WRITES_SQL, self._dump_writes( diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py index 7ddb81237..79ae1ddf7 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py @@ -135,6 +135,7 @@ class AsyncPostgresSaver(BasePostgresSaver): } if value["parent_checkpoint_id"] else None, + await asyncio.to_thread(self._load_writes, value["pending_writes"]), ) async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: @@ -273,16 +274,6 @@ class AsyncPostgresSaver(BasePostgresSaver): task_id (str): Identifier for the task creating the writes. """ async with self._cursor(pipeline=True) as cur: - await cur.execute( - self.DELETE_WRITES_SQL, - ( - config["configurable"]["thread_id"], - config["configurable"]["checkpoint_ns"], - config["configurable"]["checkpoint_id"], - task_id, - len(writes), - ), - ) await cur.executemany( self.UPSERT_CHECKPOINT_WRITES_SQL, await asyncio.to_thread( diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py index 506a13a27..f53bee256 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py @@ -6,6 +6,7 @@ from langchain_core.runnables import RunnableConfig from psycopg.types.json import Jsonb from langgraph.checkpoint.base import ( + WRITES_IDX_MAP, BaseCheckpointSaver, Checkpoint, EmptyChannelError, @@ -105,15 +106,6 @@ UPSERT_CHECKPOINT_WRITES_SQL = """ ON CONFLICT (thread_id, checkpoint_ns, checkpoint_id, task_id, idx) DO NOTHING """ -DELETE_WRITES_SQL = """ - DELETE FROM checkpoint_writes - WHERE thread_id = %s - AND checkpoint_ns = %s - AND checkpoint_id = %s - AND task_id = %s - AND idx >= %s -""" - class BasePostgresSaver(BaseCheckpointSaver): SELECT_SQL = SELECT_SQL @@ -121,7 +113,6 @@ class BasePostgresSaver(BaseCheckpointSaver): UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL - DELETE_WRITES_SQL = DELETE_WRITES_SQL jsonplus_serde = JsonPlusSerializer() @@ -210,7 +201,7 @@ class BasePostgresSaver(BaseCheckpointSaver): checkpoint_ns, checkpoint_id, task_id, - idx, + WRITES_IDX_MAP.get(channel, idx), channel, *self.serde.dumps_typed(value), ) diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py index 9bf4139ad..4767bfb70 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py @@ -1,12 +1,13 @@ import sqlite3 import threading -from contextlib import contextmanager +from contextlib import closing, contextmanager from hashlib import md5 from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple from langchain_core.runnables import RunnableConfig from langgraph.checkpoint.base import ( + WRITES_IDX_MAP, BaseCheckpointSaver, ChannelVersions, Checkpoint, @@ -318,7 +319,7 @@ class SqliteSaver(BaseCheckpointSaver): ORDER BY checkpoint_id DESC""" if limit: query += f" LIMIT {limit}" - with self.cursor(transaction=False) as cur: + with self.cursor(transaction=False) as cur, closing(self.conn.cursor()) as wcur: cur.execute(query, param_values) for ( thread_id, @@ -329,6 +330,10 @@ class SqliteSaver(BaseCheckpointSaver): checkpoint, metadata, ) in cur: + wcur.execute( + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?", + (thread_id, checkpoint_ns, checkpoint_id), + ) yield CheckpointTuple( { "configurable": { @@ -350,6 +355,10 @@ class SqliteSaver(BaseCheckpointSaver): if parent_checkpoint_id else None ), + [ + (task_id, channel, self.serde.loads_typed((type, value))) + for task_id, channel, type, value in wcur + ], ) def put( @@ -424,25 +433,15 @@ class SqliteSaver(BaseCheckpointSaver): task_id (str): Identifier for the task creating the writes. """ with self.lock, self.cursor() as cur: - cur.execute( - "DELETE FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? AND task_id = ? AND idx >= ?", - ( - str(config["configurable"]["thread_id"]), - str(config["configurable"]["checkpoint_ns"]), - str(config["configurable"]["checkpoint_id"]), - task_id, - len(writes), - ), - ) cur.executemany( - "INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [ ( str(config["configurable"]["thread_id"]), str(config["configurable"]["checkpoint_ns"]), str(config["configurable"]["checkpoint_id"]), task_id, - idx, + WRITES_IDX_MAP.get(channel, idx), channel, *self.serde.dumps_typed(value), ) diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py index 56d14f613..85869d3f0 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py @@ -16,6 +16,7 @@ import aiosqlite from langchain_core.runnables import RunnableConfig from langgraph.checkpoint.base import ( + WRITES_IDX_MAP, BaseCheckpointSaver, ChannelVersions, Checkpoint, @@ -329,14 +330,14 @@ class AsyncSqliteSaver(BaseCheckpointSaver): AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples. """ await self.setup() - where, param_values = search_where(config, filter, before) + where, params = search_where(config, filter, before) query = f"""SELECT thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata FROM checkpoints {where} ORDER BY checkpoint_id DESC""" if limit: query += f" LIMIT {limit}" - async with self.conn.execute(query, param_values) as cursor: + async with self.conn.execute(query, params) as cur, self.conn.cursor() as wcur: async for ( thread_id, checkpoint_ns, @@ -345,7 +346,11 @@ class AsyncSqliteSaver(BaseCheckpointSaver): type, checkpoint, metadata, - ) in cursor: + ) in cur: + await wcur.execute( + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?", + (thread_id, checkpoint_ns, checkpoint_id), + ) yield CheckpointTuple( { "configurable": { @@ -367,6 +372,10 @@ class AsyncSqliteSaver(BaseCheckpointSaver): if parent_checkpoint_id else None ), + [ + (task_id, channel, self.serde.loads_typed((type, value))) + async for task_id, channel, type, value in wcur + ], ) async def aput( @@ -433,25 +442,15 @@ class AsyncSqliteSaver(BaseCheckpointSaver): """ await self.setup() async with self.conn.cursor() as cur: - await cur.execute( - "DELETE FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? AND task_id = ? AND idx >= ?", - ( - str(config["configurable"]["thread_id"]), - str(config["configurable"]["checkpoint_ns"]), - str(config["configurable"]["checkpoint_id"]), - task_id, - len(writes), - ), - ) await cur.executemany( - "INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [ ( str(config["configurable"]["thread_id"]), str(config["configurable"]["checkpoint_ns"]), str(config["configurable"]["checkpoint_id"]), task_id, - idx, + WRITES_IDX_MAP.get(channel, idx), channel, *self.serde.dumps_typed(value), ) diff --git a/libs/checkpoint/langgraph/checkpoint/base/__init__.py b/libs/checkpoint/langgraph/checkpoint/base/__init__.py index 86c8b0eec..21d021ad5 100644 --- a/libs/checkpoint/langgraph/checkpoint/base/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/base/__init__.py @@ -22,6 +22,7 @@ from langgraph.checkpoint.base.id import uuid6 from langgraph.checkpoint.serde.base import SerializerProtocol, maybe_add_typed_methods from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer from langgraph.checkpoint.serde.types import ( + ERROR, ChannelProtocol, SendProtocol, ) @@ -98,6 +99,7 @@ class Checkpoint(TypedDict): Cleared by the next checkpoint.""" current_tasks: Dict[str, TaskInfo] """Map from task ID to task info.""" + # TODO remove this def empty_checkpoint() -> Checkpoint: @@ -140,6 +142,8 @@ def create_checkpoint( else: values: dict[str, Any] = {} for k, v in channels.items(): + if k not in checkpoint["channel_versions"]: + continue try: values[k] = v.checkpoint() except EmptyChannelError: @@ -437,3 +441,13 @@ def get_checkpoint_id(config: RunnableConfig) -> Optional[str]: return config["configurable"].get( "checkpoint_id", config["configurable"].get("thread_ts") ) + + +""" +Mapping from error type to error index. +Regular writes just map to their index in the list of writes being saved. +Special writes (e.g. errors) map to negative indices, to avoid those writes from +saving regular writes. +Each Checkpointer implementation should use this mapping in put_writes. +""" +WRITES_IDX_MAP = {ERROR: -1} diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 3e8d328e6..52102ffef 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -8,6 +8,7 @@ from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Tuple from langchain_core.runnables import RunnableConfig from langgraph.checkpoint.base import ( + WRITES_IDX_MAP, BaseCheckpointSaver, ChannelVersions, Checkpoint, @@ -52,6 +53,9 @@ class MemorySaver( # thread ID -> checkpoint NS -> checkpoint ID -> checkpoint mapping storage: defaultdict[str, dict[str, dict[str, tuple[bytes, bytes, Optional[str]]]]] + writes: defaultdict[ + tuple[str, str, str], dict[tuple[str, int], tuple[str, str, bytes]] + ] def __init__( self, @@ -60,7 +64,7 @@ class MemorySaver( ) -> None: super().__init__(serde=serde) self.storage = defaultdict(lambda: defaultdict(dict)) - self.writes = defaultdict(list) + self.writes = defaultdict(dict) def __enter__(self) -> "MemorySaver": return self @@ -103,7 +107,7 @@ class MemorySaver( if checkpoint_id := get_checkpoint_id(config): if saved := self.storage[thread_id][checkpoint_ns].get(checkpoint_id): checkpoint, metadata, parent_checkpoint_id = saved - writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)] + writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values() return CheckpointTuple( config=config, checkpoint=self.serde.loads_typed(checkpoint), @@ -125,7 +129,7 @@ class MemorySaver( if checkpoints := self.storage[thread_id][checkpoint_ns]: checkpoint_id = max(checkpoints.keys()) checkpoint, metadata, parent_checkpoint_id = checkpoints[checkpoint_id] - writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)] + writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values() return CheckpointTuple( config={ "configurable": { @@ -204,6 +208,8 @@ class MemorySaver( elif limit is not None: limit -= 1 + writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values() + yield CheckpointTuple( config={ "configurable": { @@ -223,6 +229,9 @@ class MemorySaver( } if parent_checkpoint_id else None, + pending_writes=[ + (id, c, self.serde.loads_typed(v)) for id, c, v in writes + ], ) def put( @@ -287,11 +296,10 @@ class MemorySaver( thread_id = config["configurable"]["thread_id"] checkpoint_ns = config["configurable"]["checkpoint_ns"] checkpoint_id = config["configurable"]["checkpoint_id"] - key = (thread_id, checkpoint_ns, checkpoint_id) - self.writes[key] = [w for w in self.writes[key] if w[0] != task_id] - self.writes[key].extend( - [(task_id, c, self.serde.dumps_typed(v)) for c, v in writes] - ) + outer_key = (thread_id, checkpoint_ns, checkpoint_id) + for idx, (c, v) in enumerate(writes): + inner_key = (task_id, WRITES_IDX_MAP.get(c, idx)) + self.writes[outer_key][inner_key] = (task_id, c, self.serde.dumps_typed(v)) async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: """Asynchronous version of get_tuple. diff --git a/libs/checkpoint/langgraph/checkpoint/serde/types.py b/libs/checkpoint/langgraph/checkpoint/serde/types.py index de61ef78b..d166a6a0d 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/types.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/types.py @@ -12,6 +12,8 @@ from typing import ( from langchain_core.runnables import RunnableConfig from typing_extensions import Self +ERROR = "__error__" + Value = TypeVar("Value") Update = TypeVar("Update") C = TypeVar("C") diff --git a/libs/langgraph/langgraph/errors.py b/libs/langgraph/langgraph/errors.py index 00dc36e58..27a7689fa 100644 --- a/libs/langgraph/langgraph/errors.py +++ b/libs/langgraph/langgraph/errors.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Sequence from langgraph.checkpoint.base import EmptyChannelError from langgraph.constants import Interrupt @@ -32,7 +32,7 @@ class InvalidUpdateError(Exception): class GraphInterrupt(Exception): """Raised when a subgraph is interrupted.""" - def __init__(self, interrupts: list[Interrupt]) -> None: + def __init__(self, interrupts: Sequence[Interrupt] = ()) -> None: super().__init__(interrupts) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 270a3c7ba..c06f2bce3 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -65,16 +65,10 @@ from langgraph.constants import ( CONFIG_KEY_SEND, ERROR, INTERRUPT, - Interrupt, ) from langgraph.errors import GraphInterrupt, GraphRecursionError, InvalidUpdateError from langgraph.managed.base import ManagedValueSpec -from langgraph.pregel.algo import ( - apply_writes, - local_read, - prepare_next_tasks, - should_interrupt, -) +from langgraph.pregel.algo import apply_writes, local_read, prepare_next_tasks from langgraph.pregel.debug import ( print_step_checkpoint, print_step_tasks, @@ -570,7 +564,7 @@ class Pregel( # update channels with ChannelsManager(self.channels, checkpoint, config) as ( channels, - managed, + _, ): # create task to run all writers of the chosen node writers = self.nodes[as_node].get_writers() @@ -606,31 +600,6 @@ class Pregel( checkpoint, channels, [task], self.checkpointer.get_next_version ), "Can't write to SharedValues from update_state" checkpoint = create_checkpoint(checkpoint, channels, step + 1) - # check interrupt before - if tasks := should_interrupt( - checkpoint, - self.interrupt_before_nodes, - prepare_next_tasks( - checkpoint, - self.nodes, - channels, - managed, - config, - step + 2, - for_execution=False, - ), - ): - for t in tasks: - self.checkpointer.put_writes( - { - "configurable": { - **checkpoint_config["configurable"], - "checkpoint_id": checkpoint["id"], - } - }, - [(INTERRUPT, Interrupt("before"))], - t.id, - ) return self.checkpointer.put( checkpoint_config, checkpoint, @@ -713,7 +682,7 @@ class Pregel( # update channels, acting as the chosen node async with AsyncChannelsManager(self.channels, checkpoint, config) as ( channels, - managed, + _, ): # create task to run all writers of the chosen node writers = self.nodes[as_node].get_writers() @@ -749,35 +718,6 @@ class Pregel( checkpoint, channels, [task], self.checkpointer.get_next_version ), "Can't write to SharedValues from update_state" checkpoint = create_checkpoint(checkpoint, channels, step + 1) - # check interrupt before - if tasks := should_interrupt( - checkpoint, - self.interrupt_before_nodes, - prepare_next_tasks( - checkpoint, - self.nodes, - channels, - managed, - config, - step + 2, - for_execution=False, - ), - ): - await asyncio.gather( - *( - self.checkpointer.aput_writes( - { - "configurable": { - **checkpoint_config["configurable"], - "checkpoint_id": checkpoint["id"], - } - }, - [(INTERRUPT, Interrupt("before"))], - t.id, - ) - for t in tasks - ) - ) return await self.checkpointer.aput( checkpoint_config, checkpoint, diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 061b932c0..a501232a9 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -41,7 +41,6 @@ from langgraph.constants import ( ERROR, INPUT, INTERRUPT, - Interrupt, ) from langgraph.errors import EmptyInputError, GraphInterrupt from langgraph.managed.base import ( @@ -162,6 +161,8 @@ class PregelLoop: def put_writes(self, task_id: str, writes: Sequence[tuple[str, Any]]) -> None: """Put writes for a task, to be read by the next tick.""" + if not writes: + return self.checkpoint_pending_writes.extend((task_id, k, v) for k, v in writes) if self.checkpointer_put_writes is not None: self.submit( @@ -238,13 +239,10 @@ class PregelLoop: } ) # after execution, check if we should interrupt - if tasks := should_interrupt(self.checkpoint, interrupt_after, self.tasks): + if should_interrupt(self.checkpoint, interrupt_after, self.tasks): self.status = "interrupt_after" - interrupts = [(t.id, Interrupt("after")) for t in tasks] - for tid, interrupt in interrupts: - self.put_writes(tid, [(INTERRUPT, interrupt)]) if self.is_nested: - raise GraphInterrupt([i[1] for i in interrupts]) + raise GraphInterrupt() else: return False else: @@ -308,13 +306,10 @@ class PregelLoop: ) # before execution, check if we should interrupt - if tasks := should_interrupt(self.checkpoint, interrupt_before, self.tasks): + if should_interrupt(self.checkpoint, interrupt_before, self.tasks): self.status = "interrupt_before" - interrupts = [(t.id, Interrupt("before")) for t in tasks] - for tid, interrupt in interrupts: - self.put_writes(tid, [(INTERRUPT, interrupt)]) if self.is_nested: - raise GraphInterrupt([i[1] for i in interrupts]) + raise GraphInterrupt() else: return False diff --git a/libs/langgraph/tests/any_str.py b/libs/langgraph/tests/any_str.py index 9aa030d3b..a98962cdc 100644 --- a/libs/langgraph/tests/any_str.py +++ b/libs/langgraph/tests/any_str.py @@ -1,3 +1,6 @@ +from typing import Any, Sequence + + class AnyStr(str): def __init__(self) -> None: super().__init__() @@ -9,6 +12,17 @@ class AnyStr(str): return hash(str(self)) +class AnyVersion: + def __init__(self) -> None: + super().__init__() + + def __eq__(self, other: object) -> bool: + return isinstance(other, (str, int, float)) + + def __hash__(self) -> int: + return hash(str(self)) + + class ExceptionLike: def __init__(self, exc: Exception) -> None: self.exc = exc @@ -22,3 +36,24 @@ class ExceptionLike: def __hash__(self) -> int: return hash((self.exc.__class__, str(self.exc))) + + def __repr__(self) -> str: + return str(self.exc) + + +class UnsortedSequence: + def __init__(self, *values: Any) -> None: + self.seq = values + + def __eq__(self, value: object) -> bool: + return ( + isinstance(value, Sequence) + and len(self.seq) == len(value) + and all(a in value for a in self.seq) + ) + + def __hash__(self) -> int: + return hash(frozenset(self.seq)) + + def __repr__(self) -> str: + return repr(self.seq) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 3e40f31fc..56e7ed5a3 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -67,7 +67,7 @@ from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore -from tests.any_str import AnyStr, ExceptionLike +from tests.any_str import AnyStr, AnyVersion, ExceptionLike, UnsortedSequence from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -1411,6 +1411,147 @@ def test_pending_writes_resume( # both the pending write and the new write were applied, 1 + 2 + 3 = 6 assert graph.invoke(None, thread1) == {"value": 6} + # check all final checkpoints + checkpoints = [c for c in checkpointer.list(thread1)] + # we should have 3 + assert len(checkpoints) == 3 + # the last one not too interesting for this test + assert checkpoints[0] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "current_tasks": {}, + "pending_sends": [], + "versions_seen": { + "one": { + "start:one": AnyVersion(), + }, + "two": { + "start:two": AnyVersion(), + }, + "__input__": {}, + "__start__": { + "__start__": AnyVersion(), + }, + "__interrupt__": { + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + }, + "channel_versions": { + "one": AnyVersion(), + "two": AnyVersion(), + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + "channel_values": {"one": "one", "two": "two", "value": 6}, + }, + metadata={ + "step": 1, + "source": "loop", + "writes": {"one": {"value": 2}, "two": {"value": 3}}, + }, + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": checkpoints[1].config["configurable"]["checkpoint_id"], + } + }, + pending_writes=[], + ) + # the previous one we assert that pending writes contains both + # - original error + # - successful writes from resuming after preventing error + assert checkpoints[1] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "current_tasks": {}, + "pending_sends": [], + "versions_seen": { + "__input__": {}, + "__start__": { + "__start__": AnyVersion(), + }, + }, + "channel_versions": { + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + "channel_values": { + "value": 1, + "start:one": "__start__", + "start:two": "__start__", + }, + }, + metadata={"step": 0, "source": "loop", "writes": None}, + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": checkpoints[2].config["configurable"]["checkpoint_id"], + } + }, + pending_writes=UnsortedSequence( + (AnyStr(), "one", "one"), + (AnyStr(), "value", 2), + (AnyStr(), "__error__", ExceptionLike(ConnectionError("I'm not good"))), + (AnyStr(), "two", "two"), + (AnyStr(), "value", 3), + ), + ) + assert checkpoints[2] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "current_tasks": {}, + "pending_sends": [], + "versions_seen": {"__input__": {}}, + "channel_versions": { + "__start__": AnyVersion(), + }, + "channel_values": {"__start__": {"value": 1}}, + }, + metadata={"step": -1, "source": "input", "writes": {"value": 1}}, + parent_config=None, + pending_writes=UnsortedSequence( + (AnyStr(), "value", 1), + (AnyStr(), "start:one", "__start__"), + (AnyStr(), "start:two", "__start__"), + ), + ) + def test_cond_edge_after_send() -> None: class Node: @@ -2235,7 +2376,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: ), }, }, - tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -2281,7 +2422,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "input": "what is weather in sf", }, }, - tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -2442,7 +2583,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: ), }, }, - tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -3043,7 +3184,7 @@ def test_conditional_state_graph( ), "intermediate_steps": [], }, - tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -3083,7 +3224,7 @@ def test_conditional_state_graph( ), "intermediate_steps": [], }, - tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -3192,7 +3333,7 @@ def test_conditional_state_graph( values={ "intermediate_steps": [], }, - tasks=(PregelTask(AnyStr(), "agent", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "agent"),), next=("agent",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -3217,7 +3358,7 @@ def test_conditional_state_graph( ), "intermediate_steps": [], }, - tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -3270,7 +3411,7 @@ def test_conditional_state_graph( ) ], }, - tasks=(PregelTask(AnyStr(), "agent", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "agent"),), next=("agent",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -4872,13 +5013,7 @@ def test_message_graph( id="ai1", ), ], - tasks=( - PregelTask( - AnyStr(), - "tools", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -4923,7 +5058,7 @@ def test_message_graph( ], ), ], - tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -5005,7 +5140,7 @@ def test_message_graph( id="ai2", ), ], - tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -5597,13 +5732,7 @@ def test_root_graph( id="ai1", ), ], - tasks=( - PregelTask( - AnyStr(), - "tools", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -5648,7 +5777,7 @@ def test_root_graph( ], ), ], - tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -5730,7 +5859,7 @@ def test_root_graph( id="ai2", ), ], - tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], @@ -6340,13 +6469,7 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: ] assert tool_two.get_state(thread1) == StateSnapshot( values={"my_key": "value ⛰️", "market": "DE"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_slow", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), next=("tool_two_slow",), config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], @@ -6380,13 +6503,7 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: } assert tool_two.get_state(thread2) == StateSnapshot( values={"my_key": "value", "market": "US"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_fast", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), next=("tool_two_fast",), config=tool_two.checkpointer.get_tuple(thread2).config, created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], @@ -6420,13 +6537,7 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: } assert tool_two.get_state(thread3) == StateSnapshot( values={"my_key": "value", "market": "US"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_fast", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), next=("tool_two_fast",), config=tool_two.checkpointer.get_tuple(thread3).config, created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], @@ -6437,13 +6548,7 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: tool_two.update_state(thread3, {"my_key": "key"}) # appends to my_key assert tool_two.get_state(thread3) == StateSnapshot( values={"my_key": "valuekey", "market": "US"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_fast", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), next=("tool_two_fast",), config=tool_two.checkpointer.get_tuple(thread3).config, created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], @@ -6744,13 +6849,7 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: } assert tool_two.get_state(thread1) == StateSnapshot( values={"my_key": "value prepared", "market": "DE"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_slow", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), next=("tool_two_slow",), config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], @@ -6788,13 +6887,7 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: } assert tool_two.get_state(thread2) == StateSnapshot( values={"my_key": "value prepared", "market": "US"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_fast", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), next=("tool_two_fast",), config=tool_two.checkpointer.get_tuple(thread2).config, created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], @@ -6841,7 +6934,7 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "my_key": "value prepared slow", "market": "DE", }, - tasks=(PregelTask(AnyStr(), "finish", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "finish"),), next=("finish",), config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], @@ -6860,7 +6953,7 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "my_key": "value prepared slower", "market": "DE", }, - tasks=(PregelTask(AnyStr(), "finish", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "finish"),), next=("finish",), config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], @@ -7117,7 +7210,7 @@ def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> "query": "analyzed: query: what is weather in sf", "docs": ["doc1", "doc2", "doc3", "doc4", "doc5"], }, - tasks=(PregelTask(AnyStr(), "qa", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "qa"),), next=("qa",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 692d101e0..0308deb54 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -62,7 +62,7 @@ from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore -from tests.any_str import AnyStr, ExceptionLike +from tests.any_str import AnyStr, AnyVersion, ExceptionLike, UnsortedSequence from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -207,7 +207,15 @@ async def test_node_cancellation_on_other_node_exception() -> None: assert inner_task_cancelled -async def test_dynamic_interrupt(snapshot: SnapshotAssertion) -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], +) +async def test_dynamic_interrupt( + checkpointer_name: str, snapshot: SnapshotAssertion, request: pytest.FixtureRequest +) -> None: + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") + class State(TypedDict): my_key: Annotated[str, operator.add] market: str @@ -245,51 +253,49 @@ async def test_dynamic_interrupt(snapshot: SnapshotAssertion) -> None: "market": "US", } - async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: - tool_two = tool_two_graph.compile(checkpointer=saver) + tool_two = tool_two_graph.compile(checkpointer=checkpointer) - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - await tool_two.ainvoke({"my_key": "value", "market": "DE"}) + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1"}} - # stop when about to enter node - assert await tool_two.ainvoke( - {"my_key": "value ⛰️", "market": "DE"}, thread1 - ) == { - "my_key": "value ⛰️", - "market": "DE", - } - assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ - { - "source": "loop", - "step": 0, - "writes": None, - }, - { - "source": "input", - "step": -1, - "writes": {"my_key": "value ⛰️", "market": "DE"}, - }, - ] - tup = await tool_two.checkpointer.aget_tuple(thread1) - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value ⛰️", "market": "DE"}, - next=("tool_two",), - tasks=( - PregelTask( - AnyStr(), - "tool_two", - interrupts=(Interrupt("during", "Just because..."),), - ), + thread1 = {"configurable": {"thread_id": "1"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == { + "my_key": "value ⛰️", + "market": "DE", + } + assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ + { + "source": "loop", + "step": 0, + "writes": None, + }, + { + "source": "input", + "step": -1, + "writes": {"my_key": "value ⛰️", "market": "DE"}, + }, + ] + tup = await tool_two.checkpointer.aget_tuple(thread1) + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value ⛰️", "market": "DE"}, + next=("tool_two",), + tasks=( + PregelTask( + AnyStr(), + "tool_two", + interrupts=(Interrupt("during", "Just because..."),), ), - config=tup.config, - created_at=tup.checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread1, limit=2) - ][-1].config, - ) + ), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={"source": "loop", "step": 0, "writes": None}, + parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ + -1 + ].config, + ) + # TODO use aget_state_history @pytest.mark.parametrize( @@ -1609,6 +1615,10 @@ async def test_pending_writes_resume( error_write = next(w for w in checkpoint.pending_writes if w[1] == ERROR) assert error_write[0] != non_error_writes[0][0] + # TODO arguably this shouldn't even run the failed task again, + # and should require empty update_state (ie new checkpoint_id) + # in order to try again + # resume execution with pytest.raises(ValueError, match="I'm not good"): await graph.ainvoke(None, thread1) @@ -1627,6 +1637,147 @@ async def test_pending_writes_resume( # both the pending write and the new write were applied, 1 + 2 + 3 = 6 assert await graph.ainvoke(None, thread1) == {"value": 6} + # check all final checkpoints + checkpoints = [c async for c in checkpointer.alist(thread1)] + # we should have 3 + assert len(checkpoints) == 3 + # the last one not too interesting for this test + assert checkpoints[0] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "current_tasks": {}, + "pending_sends": [], + "versions_seen": { + "one": { + "start:one": AnyVersion(), + }, + "two": { + "start:two": AnyVersion(), + }, + "__input__": {}, + "__start__": { + "__start__": AnyVersion(), + }, + "__interrupt__": { + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + }, + "channel_versions": { + "one": AnyVersion(), + "two": AnyVersion(), + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + "channel_values": {"one": "one", "two": "two", "value": 6}, + }, + metadata={ + "step": 1, + "source": "loop", + "writes": {"one": {"value": 2}, "two": {"value": 3}}, + }, + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": checkpoints[1].config["configurable"]["checkpoint_id"], + } + }, + pending_writes=[], + ) + # the previous one we assert that pending writes contains both + # - original error + # - successful writes from resuming after preventing error + assert checkpoints[1] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "current_tasks": {}, + "pending_sends": [], + "versions_seen": { + "__input__": {}, + "__start__": { + "__start__": AnyVersion(), + }, + }, + "channel_versions": { + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + "channel_values": { + "value": 1, + "start:one": "__start__", + "start:two": "__start__", + }, + }, + metadata={"step": 0, "source": "loop", "writes": None}, + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": checkpoints[2].config["configurable"]["checkpoint_id"], + } + }, + pending_writes=UnsortedSequence( + (AnyStr(), "one", "one"), + (AnyStr(), "value", 2), + (AnyStr(), "__error__", ExceptionLike(ValueError("I'm not good"))), + (AnyStr(), "two", "two"), + (AnyStr(), "value", 3), + ), + ) + assert checkpoints[2] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "current_tasks": {}, + "pending_sends": [], + "versions_seen": {"__input__": {}}, + "channel_versions": { + "__start__": AnyVersion(), + }, + "channel_values": {"__start__": {"value": 1}}, + }, + metadata={"step": -1, "source": "input", "writes": {"value": 1}}, + parent_config=None, + pending_writes=UnsortedSequence( + (AnyStr(), "value", 1), + (AnyStr(), "start:one", "__start__"), + (AnyStr(), "start:two", "__start__"), + ), + ) + async def test_cond_edge_after_send() -> None: class Node: @@ -2516,13 +2667,7 @@ async def test_conditional_graph() -> None: ), }, }, - tasks=( - PregelTask( - AnyStr(), - "tools", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ @@ -2572,13 +2717,7 @@ async def test_conditional_graph() -> None: "input": "what is weather in sf", }, }, - tasks=( - PregelTask( - AnyStr(), - "tools", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ @@ -2750,13 +2889,7 @@ async def test_conditional_graph() -> None: ), }, }, - tasks=( - PregelTask( - AnyStr(), - "tools", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ @@ -3324,13 +3457,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: ), "intermediate_steps": [], }, - tasks=( - PregelTask( - AnyStr(), - "tools", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ @@ -3374,7 +3501,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: ), "intermediate_steps": [], }, - tasks=(PregelTask(AnyStr(), "tools", interrupts=(Interrupt("before"),)),), + tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ @@ -4957,13 +5084,7 @@ async def test_start_branch_then() -> None: ] assert await tool_two.aget_state(thread1) == StateSnapshot( values={"my_key": "value", "market": "DE"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_slow", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), next=("tool_two_slow",), config=(await tool_two.checkpointer.aget_tuple(thread1)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ @@ -5005,13 +5126,7 @@ async def test_start_branch_then() -> None: } assert await tool_two.aget_state(thread2) == StateSnapshot( values={"my_key": "value", "market": "US"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_fast", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), next=("tool_two_fast",), config=(await tool_two.checkpointer.aget_tuple(thread2)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ @@ -5053,13 +5168,7 @@ async def test_start_branch_then() -> None: } assert await tool_two.aget_state(thread3) == StateSnapshot( values={"my_key": "value", "market": "US"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_fast", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), next=("tool_two_fast",), config=(await tool_two.checkpointer.aget_tuple(thread3)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ @@ -5074,13 +5183,7 @@ async def test_start_branch_then() -> None: await tool_two.aupdate_state(thread3, {"my_key": "key"}) # appends to my_key assert await tool_two.aget_state(thread3) == StateSnapshot( values={"my_key": "valuekey", "market": "US"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_fast", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), next=("tool_two_fast",), config=(await tool_two.checkpointer.aget_tuple(thread3)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ @@ -5497,28 +5600,10 @@ async def test_branch_then() -> None: ], }, }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": "054b0ced-4546-58f4-bee5-548f029e1a8e", - "name": "tool_two_slow", - "result": [], - "error": None, - "interrupts": [{"when": "before", "value": None}], - }, - }, ] assert await tool_two.aget_state(thread1) == StateSnapshot( values={"my_key": "value prepared", "market": "DE"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_slow", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), next=("tool_two_slow",), config=(await tool_two.checkpointer.aget_tuple(thread1)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ @@ -5564,13 +5649,7 @@ async def test_branch_then() -> None: } assert await tool_two.aget_state(thread2) == StateSnapshot( values={"my_key": "value prepared", "market": "US"}, - tasks=( - PregelTask( - AnyStr(), - "tool_two_fast", - interrupts=(Interrupt("before"),), - ), - ), + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), next=("tool_two_fast",), config=(await tool_two.checkpointer.aget_tuple(thread2)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ From f93512e3b33346e67880bb55e3a2649147ca64cf Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 12:33:25 -0700 Subject: [PATCH 076/287] Remove unused when values for Interrupt --- libs/langgraph/langgraph/constants.py | 4 ++-- libs/langgraph/langgraph/errors.py | 2 +- libs/langgraph/tests/test_pregel.py | 2 +- libs/langgraph/tests/test_pregel_async.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index 51b6e1437..21844a565 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -102,5 +102,5 @@ class Send: @dataclass class Interrupt: - when: Literal["before", "during", "after"] - value: Any = None + value: Any + when: Literal["during"] = "during" diff --git a/libs/langgraph/langgraph/errors.py b/libs/langgraph/langgraph/errors.py index 27a7689fa..25ba93a72 100644 --- a/libs/langgraph/langgraph/errors.py +++ b/libs/langgraph/langgraph/errors.py @@ -40,7 +40,7 @@ class NodeInterrupt(GraphInterrupt): """Raised by a node to interrupt execution.""" def __init__(self, value: Any) -> None: - super().__init__([Interrupt("during", value)]) + super().__init__([Interrupt(value)]) class EmptyInputError(Exception): diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 56e7ed5a3..015bba9a6 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -6383,7 +6383,7 @@ def test_dynamic_interrupt(snapshot: SnapshotAssertion) -> None: PregelTask( AnyStr(), "tool_two", - interrupts=(Interrupt("during", "Just because..."),), + interrupts=(Interrupt("Just because..."),), ), ), config=tool_two.checkpointer.get_tuple(thread1).config, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 0308deb54..26bea855a 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -285,7 +285,7 @@ async def test_dynamic_interrupt( PregelTask( AnyStr(), "tool_two", - interrupts=(Interrupt("during", "Just because..."),), + interrupts=(Interrupt("Just because..."),), ), ), config=tup.config, From 6c7d9c35bc34a18e132f96aa181fa8acbc822f6f Mon Sep 17 00:00:00 2001 From: vbarda Date: Thu, 22 Aug 2024 15:34:26 -0400 Subject: [PATCH 077/287] remove interrupts --- libs/langgraph/tests/test_pregel.py | 69 ++++------------------- libs/langgraph/tests/test_pregel_async.py | 61 ++++++-------------- 2 files changed, 30 insertions(+), 100 deletions(-) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 2474f6790..df6ecf8ec 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -8179,7 +8179,6 @@ def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner",), @@ -8213,7 +8212,6 @@ def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner_2", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner_2",), @@ -8493,7 +8491,6 @@ def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner",), @@ -8568,7 +8565,6 @@ def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner",), @@ -8602,7 +8598,6 @@ def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner_2", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner_2",), @@ -8858,7 +8853,6 @@ def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner",), @@ -8892,7 +8886,6 @@ def test_nested_graph_interrupts( PregelTask( AnyStr(), name="inner_2", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner_2",), @@ -9271,9 +9264,7 @@ def test_nested_graph_interrupts( assert state_history == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=( - PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), - ), + tasks=(PregelTask(AnyStr(), "inner"),), next=("inner",), config={ "configurable": { @@ -9301,11 +9292,7 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=( - PregelTask( - AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) - ), - ), + tasks=(PregelTask(AnyStr(), "inner_2"),), next=("inner_2",), config={ "configurable": { @@ -9389,9 +9376,7 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=( - PregelTask(AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),)), - ), + tasks=(PregelTask(AnyStr(), "inner_2"),), next=("inner_2",), config={ "configurable": { @@ -9440,9 +9425,7 @@ def test_nested_graph_interrupts( assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=( - PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), - ), + tasks=(PregelTask(AnyStr(), "inner"),), next=("inner",), config={ "configurable": { @@ -9470,11 +9453,7 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=( - PregelTask( - AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) - ), - ), + tasks=(PregelTask(AnyStr(), "inner_2"),), next=("inner_2",), config={ "configurable": { @@ -9506,9 +9485,7 @@ def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=( - PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), - ), + tasks=(PregelTask(AnyStr(), "inner"),), next=("inner",), config={ "configurable": { @@ -9536,11 +9513,7 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=( - PregelTask( - AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) - ), - ), + tasks=(PregelTask(AnyStr(), "inner_2"),), next=("inner_2",), config={ "configurable": { @@ -9671,9 +9644,7 @@ def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=( - PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), - ), + tasks=(PregelTask(AnyStr(), "inner"),), next=("inner",), config={ "configurable": { @@ -9701,11 +9672,7 @@ def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=( - PregelTask( - AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) - ), - ), + tasks=(PregelTask(AnyStr(), "inner_2"),), next=("inner_2",), config={ "configurable": { @@ -10126,11 +10093,7 @@ def test_nested_graph_state( # test state w/ nested subgraph state (right after interrupt) assert app.get_state(config) == StateSnapshot( values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), "inner", interrupts=(Interrupt(when="before", value=None),) - ), - ), + tasks=(PregelTask(AnyStr(), "inner"),), next=("inner",), config={ "configurable": { @@ -10160,7 +10123,6 @@ def test_nested_graph_state( AnyStr(), name="inner_2", error=None, - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner_2",), @@ -10200,7 +10162,6 @@ def test_nested_graph_state( PregelTask( AnyStr(), "inner", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner",), @@ -10235,7 +10196,6 @@ def test_nested_graph_state( AnyStr(), name="inner_2", error=None, - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner_2",), @@ -10638,7 +10598,7 @@ def test_doubly_nested_graph_state( app.invoke({"my_key": "my value"}, config, debug=True) assert app.get_state(config) == StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "child", interrupts=(Interrupt(when="before"),)),), + tasks=(PregelTask(AnyStr(), "child"),), next=("child",), config={ "configurable": { @@ -10663,11 +10623,7 @@ def test_doubly_nested_graph_state( subgraph_state_snapshots={ "child": StateSnapshot( values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), "child_1", interrupts=(Interrupt(when="before"),) - ), - ), + tasks=(PregelTask(AnyStr(), "child_1"),), next=("child_1",), config={ "configurable": { @@ -10692,7 +10648,6 @@ def test_doubly_nested_graph_state( PregelTask( AnyStr(), "grandchild_2", - interrupts=(Interrupt(when="before"),), ), ), next=("grandchild_2",), diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 409448a9d..0d6f21bf7 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -6801,7 +6801,6 @@ async def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner",), @@ -6835,7 +6834,6 @@ async def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner_2", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner_2",), @@ -7120,7 +7118,6 @@ async def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner",), @@ -7195,7 +7192,6 @@ async def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner",), @@ -7229,7 +7225,6 @@ async def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner_2", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner_2",), @@ -7488,7 +7483,6 @@ async def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner",), @@ -7522,7 +7516,6 @@ async def test_nested_graph_interrupts( PregelTask( AnyStr(), name="inner_2", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner_2",), @@ -7901,7 +7894,10 @@ async def test_nested_graph_interrupts( StateSnapshot( values={"my_key": "hi my value"}, tasks=( - PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), + PregelTask( + AnyStr(), + "inner", + ), ), next=("inner",), config={ @@ -7932,7 +7928,8 @@ async def test_nested_graph_interrupts( }, tasks=( PregelTask( - AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) + AnyStr(), + "inner_2", ), ), next=("inner_2",), @@ -8019,9 +8016,7 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=( - PregelTask(AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),)), - ), + tasks=(PregelTask(AnyStr(), "inner_2"),), next=("inner_2",), config={ "configurable": { @@ -8070,9 +8065,7 @@ async def test_nested_graph_interrupts( assert [s async for s in app.aget_state_history(config)] == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=( - PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), - ), + tasks=(PregelTask(AnyStr(), "inner"),), next=("inner",), config={ "configurable": { @@ -8100,11 +8093,7 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=( - PregelTask( - AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) - ), - ), + tasks=(PregelTask(AnyStr(), "inner_2"),), next=("inner_2",), config={ "configurable": { @@ -8136,9 +8125,7 @@ async def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=( - PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), - ), + tasks=(PregelTask(AnyStr(), "inner"),), next=("inner",), config={ "configurable": { @@ -8166,11 +8153,7 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=( - PregelTask( - AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) - ), - ), + tasks=(PregelTask(AnyStr(), "inner_2"),), next=("inner_2",), config={ "configurable": { @@ -8301,9 +8284,7 @@ async def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=( - PregelTask(AnyStr(), "inner", interrupts=(Interrupt(when="before"),)), - ), + tasks=(PregelTask(AnyStr(), "inner"),), next=("inner",), config={ "configurable": { @@ -8331,11 +8312,7 @@ async def test_nested_graph_interrupts( "my_key": "hi my value here", "my_other_key": "hi my value", }, - tasks=( - PregelTask( - AnyStr(), "inner_2", interrupts=(Interrupt(when="before"),) - ), - ), + tasks=(PregelTask(AnyStr(), "inner_2"),), next=("inner_2",), config={ "configurable": { @@ -8763,7 +8740,8 @@ async def test_nested_graph_state( values={"my_key": "hi my value"}, tasks=( PregelTask( - AnyStr(), "inner", interrupts=(Interrupt(when="before", value=None),) + AnyStr(), + "inner", ), ), next=("inner",), @@ -8795,7 +8773,6 @@ async def test_nested_graph_state( AnyStr(), name="inner_2", error=None, - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner_2",), @@ -8835,7 +8812,6 @@ async def test_nested_graph_state( PregelTask( AnyStr(), "inner", - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner",), @@ -8870,7 +8846,6 @@ async def test_nested_graph_state( AnyStr(), name="inner_2", error=None, - interrupts=(Interrupt(when="before", value=None),), ), ), next=("inner_2",), @@ -9273,7 +9248,7 @@ async def test_doubly_nested_graph_state( await app.ainvoke({"my_key": "my value"}, config, debug=True) assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "child", interrupts=(Interrupt(when="before"),)),), + tasks=(PregelTask(AnyStr(), "child"),), next=("child",), config={ "configurable": { @@ -9300,7 +9275,8 @@ async def test_doubly_nested_graph_state( values={"my_key": "hi my value"}, tasks=( PregelTask( - AnyStr(), "child_1", interrupts=(Interrupt(when="before"),) + AnyStr(), + "child_1", ), ), next=("child_1",), @@ -9327,7 +9303,6 @@ async def test_doubly_nested_graph_state( PregelTask( AnyStr(), "grandchild_2", - interrupts=(Interrupt(when="before"),), ), ), next=("grandchild_2",), From 7e32de940548574d35816a80e28bbd1f9581e261 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 12:44:26 -0700 Subject: [PATCH 078/287] Remove current_tasks from checkpoint interface (#1440) * Remove current_tasks from checkpoint interface - Not used, now clear that it can be supported with put_writes(SCHEDULE) * Add comment --- libs/checkpoint-postgres/README.md | 2 -- libs/checkpoint-sqlite/README.md | 4 +--- libs/checkpoint/README.md | 1 - libs/checkpoint/langgraph/checkpoint/base/__init__.py | 7 +------ libs/langgraph/langgraph/pregel/loop.py | 4 ---- libs/langgraph/tests/test_pregel.py | 3 --- libs/langgraph/tests/test_pregel_async.py | 3 --- 7 files changed, 2 insertions(+), 22 deletions(-) diff --git a/libs/checkpoint-postgres/README.md b/libs/checkpoint-postgres/README.md index 24a77673a..24652a2b2 100644 --- a/libs/checkpoint-postgres/README.md +++ b/libs/checkpoint-postgres/README.md @@ -44,7 +44,6 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer: } }, "pending_sends": [], - "current_tasks": {} } # store checkpoint @@ -87,7 +86,6 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer: } }, "pending_sends": [], - "current_tasks": {} } # store checkpoint diff --git a/libs/checkpoint-sqlite/README.md b/libs/checkpoint-sqlite/README.md index 86ca9cafc..73fe94333 100644 --- a/libs/checkpoint-sqlite/README.md +++ b/libs/checkpoint-sqlite/README.md @@ -35,7 +35,6 @@ with SqliteSaver.from_conn_string(":memory:") as checkpointer: } }, "pending_sends": [], - "current_tasks": {} } # store checkpoint @@ -78,7 +77,6 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer: } }, "pending_sends": [], - "current_tasks": {} } # store checkpoint @@ -89,4 +87,4 @@ async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer: # list checkpoints [c async for c in checkpointer.alist(read_config)] -``` \ No newline at end of file +``` diff --git a/libs/checkpoint/README.md b/libs/checkpoint/README.md index 7f6b26d6e..19c7d3807 100644 --- a/libs/checkpoint/README.md +++ b/libs/checkpoint/README.md @@ -74,7 +74,6 @@ checkpoint = { } }, "pending_sends": [], - "current_tasks": {} } # store checkpoint diff --git a/libs/checkpoint/langgraph/checkpoint/base/__init__.py b/libs/checkpoint/langgraph/checkpoint/base/__init__.py index 21d021ad5..a4f76f87b 100644 --- a/libs/checkpoint/langgraph/checkpoint/base/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/base/__init__.py @@ -97,9 +97,6 @@ class Checkpoint(TypedDict): pending_sends: List[SendProtocol] """List of packets sent to nodes but not yet processed. Cleared by the next checkpoint.""" - current_tasks: Dict[str, TaskInfo] - """Map from task ID to task info.""" - # TODO remove this def empty_checkpoint() -> Checkpoint: @@ -111,7 +108,6 @@ def empty_checkpoint() -> Checkpoint: channel_versions={}, versions_seen={}, pending_sends=[], - current_tasks={}, ) @@ -124,7 +120,6 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint: channel_versions=checkpoint["channel_versions"].copy(), versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()}, pending_sends=checkpoint.get("pending_sends", []).copy(), - current_tasks=checkpoint.get("current_tasks", {}).copy(), ) @@ -156,7 +151,6 @@ def create_checkpoint( channel_versions=checkpoint["channel_versions"], versions_seen=checkpoint["versions_seen"], pending_sends=checkpoint.get("pending_sends", []), - current_tasks={}, ) @@ -451,3 +445,4 @@ saving regular writes. Each Checkpointer implementation should use this mapping in put_writes. """ WRITES_IDX_MAP = {ERROR: -1} +# TODO To store scheduled status of tasks, add a special channel here diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index a501232a9..b4e6d4118 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -155,10 +155,6 @@ class PregelLoop: self.stream_keys = stream_keys self.is_nested = CONFIG_KEY_READ in self.config.get("configurable", {}) - def mark_tasks_scheduled(self, tasks: Sequence[PregelExecutableTask]) -> None: - """Mark tasks as scheduled, to be used by queue-based executors.""" - raise NotImplementedError - def put_writes(self, task_id: str, writes: Sequence[tuple[str, Any]]) -> None: """Put writes for a task, to be read by the next tick.""" if not writes: diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 015bba9a6..0f06fec2a 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -1428,7 +1428,6 @@ def test_pending_writes_resume( "v": 1, "id": AnyStr(), "ts": AnyStr(), - "current_tasks": {}, "pending_sends": [], "versions_seen": { "one": { @@ -1487,7 +1486,6 @@ def test_pending_writes_resume( "v": 1, "id": AnyStr(), "ts": AnyStr(), - "current_tasks": {}, "pending_sends": [], "versions_seen": { "__input__": {}, @@ -1535,7 +1533,6 @@ def test_pending_writes_resume( "v": 1, "id": AnyStr(), "ts": AnyStr(), - "current_tasks": {}, "pending_sends": [], "versions_seen": {"__input__": {}}, "channel_versions": { diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 26bea855a..d24a3d4f2 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1654,7 +1654,6 @@ async def test_pending_writes_resume( "v": 1, "id": AnyStr(), "ts": AnyStr(), - "current_tasks": {}, "pending_sends": [], "versions_seen": { "one": { @@ -1713,7 +1712,6 @@ async def test_pending_writes_resume( "v": 1, "id": AnyStr(), "ts": AnyStr(), - "current_tasks": {}, "pending_sends": [], "versions_seen": { "__input__": {}, @@ -1761,7 +1759,6 @@ async def test_pending_writes_resume( "v": 1, "id": AnyStr(), "ts": AnyStr(), - "current_tasks": {}, "pending_sends": [], "versions_seen": {"__input__": {}}, "channel_versions": { From 8a00a0026ed09a4ca6d65c517ab4f2e10810c343 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 12:45:20 -0700 Subject: [PATCH 079/287] checkpoint1.0.4 --- libs/checkpoint/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml index b17a77238..7ceea436e 100644 --- a/libs/checkpoint/pyproject.toml +++ b/libs/checkpoint/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint" -version = "1.0.3" +version = "1.0.4" description = "Library with base interfaces for LangGraph checkpoint savers." authors = [] license = "MIT" From 3ec419a2f621a33e242b16682f22341e1d5dd1c6 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 12:45:27 -0700 Subject: [PATCH 080/287] lib0.2.12 --- 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 1c3b20e9b..bc0b67fb4 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.11" +version = "0.2.12" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From acd8acf23708065026b6d9e6d447fdedbec38fac Mon Sep 17 00:00:00 2001 From: vbarda Date: Thu, 22 Aug 2024 16:47:12 -0400 Subject: [PATCH 081/287] lint --- libs/checkpoint/langgraph/checkpoint/memory/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 132b05c15..5d73f4ba5 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -210,7 +210,9 @@ class MemorySaver( elif limit is not None: limit -= 1 - writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values() + writes = self.writes[ + (thread_id, checkpoint_ns, checkpoint_id) + ].values() yield CheckpointTuple( config={ From 45e3d1a3f168e997baaf820af545c1f9ed172433 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 13:51:38 -0700 Subject: [PATCH 082/287] Test all checkpointers everywhere we test 1 of them --- .../tests/__snapshots__/test_pregel.ambr | 2900 +++++++++++++++++ libs/langgraph/tests/memory_assert.py | 9 - libs/langgraph/tests/test_pregel.py | 1904 ++++++----- libs/langgraph/tests/test_pregel_async.py | 1790 +++++----- 4 files changed, 4797 insertions(+), 1806 deletions(-) diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index c92bc4688..4835ec611 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -33,6 +33,142 @@ ''' # --- +# name: test_branch_then[memory] + ''' + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + + ''' +# --- +# name: test_branch_then[memory].1 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + prepare(prepare) + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + finish(finish) + __end__([__end__]):::last + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_branch_then[postgres] + ''' + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + + ''' +# --- +# name: test_branch_then[postgres].1 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + prepare(prepare) + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + finish(finish) + __end__([__end__]):::last + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_branch_then[postgres_pipe] + ''' + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + + ''' +# --- +# name: test_branch_then[postgres_pipe].1 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + prepare(prepare) + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + finish(finish) + __end__([__end__]):::last + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_branch_then[sqlite] + ''' + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + + ''' +# --- +# name: test_branch_then[sqlite].1 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + prepare(prepare) + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + finish(finish) + __end__([__end__]):::last + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- # name: test_conditional_entrypoint_graph '{"title": "LangGraphInput"}' # --- @@ -601,6 +737,1386 @@ ''' # --- +# name: test_conditional_graph[memory] + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableAssign" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[memory].1 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[memory].2 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent) + tools(tools
version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[memory].3 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": 1, + "type": "schema", + "data": "ParallelInput" + }, + { + "id": 2, + "type": "schema", + "data": "ParallelOutput" + }, + { + "id": 3, + "type": "runnable", + "data": { + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "name": "PromptTemplate" + } + }, + { + "id": 4, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "language_models", + "fake", + "FakeStreamingListLLM" + ], + "name": "FakeStreamingListLLM" + } + }, + { + "id": 5, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "name": "agent_parser" + } + }, + { + "id": 6, + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnablePassthrough" + ], + "name": "Passthrough" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": 3, + "target": 4 + }, + { + "source": 4, + "target": 5 + }, + { + "source": 1, + "target": 3 + }, + { + "source": 5, + "target": 2 + }, + { + "source": 1, + "target": 6 + }, + { + "source": 6, + "target": 2 + }, + { + "source": "__start__", + "target": 1 + }, + { + "source": "tools", + "target": 1 + }, + { + "source": 2, + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": 2, + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[memory].4 + ''' + graph TD; + PromptTemplate --> FakeStreamingListLLM; + FakeStreamingListLLM --> agent_parser; + Parallel_agent_outcome_Input --> PromptTemplate; + agent_parser --> Parallel_agent_outcome_Output; + Parallel_agent_outcome_Input --> Passthrough; + Passthrough --> Parallel_agent_outcome_Output; + __start__ --> Parallel_agent_outcome_Input; + tools --> Parallel_agent_outcome_Input; + Parallel_agent_outcome_Output -.  continue  .-> tools; + Parallel_agent_outcome_Output -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[memory].5 + dict({ + 'edges': list([ + dict({ + 'source': '__start__', + 'target': 'agent', + }), + dict({ + 'source': 'tools', + 'target': 'agent', + }), + dict({ + 'conditional': True, + 'data': 'continue', + 'source': 'agent', + 'target': 'tools', + }), + dict({ + 'conditional': True, + 'data': 'exit', + 'source': 'agent', + 'target': '__end__', + }), + ]), + 'nodes': list([ + dict({ + 'data': '__start__', + 'id': '__start__', + 'type': 'schema', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langchain', + 'schema', + 'runnable', + 'RunnableAssign', + ]), + 'name': 'agent', + }), + 'id': 'agent', + 'metadata': dict({ + '__interrupt': 'after', + }), + 'type': 'runnable', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langgraph', + 'utils', + 'RunnableCallable', + ]), + 'name': 'tools', + }), + 'id': 'tools', + 'metadata': dict({ + 'variant': 'b', + 'version': 2, + }), + 'type': 'runnable', + }), + dict({ + 'data': '__end__', + 'id': '__end__', + 'type': 'schema', + }), + ]), + }) +# --- +# name: test_conditional_graph[memory].6 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent
__interrupt = after) + tools(tools
version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[postgres] + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableAssign" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres].1 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres].2 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent) + tools(tools
version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[postgres].3 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": 1, + "type": "schema", + "data": "ParallelInput" + }, + { + "id": 2, + "type": "schema", + "data": "ParallelOutput" + }, + { + "id": 3, + "type": "runnable", + "data": { + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "name": "PromptTemplate" + } + }, + { + "id": 4, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "language_models", + "fake", + "FakeStreamingListLLM" + ], + "name": "FakeStreamingListLLM" + } + }, + { + "id": 5, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "name": "agent_parser" + } + }, + { + "id": 6, + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnablePassthrough" + ], + "name": "Passthrough" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": 3, + "target": 4 + }, + { + "source": 4, + "target": 5 + }, + { + "source": 1, + "target": 3 + }, + { + "source": 5, + "target": 2 + }, + { + "source": 1, + "target": 6 + }, + { + "source": 6, + "target": 2 + }, + { + "source": "__start__", + "target": 1 + }, + { + "source": "tools", + "target": 1 + }, + { + "source": 2, + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": 2, + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres].4 + ''' + graph TD; + PromptTemplate --> FakeStreamingListLLM; + FakeStreamingListLLM --> agent_parser; + Parallel_agent_outcome_Input --> PromptTemplate; + agent_parser --> Parallel_agent_outcome_Output; + Parallel_agent_outcome_Input --> Passthrough; + Passthrough --> Parallel_agent_outcome_Output; + __start__ --> Parallel_agent_outcome_Input; + tools --> Parallel_agent_outcome_Input; + Parallel_agent_outcome_Output -.  continue  .-> tools; + Parallel_agent_outcome_Output -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres].5 + dict({ + 'edges': list([ + dict({ + 'source': '__start__', + 'target': 'agent', + }), + dict({ + 'source': 'tools', + 'target': 'agent', + }), + dict({ + 'conditional': True, + 'data': 'continue', + 'source': 'agent', + 'target': 'tools', + }), + dict({ + 'conditional': True, + 'data': 'exit', + 'source': 'agent', + 'target': '__end__', + }), + ]), + 'nodes': list([ + dict({ + 'data': '__start__', + 'id': '__start__', + 'type': 'schema', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langchain', + 'schema', + 'runnable', + 'RunnableAssign', + ]), + 'name': 'agent', + }), + 'id': 'agent', + 'metadata': dict({ + '__interrupt': 'after', + }), + 'type': 'runnable', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langgraph', + 'utils', + 'RunnableCallable', + ]), + 'name': 'tools', + }), + 'id': 'tools', + 'metadata': dict({ + 'variant': 'b', + 'version': 2, + }), + 'type': 'runnable', + }), + dict({ + 'data': '__end__', + 'id': '__end__', + 'type': 'schema', + }), + ]), + }) +# --- +# name: test_conditional_graph[postgres].6 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent
__interrupt = after) + tools(tools
version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[postgres_pipe] + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableAssign" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres_pipe].1 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres_pipe].2 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent) + tools(tools
version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[postgres_pipe].3 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": 1, + "type": "schema", + "data": "ParallelInput" + }, + { + "id": 2, + "type": "schema", + "data": "ParallelOutput" + }, + { + "id": 3, + "type": "runnable", + "data": { + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "name": "PromptTemplate" + } + }, + { + "id": 4, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "language_models", + "fake", + "FakeStreamingListLLM" + ], + "name": "FakeStreamingListLLM" + } + }, + { + "id": 5, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "name": "agent_parser" + } + }, + { + "id": 6, + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnablePassthrough" + ], + "name": "Passthrough" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": 3, + "target": 4 + }, + { + "source": 4, + "target": 5 + }, + { + "source": 1, + "target": 3 + }, + { + "source": 5, + "target": 2 + }, + { + "source": 1, + "target": 6 + }, + { + "source": 6, + "target": 2 + }, + { + "source": "__start__", + "target": 1 + }, + { + "source": "tools", + "target": 1 + }, + { + "source": 2, + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": 2, + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres_pipe].4 + ''' + graph TD; + PromptTemplate --> FakeStreamingListLLM; + FakeStreamingListLLM --> agent_parser; + Parallel_agent_outcome_Input --> PromptTemplate; + agent_parser --> Parallel_agent_outcome_Output; + Parallel_agent_outcome_Input --> Passthrough; + Passthrough --> Parallel_agent_outcome_Output; + __start__ --> Parallel_agent_outcome_Input; + tools --> Parallel_agent_outcome_Input; + Parallel_agent_outcome_Output -.  continue  .-> tools; + Parallel_agent_outcome_Output -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres_pipe].5 + dict({ + 'edges': list([ + dict({ + 'source': '__start__', + 'target': 'agent', + }), + dict({ + 'source': 'tools', + 'target': 'agent', + }), + dict({ + 'conditional': True, + 'data': 'continue', + 'source': 'agent', + 'target': 'tools', + }), + dict({ + 'conditional': True, + 'data': 'exit', + 'source': 'agent', + 'target': '__end__', + }), + ]), + 'nodes': list([ + dict({ + 'data': '__start__', + 'id': '__start__', + 'type': 'schema', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langchain', + 'schema', + 'runnable', + 'RunnableAssign', + ]), + 'name': 'agent', + }), + 'id': 'agent', + 'metadata': dict({ + '__interrupt': 'after', + }), + 'type': 'runnable', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langgraph', + 'utils', + 'RunnableCallable', + ]), + 'name': 'tools', + }), + 'id': 'tools', + 'metadata': dict({ + 'variant': 'b', + 'version': 2, + }), + 'type': 'runnable', + }), + dict({ + 'data': '__end__', + 'id': '__end__', + 'type': 'schema', + }), + ]), + }) +# --- +# name: test_conditional_graph[postgres_pipe].6 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent
__interrupt = after) + tools(tools
version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[sqlite] + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableAssign" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[sqlite].1 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[sqlite].2 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent) + tools(tools
version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[sqlite].3 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": 1, + "type": "schema", + "data": "ParallelInput" + }, + { + "id": 2, + "type": "schema", + "data": "ParallelOutput" + }, + { + "id": 3, + "type": "runnable", + "data": { + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "name": "PromptTemplate" + } + }, + { + "id": 4, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "language_models", + "fake", + "FakeStreamingListLLM" + ], + "name": "FakeStreamingListLLM" + } + }, + { + "id": 5, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "name": "agent_parser" + } + }, + { + "id": 6, + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnablePassthrough" + ], + "name": "Passthrough" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": 3, + "target": 4 + }, + { + "source": 4, + "target": 5 + }, + { + "source": 1, + "target": 3 + }, + { + "source": 5, + "target": 2 + }, + { + "source": 1, + "target": 6 + }, + { + "source": 6, + "target": 2 + }, + { + "source": "__start__", + "target": 1 + }, + { + "source": "tools", + "target": 1 + }, + { + "source": 2, + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": 2, + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[sqlite].4 + ''' + graph TD; + PromptTemplate --> FakeStreamingListLLM; + FakeStreamingListLLM --> agent_parser; + Parallel_agent_outcome_Input --> PromptTemplate; + agent_parser --> Parallel_agent_outcome_Output; + Parallel_agent_outcome_Input --> Passthrough; + Passthrough --> Parallel_agent_outcome_Output; + __start__ --> Parallel_agent_outcome_Input; + tools --> Parallel_agent_outcome_Input; + Parallel_agent_outcome_Output -.  continue  .-> tools; + Parallel_agent_outcome_Output -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[sqlite].5 + dict({ + 'edges': list([ + dict({ + 'source': '__start__', + 'target': 'agent', + }), + dict({ + 'source': 'tools', + 'target': 'agent', + }), + dict({ + 'conditional': True, + 'data': 'continue', + 'source': 'agent', + 'target': 'tools', + }), + dict({ + 'conditional': True, + 'data': 'exit', + 'source': 'agent', + 'target': '__end__', + }), + ]), + 'nodes': list([ + dict({ + 'data': '__start__', + 'id': '__start__', + 'type': 'schema', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langchain', + 'schema', + 'runnable', + 'RunnableAssign', + ]), + 'name': 'agent', + }), + 'id': 'agent', + 'metadata': dict({ + '__interrupt': 'after', + }), + 'type': 'runnable', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langgraph', + 'utils', + 'RunnableCallable', + ]), + 'name': 'tools', + }), + 'id': 'tools', + 'metadata': dict({ + 'variant': 'b', + 'version': 2, + }), + 'type': 'runnable', + }), + dict({ + 'data': '__end__', + 'id': '__end__', + 'type': 'schema', + }), + ]), + }) +# --- +# name: test_conditional_graph[sqlite].6 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent
__interrupt = after) + tools(tools
version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- # name: test_conditional_state_graph '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' # --- @@ -682,6 +2198,330 @@ ''' # --- +# name: test_conditional_state_graph[memory] + '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' +# --- +# name: test_conditional_state_graph[memory].1 + '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' +# --- +# name: test_conditional_state_graph[memory].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_state_graph[memory].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_state_graph[postgres] + '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' +# --- +# name: test_conditional_state_graph[postgres].1 + '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' +# --- +# name: test_conditional_state_graph[postgres].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_state_graph[postgres].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_state_graph[postgres_pipe] + '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' +# --- +# name: test_conditional_state_graph[postgres_pipe].1 + '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' +# --- +# name: test_conditional_state_graph[postgres_pipe].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_state_graph[postgres_pipe].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_state_graph[sqlite] + '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' +# --- +# name: test_conditional_state_graph[sqlite].1 + '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' +# --- +# name: test_conditional_state_graph[sqlite].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_state_graph[sqlite].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- # name: test_dynamic_interrupt ''' %%{init: {'flowchart': {'curve': 'linear'}}}%% @@ -713,6 +2553,58 @@ ''' # --- +# name: test_in_one_fan_out_state_graph_waiting_edge[memory] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge[postgres] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge[postgres_pipe] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge[sqlite] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + + ''' +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class ''' graph TD; @@ -796,6 +2688,286 @@ 'type': 'object', }) # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[memory] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[memory].1 + dict({ + 'definitions': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/definitions/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[memory].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres].1 + dict({ + 'definitions': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/definitions/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pipe] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pipe].1 + dict({ + 'definitions': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/definitions/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pipe].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[sqlite] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[sqlite].1 + dict({ + 'definitions': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/definitions/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[sqlite].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2 ''' graph TD; @@ -866,6 +3038,286 @@ 'type': 'object', }) # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pipe] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pipe].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pipe].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_via_branch ''' graph TD; @@ -879,6 +3331,58 @@ ''' # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[memory] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres_pipe] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[sqlite] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- # name: test_message_graph '{"title": "LangGraphInput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' # --- @@ -960,6 +3464,330 @@ ''' # --- +# name: test_message_graph[memory] + '{"title": "LangGraphInput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' +# --- +# name: test_message_graph[memory].1 + '{"title": "LangGraphOutput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' +# --- +# name: test_message_graph[memory].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "tests", + "test_pregel", + "FakeFuntionChatModel" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "prebuilt", + "tool_node", + "ToolNode" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "end", + "conditional": true + } + ] + } + ''' +# --- +# name: test_message_graph[memory].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  end  .-> __end__; + + ''' +# --- +# name: test_message_graph[postgres] + '{"title": "LangGraphInput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' +# --- +# name: test_message_graph[postgres].1 + '{"title": "LangGraphOutput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' +# --- +# name: test_message_graph[postgres].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "tests", + "test_pregel", + "FakeFuntionChatModel" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "prebuilt", + "tool_node", + "ToolNode" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "end", + "conditional": true + } + ] + } + ''' +# --- +# name: test_message_graph[postgres].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  end  .-> __end__; + + ''' +# --- +# name: test_message_graph[postgres_pipe] + '{"title": "LangGraphInput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' +# --- +# name: test_message_graph[postgres_pipe].1 + '{"title": "LangGraphOutput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' +# --- +# name: test_message_graph[postgres_pipe].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "tests", + "test_pregel", + "FakeFuntionChatModel" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "prebuilt", + "tool_node", + "ToolNode" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "end", + "conditional": true + } + ] + } + ''' +# --- +# name: test_message_graph[postgres_pipe].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  end  .-> __end__; + + ''' +# --- +# name: test_message_graph[sqlite] + '{"title": "LangGraphInput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' +# --- +# name: test_message_graph[sqlite].1 + '{"title": "LangGraphOutput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' +# --- +# name: test_message_graph[sqlite].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "tests", + "test_pregel", + "FakeFuntionChatModel" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "prebuilt", + "tool_node", + "ToolNode" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "end", + "conditional": true + } + ] + } + ''' +# --- +# name: test_message_graph[sqlite].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  end  .-> __end__; + + ''' +# --- # name: test_nested_graph ''' graph TD; @@ -1347,6 +4175,78 @@ ''' # --- +# name: test_start_branch_then[memory] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_start_branch_then[postgres] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_start_branch_then[postgres_pipe] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_start_branch_then[sqlite] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- # name: test_state_graph_w_config '{"title": "LangGraphConfig", "type": "object", "properties": {"configurable": {"$ref": "#/definitions/Configurable"}}, "definitions": {"Configurable": {"title": "Configurable", "type": "object", "properties": {"tools": {"title": "Tools", "type": "array", "items": {"type": "string"}}}}}}' # --- diff --git a/libs/langgraph/tests/memory_assert.py b/libs/langgraph/tests/memory_assert.py index b02cbb65b..0b0bcf62f 100644 --- a/libs/langgraph/tests/memory_assert.py +++ b/libs/langgraph/tests/memory_assert.py @@ -74,15 +74,6 @@ class MemorySaverAssertCheckpointMetadata(MemorySaver): should produce a side effect that can be asserted. """ - serde = NoopSerializer() - - def __init__( - self, - *, - serde: Optional[SerializerProtocol] = None, - ) -> None: - super().__init__(serde=serde) - def put( self, config: RunnableConfig, diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 0f06fec2a..06a747a17 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -49,9 +49,6 @@ from langgraph.checkpoint.base import ( CheckpointTuple, ) from langgraph.checkpoint.memory import MemorySaver -from langgraph.checkpoint.serde.base import SerializerProtocol -from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer -from langgraph.checkpoint.sqlite import SqliteSaver from langgraph.constants import ERROR, Interrupt, Send from langgraph.errors import InvalidUpdateError, NodeInterrupt from langgraph.graph import END, Graph @@ -71,9 +68,7 @@ from tests.any_str import AnyStr, AnyVersion, ExceptionLike, UnsortedSequence from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, - MemorySaverAssertImmutable, MemorySaverNoPending, - NoopSerializer, ) from tests.messages import _AnyIdAIMessage, _AnyIdHumanMessage @@ -1248,7 +1243,16 @@ def test_invoke_two_processes_two_in_two_out_valid(mocker: MockerFixture) -> Non assert app.invoke(2) == [3, 3] -def test_invoke_checkpoint(mocker: MockerFixture) -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_invoke_checkpoint_two( + mocker: MockerFixture, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) add_one = mocker.Mock(side_effect=lambda x: x["total"] + x["input"]) errored_once = False @@ -1271,8 +1275,6 @@ def test_invoke_checkpoint(mocker: MockerFixture) -> None: | raise_if_above_10 ) - memory = MemorySaverAssertImmutable() - app = Pregel( nodes={"one": one}, channels={ @@ -1282,26 +1284,26 @@ def test_invoke_checkpoint(mocker: MockerFixture) -> None: }, input_channels="input", output_channels="output", - checkpointer=memory, + checkpointer=checkpointer, retry_policy=RetryPolicy(), ) # total starts out as 0, so output is 0+2=2 assert app.invoke(2, {"configurable": {"thread_id": "1"}}) == 2 - checkpoint = memory.get({"configurable": {"thread_id": "1"}}) + checkpoint = checkpointer.get({"configurable": {"thread_id": "1"}}) assert checkpoint is not None assert checkpoint["channel_values"].get("total") == 2 # total is now 2, so output is 2+3=5 assert app.invoke(3, {"configurable": {"thread_id": "1"}}) == 5 assert errored_once, "errored and retried" - checkpoint_tup = memory.get_tuple({"configurable": {"thread_id": "1"}}) + checkpoint_tup = checkpointer.get_tuple({"configurable": {"thread_id": "1"}}) assert checkpoint_tup is not None assert checkpoint_tup.checkpoint["channel_values"].get("total") == 7 # total is now 2+5=7, so output would be 7+4=11, but raises ValueError with pytest.raises(ValueError): app.invoke(4, {"configurable": {"thread_id": "1"}}) # checkpoint is not updated, error is recorded - checkpoint_tup = memory.get_tuple({"configurable": {"thread_id": "1"}}) + checkpoint_tup = checkpointer.get_tuple({"configurable": {"thread_id": "1"}}) assert checkpoint_tup is not None assert checkpoint_tup.checkpoint["channel_values"].get("total") == 7 assert checkpoint_tup.pending_writes == [ @@ -1309,10 +1311,10 @@ def test_invoke_checkpoint(mocker: MockerFixture) -> None: ] # on a new thread, total starts out as 0, so output is 0+5=5 assert app.invoke(5, {"configurable": {"thread_id": "2"}}) == 5 - checkpoint = memory.get({"configurable": {"thread_id": "1"}}) + checkpoint = checkpointer.get({"configurable": {"thread_id": "1"}}) assert checkpoint is not None assert checkpoint["channel_values"].get("total") == 7 - checkpoint = memory.get({"configurable": {"thread_id": "2"}}) + checkpoint = checkpointer.get({"configurable": {"thread_id": "2"}}) assert checkpoint is not None assert checkpoint["channel_values"].get("total") == 5 @@ -1599,7 +1601,14 @@ async def test_checkpointer_null_pending_writes() -> None: ] * 4 -def test_invoke_checkpoint_sqlite(mocker: MockerFixture) -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_invoke_checkpoint_three( + mocker: MockerFixture, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") adder = mocker.Mock(side_effect=lambda x: x["total"] + x["input"]) def raise_if_above_10(input: int) -> int: @@ -1614,121 +1623,119 @@ def test_invoke_checkpoint_sqlite(mocker: MockerFixture) -> None: | raise_if_above_10 ) - with SqliteSaver.from_conn_string(":memory:") as memory: - app = Pregel( - nodes={"one": one}, - channels={ - "total": BinaryOperatorAggregate(int, operator.add), - "input": LastValue(int), - "output": LastValue(int), - }, - input_channels="input", - output_channels="output", - checkpointer=memory, - ) + app = Pregel( + nodes={"one": one}, + channels={ + "total": BinaryOperatorAggregate(int, operator.add), + "input": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", + checkpointer=checkpointer, + ) - thread_1 = {"configurable": {"thread_id": "1"}} - # total starts out as 0, so output is 0+2=2 - assert app.invoke(2, thread_1, debug=1) == 2 - state = app.get_state(thread_1) - assert state is not None - assert state.values.get("total") == 2 - assert state.next == () - assert ( - state.config["configurable"]["checkpoint_id"] == memory.get(thread_1)["id"] - ) - # total is now 2, so output is 2+3=5 - assert app.invoke(3, thread_1) == 5 - state = app.get_state(thread_1) - assert state is not None - assert state.values.get("total") == 7 - assert ( - state.config["configurable"]["checkpoint_id"] == memory.get(thread_1)["id"] - ) - # total is now 2+5=7, so output would be 7+4=11, but raises ValueError - with pytest.raises(ValueError): - app.invoke(4, thread_1) - # checkpoint is updated with new input - state = app.get_state(thread_1) - assert state is not None - assert state.values.get("total") == 7 - assert state.next == ("one",) - """we checkpoint inputs and it failed on "one", so the next node is one""" - # we can recover from error by sending new inputs - assert app.invoke(2, thread_1) == 9 - state = app.get_state(thread_1) - assert state is not None - assert state.values.get("total") == 16, "total is now 7+9=16" - assert state.next == () + thread_1 = {"configurable": {"thread_id": "1"}} + # total starts out as 0, so output is 0+2=2 + assert app.invoke(2, thread_1, debug=1) == 2 + state = app.get_state(thread_1) + assert state is not None + assert state.values.get("total") == 2 + assert state.next == () + assert ( + state.config["configurable"]["checkpoint_id"] + == checkpointer.get(thread_1)["id"] + ) + # total is now 2, so output is 2+3=5 + assert app.invoke(3, thread_1) == 5 + state = app.get_state(thread_1) + assert state is not None + assert state.values.get("total") == 7 + assert ( + state.config["configurable"]["checkpoint_id"] + == checkpointer.get(thread_1)["id"] + ) + # total is now 2+5=7, so output would be 7+4=11, but raises ValueError + with pytest.raises(ValueError): + app.invoke(4, thread_1) + # checkpoint is updated with new input + state = app.get_state(thread_1) + assert state is not None + assert state.values.get("total") == 7 + assert state.next == ("one",) + """we checkpoint inputs and it failed on "one", so the next node is one""" + # we can recover from error by sending new inputs + assert app.invoke(2, thread_1) == 9 + state = app.get_state(thread_1) + assert state is not None + assert state.values.get("total") == 16, "total is now 7+9=16" + assert state.next == () - thread_2 = {"configurable": {"thread_id": "2"}} - # on a new thread, total starts out as 0, so output is 0+5=5 - assert app.invoke(5, thread_2, debug=True) == 5 - state = app.get_state({"configurable": {"thread_id": "1"}}) - assert state is not None - assert state.values.get("total") == 16 - assert state.next == (), "checkpoint of other thread not touched" - state = app.get_state(thread_2) - assert state is not None - assert state.values.get("total") == 5 - assert state.next == () + thread_2 = {"configurable": {"thread_id": "2"}} + # on a new thread, total starts out as 0, so output is 0+5=5 + assert app.invoke(5, thread_2, debug=True) == 5 + state = app.get_state({"configurable": {"thread_id": "1"}}) + assert state is not None + assert state.values.get("total") == 16 + assert state.next == (), "checkpoint of other thread not touched" + state = app.get_state(thread_2) + assert state is not None + assert state.values.get("total") == 5 + assert state.next == () - assert len(list(app.get_state_history(thread_1, limit=1))) == 1 - # list all checkpoints for thread 1 - thread_1_history = [c for c in app.get_state_history(thread_1)] - # there are 7 checkpoints - assert len(thread_1_history) == 7 - assert Counter(c.metadata["source"] for c in thread_1_history) == { - "input": 4, - "loop": 3, - } - # sorted descending - assert ( - thread_1_history[0].config["configurable"]["checkpoint_id"] - > thread_1_history[1].config["configurable"]["checkpoint_id"] - ) - # cursor pagination - cursored = list( - app.get_state_history(thread_1, limit=1, before=thread_1_history[0].config) - ) - assert len(cursored) == 1 - assert cursored[0].config == thread_1_history[1].config - # the last checkpoint - assert thread_1_history[0].values["total"] == 16 - # the first "loop" checkpoint - assert thread_1_history[-2].values["total"] == 2 - # can get each checkpoint using aget with config - assert ( - memory.get(thread_1_history[0].config)["id"] - == thread_1_history[0].config["configurable"]["checkpoint_id"] - ) - assert ( - memory.get(thread_1_history[1].config)["id"] - == thread_1_history[1].config["configurable"]["checkpoint_id"] - ) + assert len(list(app.get_state_history(thread_1, limit=1))) == 1 + # list all checkpoints for thread 1 + thread_1_history = [c for c in app.get_state_history(thread_1)] + # there are 7 checkpoints + assert len(thread_1_history) == 7 + assert Counter(c.metadata["source"] for c in thread_1_history) == { + "input": 4, + "loop": 3, + } + # sorted descending + assert ( + thread_1_history[0].config["configurable"]["checkpoint_id"] + > thread_1_history[1].config["configurable"]["checkpoint_id"] + ) + # cursor pagination + cursored = list( + app.get_state_history(thread_1, limit=1, before=thread_1_history[0].config) + ) + assert len(cursored) == 1 + assert cursored[0].config == thread_1_history[1].config + # the last checkpoint + assert thread_1_history[0].values["total"] == 16 + # the first "loop" checkpoint + assert thread_1_history[-2].values["total"] == 2 + # can get each checkpoint using aget with config + assert ( + checkpointer.get(thread_1_history[0].config)["id"] + == thread_1_history[0].config["configurable"]["checkpoint_id"] + ) + assert ( + checkpointer.get(thread_1_history[1].config)["id"] + == thread_1_history[1].config["configurable"]["checkpoint_id"] + ) - thread_1_next_config = app.update_state(thread_1_history[1].config, 10) - # update creates a new checkpoint - assert ( - thread_1_next_config["configurable"]["checkpoint_id"] - > thread_1_history[0].config["configurable"]["checkpoint_id"] - ) - # update makes new checkpoint child of the previous one - assert ( - app.get_state(thread_1_next_config).parent_config - == thread_1_history[1].config - ) - # 1 more checkpoint in history - assert len(list(app.get_state_history(thread_1))) == 8 - assert Counter( - c.metadata["source"] for c in app.get_state_history(thread_1) - ) == { - "update": 1, - "input": 4, - "loop": 3, - } - # the latest checkpoint is the updated one - assert app.get_state(thread_1) == app.get_state(thread_1_next_config) + thread_1_next_config = app.update_state(thread_1_history[1].config, 10) + # update creates a new checkpoint + assert ( + thread_1_next_config["configurable"]["checkpoint_id"] + > thread_1_history[0].config["configurable"]["checkpoint_id"] + ) + # update makes new checkpoint child of the previous one + assert ( + app.get_state(thread_1_next_config).parent_config == thread_1_history[1].config + ) + # 1 more checkpoint in history + assert len(list(app.get_state_history(thread_1))) == 8 + assert Counter(c.metadata["source"] for c in app.get_state_history(thread_1)) == { + "update": 1, + "input": 4, + "loop": 3, + } + # the latest checkpoint is the updated one + assert app.get_state(thread_1) == app.get_state(thread_1_next_config) def test_invoke_two_processes_two_in_join_two_out(mocker: MockerFixture) -> None: @@ -1927,7 +1934,13 @@ def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: assert cleanup.call_count == 1, "Expected cleanup to be called once" -def test_conditional_graph(snapshot: SnapshotAssertion) -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_conditional_graph( + snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: from copy import deepcopy from langchain_core.agents import AgentAction, AgentFinish @@ -1936,6 +1949,10 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: from langchain_core.runnables import RunnablePassthrough from langchain_core.tools import tool + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + # Assemble the tools @tool() def search_api(query: str) -> str: @@ -1973,7 +1990,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: ) if data.get("intermediate_steps") is None: data["intermediate_steps"] = [] - data["intermediate_steps"].append((agent_action, observation)) + data["intermediate_steps"].append([agent_action, observation]) return data # Define decision-making logic @@ -2009,22 +2026,22 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: assert app.invoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2045,14 +2062,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -2060,14 +2077,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2080,22 +2097,22 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -2103,22 +2120,22 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2130,7 +2147,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: # test state get/update methods with interrupt_after app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -2238,14 +2255,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -2253,14 +2270,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2276,14 +2293,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2297,14 +2314,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2323,14 +2340,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2345,7 +2362,7 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: # test state get/update methods with interrupt_before app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], ) config = {"configurable": {"thread_id": "2"}} @@ -2445,14 +2462,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -2460,14 +2477,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2483,14 +2500,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2504,14 +2521,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2530,14 +2547,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2552,10 +2569,10 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: # test re-invoke to continue with interrupt_before app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], ) - config = {"configurable": {"thread_id": "2"}} + config = {"configurable": {"thread_id": "3"}} llm.i = 0 # reset the llm assert [ @@ -2608,14 +2625,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -2623,14 +2640,14 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2646,22 +2663,22 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -2669,22 +2686,22 @@ def test_conditional_graph(snapshot: SnapshotAssertion) -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2780,14 +2797,24 @@ def test_conditional_entrypoint_to_multiple_state_graph( } +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) def test_conditional_state_graph( - snapshot: SnapshotAssertion, mocker: MockerFixture + snapshot: SnapshotAssertion, + mocker: MockerFixture, + request: pytest.FixtureRequest, + checkpointer_name: str, ) -> None: from langchain_core.agents import AgentAction, AgentFinish from langchain_core.language_models.fake import FakeStreamingListLLM from langchain_core.prompts import PromptTemplate from langchain_core.tools import tool + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) setup = mocker.Mock() teardown = mocker.Mock() @@ -2870,7 +2897,7 @@ def test_conditional_state_graph( observation = {t.name: t for t in tools}[agent_action.tool].invoke( agent_action.tool_input ) - return {"intermediate_steps": [(agent_action, observation)]} + return {"intermediate_steps": [[agent_action, observation]]} # Define decision-making logic def should_continue(data: AgentState) -> str: @@ -2907,22 +2934,22 @@ def test_conditional_state_graph( assert app.invoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2943,14 +2970,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -2966,14 +2993,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -2989,7 +3016,7 @@ def test_conditional_state_graph( # test state get/update methods with interrupt_after app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -3083,14 +3110,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -3123,14 +3150,14 @@ def test_conditional_state_graph( log="finish:a really nice answer", ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], }, tasks=(), @@ -3155,7 +3182,7 @@ def test_conditional_state_graph( # test state get/update methods with interrupt_before app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], debug=True, ) @@ -3245,14 +3272,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -3284,14 +3311,14 @@ def test_conditional_state_graph( log="finish:a really nice answer", ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], }, tasks=(), @@ -3315,7 +3342,7 @@ def test_conditional_state_graph( # test w interrupt before all app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before="*", debug=True, ) @@ -3379,14 +3406,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -3398,14 +3425,14 @@ def test_conditional_state_graph( tool="search_api", tool_input="query", log="tool:search_api:query" ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], }, tasks=(PregelTask(AnyStr(), "agent"),), @@ -3418,14 +3445,14 @@ def test_conditional_state_graph( "writes": { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -3447,7 +3474,7 @@ def test_conditional_state_graph( # test w interrupt after all app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after="*", ) config = {"configurable": {"thread_id": "4"}} @@ -3496,14 +3523,14 @@ def test_conditional_state_graph( { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -3515,14 +3542,14 @@ def test_conditional_state_graph( tool="search_api", tool_input="query", log="tool:search_api:query" ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], }, tasks=(PregelTask(AnyStr(), "agent"),), @@ -3535,14 +3562,14 @@ def test_conditional_state_graph( "writes": { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -4010,8 +4037,13 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: ] -@pytest.mark.parametrize("serde", [NoopSerializer(), JsonPlusSerializer()]) -def test_state_graph_packets(serde: SerializerProtocol) -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_state_graph_packets( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, ) @@ -4024,6 +4056,10 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: ) from langchain_core.tools import tool + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + class AgentState(TypedDict): messages: Annotated[list[BaseMessage], add_messages] @@ -4244,7 +4280,7 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(serde=serde), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -4521,9 +4557,15 @@ def test_state_graph_packets(serde: SerializerProtocol) -> None: ) +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) def test_message_graph( snapshot: SnapshotAssertion, deterministic_uuids: MockerFixture, + request: pytest.FixtureRequest, + checkpointer_name: str, ) -> None: from copy import deepcopy @@ -4540,6 +4582,10 @@ def test_message_graph( from langchain_core.outputs import ChatGeneration, ChatResult from langchain_core.tools import tool + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + class FakeFuntionChatModel(FakeMessagesListChatModel): def bind_functions(self, functions: list): return self @@ -4743,7 +4789,7 @@ def test_message_graph( ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -4973,7 +5019,7 @@ def test_message_graph( ) app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], ) config = {"configurable": {"thread_id": "2"}} @@ -5236,15 +5282,20 @@ def test_message_graph( metadata={ "source": "update", "step": 6, - "writes": {"tools": ("ai", "an extra message")}, + "writes": {"tools": UnsortedSequence("ai", "an extra message")}, }, parent_config=[*app_w_interrupt.checkpointer.list(config, limit=2)][-1].config, ) +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) def test_root_graph( - snapshot: SnapshotAssertion, deterministic_uuids: MockerFixture, + request: pytest.FixtureRequest, + checkpointer_name: str, ) -> None: from copy import deepcopy @@ -5261,6 +5312,10 @@ def test_root_graph( from langchain_core.outputs import ChatGeneration, ChatResult from langchain_core.tools import tool + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + class FakeFuntionChatModel(FakeMessagesListChatModel): def bind_functions(self, functions: list): return self @@ -5462,7 +5517,7 @@ def test_root_graph( ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -5692,7 +5747,7 @@ def test_root_graph( ) app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], ) config = {"configurable": {"thread_id": "2"}} @@ -5955,7 +6010,7 @@ def test_root_graph( metadata={ "source": "update", "step": 6, - "writes": {"tools": ("ai", "an extra message")}, + "writes": {"tools": UnsortedSequence("ai", "an extra message")}, }, parent_config=[*app_w_interrupt.checkpointer.list(config, limit=2)][-1].config, ) @@ -5991,7 +6046,7 @@ def test_root_graph( }, ) new_workflow.add_edge("tools", "agent") - new_app = new_workflow.compile(checkpointer=app_w_interrupt.checkpointer) + new_app = new_workflow.compile(checkpointer=checkpointer) model.i = 0 # reset the llm # previous state is converted to new schema @@ -6027,7 +6082,7 @@ def test_root_graph( metadata={ "source": "update", "step": 6, - "writes": {"tools": ("ai", "an extra message")}, + "writes": {"tools": UnsortedSequence("ai", "an extra message")}, }, parent_config=[*app_w_interrupt.checkpointer.list(config, limit=2)][-1].config, ) @@ -6310,7 +6365,15 @@ def test_in_one_fan_out_out_one_graph_state() -> None: ] -def test_dynamic_interrupt(snapshot: SnapshotAssertion) -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_dynamic_interrupt( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") + class State(TypedDict): my_key: Annotated[str, operator.add] market: str @@ -6348,49 +6411,56 @@ def test_dynamic_interrupt(snapshot: SnapshotAssertion) -> None: "market": "US", } - with SqliteSaver.from_conn_string(":memory:") as saver: - tool_two = tool_two_graph.compile(checkpointer=saver) + tool_two = tool_two_graph.compile(checkpointer=checkpointer) - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - tool_two.invoke({"my_key": "value", "market": "DE"}) + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + tool_two.invoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == { - "my_key": "value ⛰️", - "market": "DE", - } - assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ - { - "source": "loop", - "step": 0, - "writes": None, - }, - { - "source": "input", - "step": -1, - "writes": {"my_key": "value ⛰️", "market": "DE"}, - }, - ] - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value ⛰️", "market": "DE"}, - next=("tool_two",), - tasks=( - PregelTask( - AnyStr(), - "tool_two", - interrupts=(Interrupt("Just because..."),), - ), + thread1 = {"configurable": {"thread_id": "1"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == { + "my_key": "value ⛰️", + "market": "DE", + } + assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ + { + "source": "loop", + "step": 0, + "writes": None, + }, + { + "source": "input", + "step": -1, + "writes": {"my_key": "value ⛰️", "market": "DE"}, + }, + ] + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value ⛰️", "market": "DE"}, + next=("tool_two",), + tasks=( + PregelTask( + AnyStr(), + "tool_two", + interrupts=(Interrupt("Just because..."),), ), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) + ), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={"source": "loop", "step": 0, "writes": None}, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) -def test_start_branch_then(snapshot: SnapshotAssertion) -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_start_branch_then( + snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") + class State(TypedDict): my_key: Annotated[str, operator.add] market: str @@ -6435,148 +6505,155 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: "market": "US", } - with SqliteSaver.from_conn_string(":memory:") as saver: - tool_two = tool_two_graph.compile( - store=MemoryStore(), - checkpointer=saver, - interrupt_before=["tool_two_fast", "tool_two_slow"], - ) + tool_two = tool_two_graph.compile( + store=MemoryStore(), + checkpointer=checkpointer, + interrupt_before=["tool_two_fast", "tool_two_slow"], + ) - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - tool_two.invoke({"my_key": "value", "market": "DE"}) + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + tool_two.invoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1", "assistant_id": "a"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == { - "my_key": "value ⛰️", - "market": "DE", - } - assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ - { - "source": "loop", - "step": 0, - "writes": None, - }, - { - "source": "input", - "step": -1, - "writes": {"my_key": "value ⛰️", "market": "DE"}, - }, - ] - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value ⛰️", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread1, debug=1) == { - "my_key": "value ⛰️ slow", - "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value ⛰️ slow", "market": "DE"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"tool_two_slow": {"my_key": " slow"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) + thread1 = {"configurable": {"thread_id": "1", "assistant_id": "a"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == { + "my_key": "value ⛰️", + "market": "DE", + } + assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ + { + "source": "loop", + "step": 0, + "writes": None, + }, + { + "source": "input", + "step": -1, + "writes": {"my_key": "value ⛰️", "market": "DE"}, + }, + ] + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value ⛰️", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={"source": "loop", "step": 0, "writes": None}, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread1, debug=1) == { + "my_key": "value ⛰️ slow", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value ⛰️ slow", "market": "DE"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"tool_two_slow": {"my_key": " slow"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) - thread2 = {"configurable": {"thread_id": "2", "assistant_id": "a"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread2, debug=1) == { - "my_key": "value fast", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value fast", "market": "US"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"tool_two_fast": {"my_key": " fast"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) + thread2 = {"configurable": {"thread_id": "2", "assistant_id": "a"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={"source": "loop", "step": 0, "writes": None}, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread2, debug=1) == { + "my_key": "value fast", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value fast", "market": "US"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"tool_two_fast": {"my_key": " fast"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) - thread3 = {"configurable": {"thread_id": "3", "assistant_id": "b"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "US"}, thread3) == { - "my_key": "value", - "market": "US", - } - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=tool_two.checkpointer.get_tuple(thread3).config, - created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, - parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, - ) - # update state - tool_two.update_state(thread3, {"my_key": "key"}) # appends to my_key - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "valuekey", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=tool_two.checkpointer.get_tuple(thread3).config, - created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], - metadata={ - "source": "update", - "step": 1, - "writes": {START: {"my_key": "key"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread3, debug=1) == { - "my_key": "valuekey fast", - "market": "US", - } - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "valuekey fast", "market": "US"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread3).config, - created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 2, - "writes": {"tool_two_fast": {"my_key": " fast"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, - ) + thread3 = {"configurable": {"thread_id": "3", "assistant_id": "b"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "US"}, thread3) == { + "my_key": "value", + "market": "US", + } + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "value", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=tool_two.checkpointer.get_tuple(thread3).config, + created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], + metadata={"source": "loop", "step": 0, "writes": None}, + parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, + ) + # update state + tool_two.update_state(thread3, {"my_key": "key"}) # appends to my_key + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "valuekey", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=tool_two.checkpointer.get_tuple(thread3).config, + created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], + metadata={ + "source": "update", + "step": 1, + "writes": {START: {"my_key": "key"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread3, debug=1) == { + "my_key": "valuekey fast", + "market": "US", + } + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "valuekey fast", "market": "US"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread3).config, + created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 2, + "writes": {"tool_two_fast": {"my_key": " fast"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, + ) -def test_branch_then(snapshot: SnapshotAssertion) -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_branch_then( + snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") + class State(TypedDict): my_key: Annotated[str, operator.add] market: str @@ -6606,504 +6683,509 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None: "market": "US", } - with SqliteSaver.from_conn_string(":memory:") as saver: - # test stream_mode=debug - tool_two = tool_two_graph.compile(checkpointer=saver) - thread10 = {"configurable": {"thread_id": "10"}} - assert [ - *tool_two.stream( - {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" - ) - ] == [ - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": -1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": {"my_key": ""}, - "metadata": { - "source": "input", - "step": -1, - "writes": {"my_key": "value", "market": "DE"}, - }, - "next": ["__start__"], - "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 0, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 0, - "writes": None, - }, - "next": ["prepare"], - "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], - }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", - "name": "prepare", - "input": {"my_key": "value", "market": "DE"}, - "triggers": ["start:prepare"], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", - "name": "prepare", - "result": [("my_key", " prepared")], - "error": None, - "interrupts": [], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value prepared", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - "next": ["tool_two_slow"], - "tasks": [ - {"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()} - ], - }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", - "name": "tool_two_slow", - "input": {"my_key": "value prepared", "market": "DE"}, - "triggers": ["branch:prepare:condition:tool_two_slow"], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", - "name": "tool_two_slow", - "result": [("my_key", " slow")], - "error": None, - "interrupts": [], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value prepared slow", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 2, - "writes": {"tool_two_slow": {"my_key": " slow"}}, - }, - "next": ["finish"], - "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], - }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", - "name": "finish", - "input": {"my_key": "value prepared slow", "market": "DE"}, - "triggers": ["branch:prepare:condition::then"], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", - "name": "finish", - "result": [("my_key", " finished")], - "error": None, - "interrupts": [], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value prepared slow finished", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - "next": [], - "tasks": [], - }, - }, - ] - - tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"] + # test stream_mode=debug + tool_two = tool_two_graph.compile(checkpointer=checkpointer) + thread10 = {"configurable": {"thread_id": "10"}} + assert [ + *tool_two.stream( + {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" ) - - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - tool_two.invoke({"my_key": "value", "market": "DE"}) - - thread1 = {"configurable": {"thread_id": "1"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { - "my_key": "value prepared", - "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": -1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": {"my_key": ""}, + "metadata": { + "source": "input", + "step": -1, + "writes": {"my_key": "value", "market": "DE"}, + }, + "next": ["__start__"], + "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread1, debug=1) == { - "my_key": "value prepared slow finished", - "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value", + "market": "DE", + }, + "metadata": { + "source": "loop", + "step": 0, + "writes": None, + }, + "next": ["prepare"], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) - - thread2 = {"configurable": {"thread_id": "2"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value prepared", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value prepared", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], }, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread2, debug=1) == { - "my_key": "value prepared fast finished", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value prepared fast finished", "market": "US"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "name": "prepare", + "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], }, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared", + "market": "DE", + }, + "metadata": { + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + "next": ["tool_two_slow"], + "tasks": [{"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()}], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "name": "tool_two_slow", + "input": {"my_key": "value prepared", "market": "DE"}, + "triggers": ["branch:prepare:condition:tool_two_slow"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "name": "tool_two_slow", + "result": [("my_key", " slow")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared slow", + "market": "DE", + }, + "metadata": { + "source": "loop", + "step": 2, + "writes": {"tool_two_slow": {"my_key": " slow"}}, + }, + "next": ["finish"], + "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "name": "finish", + "input": {"my_key": "value prepared slow", "market": "DE"}, + "triggers": ["branch:prepare:condition::then"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "name": "finish", + "result": [("my_key", " finished")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared slow finished", + "market": "DE", + }, + "metadata": { + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + "next": [], + "tasks": [], + }, + }, + ] - with SqliteSaver.from_conn_string(":memory:") as saver: - tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_before=["finish"] - ) + tool_two = tool_two_graph.compile( + checkpointer=checkpointer, interrupt_before=["tool_two_fast", "tool_two_slow"] + ) - thread1 = {"configurable": {"thread_id": "1"}} + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + tool_two.invoke({"my_key": "value", "market": "DE"}) - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { + thread1 = {"configurable": {"thread_id": "1"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { + "my_key": "value prepared", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread1, debug=1) == { + "my_key": "value prepared slow finished", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + + thread2 = {"configurable": {"thread_id": "2"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value prepared", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value prepared", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread2, debug=1) == { + "my_key": "value prepared fast finished", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value prepared fast finished", "market": "US"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) + + tool_two = tool_two_graph.compile( + checkpointer=checkpointer, interrupt_before=["finish"] + ) + + thread1 = {"configurable": {"thread_id": "11"}} + + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { + "my_key": "value prepared slow", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={ "my_key": "value prepared slow", "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={ - "my_key": "value prepared slow", - "market": "DE", - }, - tasks=(PregelTask(AnyStr(), "finish"),), - next=("finish",), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 2, - "writes": {"tool_two_slow": {"my_key": " slow"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) + }, + tasks=(PregelTask(AnyStr(), "finish"),), + next=("finish",), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 2, + "writes": {"tool_two_slow": {"my_key": " slow"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) - # update state - tool_two.update_state(thread1, {"my_key": "er"}) - assert tool_two.get_state(thread1) == StateSnapshot( - values={ - "my_key": "value prepared slower", - "market": "DE", - }, - tasks=(PregelTask(AnyStr(), "finish"),), - next=("finish",), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "update", - "step": 3, - "writes": {"tool_two_slow": {"my_key": "er"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) - - with SqliteSaver.from_conn_string(":memory:") as saver: - tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_after=["prepare"] - ) - - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - tool_two.invoke({"my_key": "value", "market": "DE"}) - - thread1 = {"configurable": {"thread_id": "1"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { - "my_key": "value prepared", + # update state + tool_two.update_state(thread1, {"my_key": "er"}) + assert tool_two.get_state(thread1) == StateSnapshot( + values={ + "my_key": "value prepared slower", "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread1, debug=1) == { - "my_key": "value prepared slow finished", - "market": "DE", - } - assert tool_two.get_state(thread1) == StateSnapshot( - values={"my_key": "value prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread1).config, - created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, - ) + }, + tasks=(PregelTask(AnyStr(), "finish"),), + next=("finish",), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "source": "update", + "step": 3, + "writes": {"tool_two_slow": {"my_key": "er"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) - thread2 = {"configurable": {"thread_id": "2"}} - # stop when about to enter node - assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value prepared", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value prepared", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread2, debug=1) == { - "my_key": "value prepared fast finished", - "market": "US", - } - assert tool_two.get_state(thread2) == StateSnapshot( - values={"my_key": "value prepared fast finished", "market": "US"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread2).config, - created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, - ) + tool_two = tool_two_graph.compile( + checkpointer=checkpointer, interrupt_after=["prepare"] + ) - thread3 = {"configurable": {"thread_id": "3"}} - # update an empty thread before first run - uconfig = tool_two.update_state(thread3, {"my_key": "key", "market": "DE"}) - # check current state - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "key", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "prepare"),), - next=("prepare",), - config=uconfig, - created_at=AnyStr(), - metadata={ - "source": "update", - "step": 0, - "writes": {START: {"my_key": "key", "market": "DE"}}, - }, - parent_config=None, - ) - # run from this point - assert tool_two.invoke(None, thread3) == { - "my_key": "key prepared", - "market": "DE", - } - # get state after first node - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "key prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=tool_two.checkpointer.get_tuple(thread3).config, - created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=uconfig, - ) - # resume, for same result as above - assert tool_two.invoke(None, thread3, debug=1) == { - "my_key": "key prepared slow finished", - "market": "DE", - } - assert tool_two.get_state(thread3) == StateSnapshot( - values={"my_key": "key prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=tool_two.checkpointer.get_tuple(thread3).config, - created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, - ) + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + tool_two.invoke({"my_key": "value", "market": "DE"}) + + thread1 = {"configurable": {"thread_id": "21"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "DE"}, thread1) == { + "my_key": "value prepared", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread1, debug=1) == { + "my_key": "value prepared slow finished", + "market": "DE", + } + assert tool_two.get_state(thread1) == StateSnapshot( + values={"my_key": "value prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread1).config, + created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, + ) + + thread2 = {"configurable": {"thread_id": "22"}} + # stop when about to enter node + assert tool_two.invoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value prepared", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value prepared", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread2, debug=1) == { + "my_key": "value prepared fast finished", + "market": "US", + } + assert tool_two.get_state(thread2) == StateSnapshot( + values={"my_key": "value prepared fast finished", "market": "US"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread2).config, + created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, + ) + + thread3 = {"configurable": {"thread_id": "23"}} + # update an empty thread before first run + uconfig = tool_two.update_state(thread3, {"my_key": "key", "market": "DE"}) + # check current state + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "key", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "prepare"),), + next=("prepare",), + config=uconfig, + created_at=AnyStr(), + metadata={ + "source": "update", + "step": 0, + "writes": {START: {"my_key": "key", "market": "DE"}}, + }, + parent_config=None, + ) + # run from this point + assert tool_two.invoke(None, thread3) == { + "my_key": "key prepared", + "market": "DE", + } + # get state after first node + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "key prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=tool_two.checkpointer.get_tuple(thread3).config, + created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=uconfig, + ) + # resume, for same result as above + assert tool_two.invoke(None, thread3, debug=1) == { + "my_key": "key prepared slow finished", + "market": "DE", + } + assert tool_two.get_state(thread3) == StateSnapshot( + values={"my_key": "key prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=tool_two.checkpointer.get_tuple(thread3).config, + created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, + ) -def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_in_one_fan_out_state_graph_waiting_edge( + snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] ) -> list[str]: @@ -7168,7 +7250,7 @@ def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["retriever_one"], ) config = {"configurable": {"thread_id": "1"}} @@ -7187,10 +7269,10 @@ def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["qa"], ) - config = {"configurable": {"thread_id": "1"}} + config = {"configurable": {"thread_id": "2"}} assert [ c for c in app_w_interrupt.stream({"query": "what is weather in sf"}, config) @@ -7224,9 +7306,17 @@ def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> ] +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) def test_in_one_fan_out_state_graph_waiting_edge_via_branch( - snapshot: SnapshotAssertion, + snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str ) -> None: + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] ) -> list[str]: @@ -7294,7 +7384,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_via_branch( ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["retriever_one"], ) config = {"configurable": {"thread_id": "1"}} @@ -7313,11 +7403,19 @@ def test_in_one_fan_out_state_graph_waiting_edge_via_branch( ] +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1( - snapshot: SnapshotAssertion, mocker: MockerFixture + snapshot: SnapshotAssertion, + mocker: MockerFixture, + request: pytest.FixtureRequest, + checkpointer_name: str, ) -> None: from langchain_core.pydantic_v1 import BaseModel, ValidationError + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") setup = mocker.Mock() teardown = mocker.Mock() @@ -7442,7 +7540,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1( ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["retriever_one"], ) config = {"configurable": {"thread_id": "1"}} @@ -7477,11 +7575,19 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1( } +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2( - snapshot: SnapshotAssertion, mocker: MockerFixture + snapshot: SnapshotAssertion, + mocker: MockerFixture, + request: pytest.FixtureRequest, + checkpointer_name: str, ) -> None: from pydantic import BaseModel, ConfigDict, ValidationError + checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") setup = mocker.Mock() teardown = mocker.Mock() @@ -7604,7 +7710,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2( ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["retriever_one"], ) config = {"configurable": {"thread_id": "1"}} @@ -7639,7 +7745,17 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2( } -def test_in_one_fan_out_state_graph_waiting_edge_plus_regular() -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite", "postgres", "postgres_pipe"], +) +def test_in_one_fan_out_state_graph_waiting_edge_plus_regular( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) + def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] ) -> list[str]: @@ -7708,7 +7824,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_plus_regular() -> None: ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["retriever_one"], ) config = {"configurable": {"thread_id": "1"}} diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index d24a3d4f2..4eed26017 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -46,7 +46,6 @@ from langgraph.checkpoint.base import ( CheckpointTuple, ) from langgraph.checkpoint.memory import MemorySaver -from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver from langgraph.constants import ERROR, Interrupt, Send from langgraph.errors import InvalidUpdateError, NodeInterrupt from langgraph.graph import END, Graph, StateGraph @@ -1803,7 +1802,14 @@ async def test_cond_edge_after_send() -> None: assert await graph.ainvoke(["0"]) == ["0", "1", "2", "2", "3"] -async def test_invoke_checkpoint_aiosqlite(mocker: MockerFixture) -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], +) +async def test_invoke_checkpoint_three( + mocker: MockerFixture, request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) add_one = mocker.Mock(side_effect=lambda x: x["total"] + x["input"]) def raise_if_above_10(input: int) -> int: @@ -1818,121 +1824,118 @@ async def test_invoke_checkpoint_aiosqlite(mocker: MockerFixture) -> None: | raise_if_above_10 ) - async with AsyncSqliteSaver.from_conn_string(":memory:") as memory: - app = Pregel( - nodes={"one": one}, - channels={ - "total": BinaryOperatorAggregate(int, operator.add), - "input": LastValue(int), - "output": LastValue(int), - }, - input_channels="input", - output_channels="output", - checkpointer=memory, - debug=True, - ) + app = Pregel( + nodes={"one": one}, + channels={ + "total": BinaryOperatorAggregate(int, operator.add), + "input": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", + checkpointer=checkpointer, + debug=True, + ) - thread_1 = {"configurable": {"thread_id": "1"}} - # total starts out as 0, so output is 0+2=2 - assert await app.ainvoke(2, thread_1) == 2 - state = await app.aget_state(thread_1) - assert state is not None - assert state.values.get("total") == 2 - assert ( - state.config["configurable"]["checkpoint_id"] - == (await memory.aget(thread_1))["id"] - ) - # total is now 2, so output is 2+3=5 - assert await app.ainvoke(3, thread_1) == 5 - state = await app.aget_state(thread_1) - assert state is not None - assert state.values.get("total") == 7 - assert ( - state.config["configurable"]["checkpoint_id"] - == (await memory.aget(thread_1))["id"] - ) - # total is now 2+5=7, so output would be 7+4=11, but raises ValueError - with pytest.raises(ValueError): - await app.ainvoke(4, thread_1) - # checkpoint is not updated - state = await app.aget_state(thread_1) - assert state is not None - assert state.values.get("total") == 7 - assert state.next == ("one",) - """we checkpoint inputs and it failed on "one", so the next node is one""" - # we can recover from error by sending new inputs - assert await app.ainvoke(2, thread_1) == 9 - state = await app.aget_state(thread_1) - assert state is not None - assert state.values.get("total") == 16, "total is now 7+9=16" - assert state.next == () + thread_1 = {"configurable": {"thread_id": "1"}} + # total starts out as 0, so output is 0+2=2 + assert await app.ainvoke(2, thread_1) == 2 + state = await app.aget_state(thread_1) + assert state is not None + assert state.values.get("total") == 2 + assert ( + state.config["configurable"]["checkpoint_id"] + == (await checkpointer.aget(thread_1))["id"] + ) + # total is now 2, so output is 2+3=5 + assert await app.ainvoke(3, thread_1) == 5 + state = await app.aget_state(thread_1) + assert state is not None + assert state.values.get("total") == 7 + assert ( + state.config["configurable"]["checkpoint_id"] + == (await checkpointer.aget(thread_1))["id"] + ) + # total is now 2+5=7, so output would be 7+4=11, but raises ValueError + with pytest.raises(ValueError): + await app.ainvoke(4, thread_1) + # checkpoint is not updated + state = await app.aget_state(thread_1) + assert state is not None + assert state.values.get("total") == 7 + assert state.next == ("one",) + """we checkpoint inputs and it failed on "one", so the next node is one""" + # we can recover from error by sending new inputs + assert await app.ainvoke(2, thread_1) == 9 + state = await app.aget_state(thread_1) + assert state is not None + assert state.values.get("total") == 16, "total is now 7+9=16" + assert state.next == () - thread_2 = {"configurable": {"thread_id": "2"}} - # on a new thread, total starts out as 0, so output is 0+5=5 - assert await app.ainvoke(5, thread_2) == 5 - state = await app.aget_state({"configurable": {"thread_id": "1"}}) - assert state is not None - assert state.values.get("total") == 16 - assert state.next == () - state = await app.aget_state(thread_2) - assert state is not None - assert state.values.get("total") == 5 - assert state.next == () + thread_2 = {"configurable": {"thread_id": "2"}} + # on a new thread, total starts out as 0, so output is 0+5=5 + assert await app.ainvoke(5, thread_2) == 5 + state = await app.aget_state({"configurable": {"thread_id": "1"}}) + assert state is not None + assert state.values.get("total") == 16 + assert state.next == () + state = await app.aget_state(thread_2) + assert state is not None + assert state.values.get("total") == 5 + assert state.next == () - assert len([c async for c in app.aget_state_history(thread_1, limit=1)]) == 1 - # list all checkpoints for thread 1 - thread_1_history = [c async for c in app.aget_state_history(thread_1)] - # there are 7 checkpoints - assert len(thread_1_history) == 7 - assert Counter(c.metadata["source"] for c in thread_1_history) == { - "input": 4, - "loop": 3, - } - # sorted descending - assert ( - thread_1_history[0].config["configurable"]["checkpoint_id"] - > thread_1_history[1].config["configurable"]["checkpoint_id"] + assert len([c async for c in app.aget_state_history(thread_1, limit=1)]) == 1 + # list all checkpoints for thread 1 + thread_1_history = [c async for c in app.aget_state_history(thread_1)] + # there are 7 checkpoints + assert len(thread_1_history) == 7 + assert Counter(c.metadata["source"] for c in thread_1_history) == { + "input": 4, + "loop": 3, + } + # sorted descending + assert ( + thread_1_history[0].config["configurable"]["checkpoint_id"] + > thread_1_history[1].config["configurable"]["checkpoint_id"] + ) + # cursor pagination + cursored = [ + c + async for c in app.aget_state_history( + thread_1, limit=1, before=thread_1_history[0].config ) - # cursor pagination - cursored = [ - c - async for c in app.aget_state_history( - thread_1, limit=1, before=thread_1_history[0].config - ) - ] - assert len(cursored) == 1 - assert cursored[0].config == thread_1_history[1].config - # the last checkpoint - assert thread_1_history[0].values["total"] == 16 - # the first "loop" checkpoint - assert thread_1_history[-2].values["total"] == 2 - # can get each checkpoint using aget with config - assert (await memory.aget(thread_1_history[0].config))[ - "id" - ] == thread_1_history[0].config["configurable"]["checkpoint_id"] - assert (await memory.aget(thread_1_history[1].config))[ - "id" - ] == thread_1_history[1].config["configurable"]["checkpoint_id"] + ] + assert len(cursored) == 1 + assert cursored[0].config == thread_1_history[1].config + # the last checkpoint + assert thread_1_history[0].values["total"] == 16 + # the first "loop" checkpoint + assert thread_1_history[-2].values["total"] == 2 + # can get each checkpoint using aget with config + assert (await checkpointer.aget(thread_1_history[0].config))[ + "id" + ] == thread_1_history[0].config["configurable"]["checkpoint_id"] + assert (await checkpointer.aget(thread_1_history[1].config))[ + "id" + ] == thread_1_history[1].config["configurable"]["checkpoint_id"] - thread_1_next_config = await app.aupdate_state(thread_1_history[1].config, 10) - # update creates a new checkpoint - assert ( - thread_1_next_config["configurable"]["checkpoint_id"] - > thread_1_history[0].config["configurable"]["checkpoint_id"] - ) - # 1 more checkpoint in history - assert len([c async for c in app.aget_state_history(thread_1)]) == 8 - assert Counter( - [c.metadata["source"] async for c in app.aget_state_history(thread_1)] - ) == { - "update": 1, - "input": 4, - "loop": 3, - } - # the latest checkpoint is the updated one - assert await app.aget_state(thread_1) == await app.aget_state( - thread_1_next_config - ) + thread_1_next_config = await app.aupdate_state(thread_1_history[1].config, 10) + # update creates a new checkpoint + assert ( + thread_1_next_config["configurable"]["checkpoint_id"] + > thread_1_history[0].config["configurable"]["checkpoint_id"] + ) + # 1 more checkpoint in history + assert len([c async for c in app.aget_state_history(thread_1)]) == 8 + assert Counter( + [c.metadata["source"] async for c in app.aget_state_history(thread_1)] + ) == { + "update": 1, + "input": 4, + "loop": 3, + } + # the latest checkpoint is the updated one + assert await app.aget_state(thread_1) == await app.aget_state(thread_1_next_config) async def test_invoke_two_processes_two_in_join_two_out(mocker: MockerFixture) -> None: @@ -2144,7 +2147,13 @@ async def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: assert cleanup_async.call_count == 1, "Expected cleanup to be called once" -async def test_conditional_graph() -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], +) +async def test_conditional_graph( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: from copy import deepcopy from langchain_core.agents import AgentAction, AgentFinish @@ -2153,6 +2162,8 @@ async def test_conditional_graph() -> None: from langchain_core.runnables import RunnablePassthrough from langchain_core.tools import tool + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + # Assemble the tools @tool() def search_api(query: str) -> str: @@ -2190,7 +2201,7 @@ async def test_conditional_graph() -> None: ) if data.get("intermediate_steps") is None: data["intermediate_steps"] = [] - data["intermediate_steps"].append((agent_action, observation)) + data["intermediate_steps"].append([agent_action, observation]) return data # Define decision-making logic @@ -2220,22 +2231,22 @@ async def test_conditional_graph() -> None: assert await app.ainvoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2258,14 +2269,14 @@ async def test_conditional_graph() -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -2273,14 +2284,14 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2293,22 +2304,22 @@ async def test_conditional_graph() -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -2316,22 +2327,22 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2368,14 +2379,14 @@ async def test_conditional_graph() -> None: { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2386,22 +2397,22 @@ async def test_conditional_graph() -> None: { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -2412,7 +2423,7 @@ async def test_conditional_graph() -> None: # test state get/update methods with interrupt_after app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -2522,14 +2533,14 @@ async def test_conditional_graph() -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -2537,14 +2548,14 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2560,14 +2571,14 @@ async def test_conditional_graph() -> None: { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2581,14 +2592,14 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2609,14 +2620,14 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2633,7 +2644,7 @@ async def test_conditional_graph() -> None: # test state get/update methods with interrupt_before app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], ) config = {"configurable": {"thread_id": "2"}} @@ -2744,14 +2755,14 @@ async def test_conditional_graph() -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -2759,14 +2770,14 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2782,14 +2793,14 @@ async def test_conditional_graph() -> None: { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2803,14 +2814,14 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2831,14 +2842,14 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], "agent_outcome": AgentFinish( return_values={"answer": "a really nice answer"}, @@ -2855,10 +2866,10 @@ async def test_conditional_graph() -> None: # test re-invoke to continue with interrupt_before app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_before=["tools"], ) - config = {"configurable": {"thread_id": "2"}} + config = {"configurable": {"thread_id": "3"}} llm.i = 0 # reset the llm assert [ @@ -2918,14 +2929,14 @@ async def test_conditional_graph() -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -2933,14 +2944,14 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], "agent_outcome": AgentAction( tool="search_api", @@ -2956,22 +2967,22 @@ async def test_conditional_graph() -> None: "tools": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -2979,22 +2990,22 @@ async def test_conditional_graph() -> None: "agent": { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -5005,7 +5016,15 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: ] -async def test_start_branch_then() -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], +) +async def test_start_branch_then( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + class State(TypedDict): my_key: Annotated[str, operator.add] market: str @@ -5050,176 +5069,169 @@ async def test_start_branch_then() -> None: "market": "US", } - async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: - tool_two = tool_two_graph.compile( - store=MemoryStore(), - checkpointer=saver, - interrupt_before=["tool_two_fast", "tool_two_slow"], - ) + tool_two = tool_two_graph.compile( + store=MemoryStore(), + checkpointer=checkpointer, + interrupt_before=["tool_two_fast", "tool_two_slow"], + ) - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - await tool_two.ainvoke({"my_key": "value", "market": "DE"}) + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1", "assistant_id": "a"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { - "my_key": "value", - "market": "DE", - } - assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ - { - "source": "loop", - "step": 0, - "writes": None, - }, - { - "source": "input", - "step": -1, - "writes": {"my_key": "value", "market": "DE"}, - }, - ] - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ - "ts" - ], - metadata={"source": "loop", "step": 0, "writes": None}, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread1, limit=2) - ][-1].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread1, debug=1) == { - "my_key": "value slow", - "market": "DE", - } - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value slow", "market": "DE"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 1, - "writes": {"tool_two_slow": {"my_key": " slow"}}, - }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread1, limit=2) - ][-1].config, - ) + thread1 = {"configurable": {"thread_id": "1", "assistant_id": "a"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { + "my_key": "value", + "market": "DE", + } + assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ + { + "source": "loop", + "step": 0, + "writes": None, + }, + { + "source": "input", + "step": -1, + "writes": {"my_key": "value", "market": "DE"}, + }, + ] + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], + metadata={"source": "loop", "step": 0, "writes": None}, + parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ + -1 + ].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread1, debug=1) == { + "my_key": "value slow", + "market": "DE", + } + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value slow", "market": "DE"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"tool_two_slow": {"my_key": " slow"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ + -1 + ].config, + ) - thread2 = {"configurable": {"thread_id": "2", "assistant_id": "a"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ - "ts" - ], - metadata={"source": "loop", "step": 0, "writes": None}, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread2, limit=2) - ][-1].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread2, debug=1) == { - "my_key": "value fast", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value fast", "market": "US"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 1, - "writes": {"tool_two_fast": {"my_key": " fast"}}, - }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread2, limit=2) - ][-1].config, - ) + thread2 = {"configurable": {"thread_id": "2", "assistant_id": "a"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], + metadata={"source": "loop", "step": 0, "writes": None}, + parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ + -1 + ].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread2, debug=1) == { + "my_key": "value fast", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value fast", "market": "US"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"tool_two_fast": {"my_key": " fast"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ + -1 + ].config, + ) - thread3 = {"configurable": {"thread_id": "3", "assistant_id": "b"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread3) == { - "my_key": "value", - "market": "US", - } - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=(await tool_two.checkpointer.aget_tuple(thread3)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ - "ts" - ], - metadata={"source": "loop", "step": 0, "writes": None}, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread3, limit=2) - ][-1].config, - ) - # update state - await tool_two.aupdate_state(thread3, {"my_key": "key"}) # appends to my_key - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "valuekey", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=(await tool_two.checkpointer.aget_tuple(thread3)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ - "ts" - ], - metadata={ - "source": "update", - "step": 1, - "writes": {START: {"my_key": "key"}}, - }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread3, limit=2) - ][-1].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread3, debug=1) == { - "my_key": "valuekey fast", - "market": "US", - } - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "valuekey fast", "market": "US"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread3)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 2, - "writes": {"tool_two_fast": {"my_key": " fast"}}, - }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread3, limit=2) - ][-1].config, - ) + thread3 = {"configurable": {"thread_id": "3", "assistant_id": "b"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread3) == { + "my_key": "value", + "market": "US", + } + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "value", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=(await tool_two.checkpointer.aget_tuple(thread3)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], + metadata={"source": "loop", "step": 0, "writes": None}, + parent_config=[c async for c in tool_two.checkpointer.alist(thread3, limit=2)][ + -1 + ].config, + ) + # update state + await tool_two.aupdate_state(thread3, {"my_key": "key"}) # appends to my_key + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "valuekey", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=(await tool_two.checkpointer.aget_tuple(thread3)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], + metadata={ + "source": "update", + "step": 1, + "writes": {START: {"my_key": "key"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread3, limit=2)][ + -1 + ].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread3, debug=1) == { + "my_key": "valuekey fast", + "market": "US", + } + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "valuekey fast", "market": "US"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread3)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 2, + "writes": {"tool_two_fast": {"my_key": " fast"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread3, limit=2)][ + -1 + ].config, + ) -async def test_branch_then() -> None: +@pytest.mark.parametrize( + "checkpointer_name", + ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], +) +async def test_branch_then( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + class State(TypedDict): my_key: Annotated[str, operator.add] market: str @@ -5247,606 +5259,578 @@ async def test_branch_then() -> None: "market": "US", } - async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: - # test stream_mode=debug - tool_two = tool_two_graph.compile(checkpointer=saver) - thread10 = {"configurable": {"thread_id": "10"}} - assert [ - c - async for c in tool_two.astream( - {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" - ) - ] == [ - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": -1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, + # test stream_mode=debug + tool_two = tool_two_graph.compile(checkpointer=checkpointer) + thread10 = {"configurable": {"thread_id": "10"}} + assert [ + c + async for c in tool_two.astream( + {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" + ) + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": -1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), }, - "values": {"my_key": ""}, - "metadata": { - "source": "input", - "step": -1, - "writes": {"my_key": "value", "market": "DE"}, - }, - "next": ["__start__"], - "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 0, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 0, - "writes": None, - }, - "next": ["prepare"], - "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], + "values": {"my_key": ""}, + "metadata": { + "source": "input", + "step": -1, + "writes": {"my_key": "value", "market": "DE"}, }, + "next": ["__start__"], + "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", - "name": "prepare", - "input": {"my_key": "value", "market": "DE"}, - "triggers": ["start:prepare"], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", - "name": "prepare", - "result": [("my_key", " prepared")], - "error": None, - "interrupts": [], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), }, - "values": { - "my_key": "value prepared", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - "next": ["tool_two_slow"], - "tasks": [ - {"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()} - ], }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", - "name": "tool_two_slow", - "input": {"my_key": "value prepared", "market": "DE"}, - "triggers": ["branch:prepare:condition:tool_two_slow"], + "values": { + "my_key": "value", + "market": "DE", }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", - "name": "tool_two_slow", - "result": [("my_key", " slow")], - "error": None, - "interrupts": [], + "metadata": { + "source": "loop", + "step": 0, + "writes": None, }, + "next": ["prepare"], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "name": "prepare", + "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), }, - "values": { - "my_key": "value prepared slow", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 2, - "writes": {"tool_two_slow": {"my_key": " slow"}}, - }, - "next": ["finish"], - "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", - "name": "finish", - "input": {"my_key": "value prepared slow", "market": "DE"}, - "triggers": ["branch:prepare:condition::then"], + "values": { + "my_key": "value prepared", + "market": "DE", }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", - "name": "finish", - "result": [("my_key", " finished")], - "error": None, - "interrupts": [], + "metadata": { + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, }, + "next": ["tool_two_slow"], + "tasks": [{"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()}], }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "name": "tool_two_slow", + "input": {"my_key": "value prepared", "market": "DE"}, + "triggers": ["branch:prepare:condition:tool_two_slow"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "name": "tool_two_slow", + "result": [("my_key", " slow")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), }, - "values": { - "my_key": "value prepared slow finished", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - "next": [], - "tasks": [], }, + "values": { + "my_key": "value prepared slow", + "market": "DE", + }, + "metadata": { + "source": "loop", + "step": 2, + "writes": {"tool_two_slow": {"my_key": " slow"}}, + }, + "next": ["finish"], + "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], }, - ] + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "name": "finish", + "input": {"my_key": "value prepared slow", "market": "DE"}, + "triggers": ["branch:prepare:condition::then"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "name": "finish", + "result": [("my_key", " finished")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared slow finished", + "market": "DE", + }, + "metadata": { + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + "next": [], + "tasks": [], + }, + }, + ] - tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"] + tool_two = tool_two_graph.compile( + checkpointer=checkpointer, interrupt_before=["tool_two_fast", "tool_two_slow"] + ) + + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + await tool_two.ainvoke({"my_key": "value", "market": "DE"}) + + thread1 = {"configurable": {"thread_id": "11"}} + # stop when about to enter node + assert [ + c + async for c in tool_two.astream( + {"my_key": "value", "market": "DE"}, thread1, stream_mode="debug" ) - - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - - thread1 = {"configurable": {"thread_id": "1"}} - # stop when about to enter node - assert [ - c - async for c in tool_two.astream( - {"my_key": "value", "market": "DE"}, thread1, stream_mode="debug" - ) - ] == [ - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": -1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "1"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": -1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "11"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "11", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), }, - "values": {"my_key": ""}, - "metadata": { - "source": "input", - "step": -1, - "writes": {"my_key": "value", "market": "DE"}, - }, - "next": ["__start__"], - "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 0, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "1"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 0, - "writes": None, - }, - "next": ["prepare"], - "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], + "values": {"my_key": ""}, + "metadata": { + "source": "input", + "step": -1, + "writes": {"my_key": "value", "market": "DE"}, }, + "next": ["__start__"], + "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "ca572c3b-b805-5fc6-a19e-3d79f52dde70", - "name": "prepare", - "input": {"my_key": "value", "market": "DE"}, - "triggers": ["start:prepare"], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "ca572c3b-b805-5fc6-a19e-3d79f52dde70", - "name": "prepare", - "result": [("my_key", " prepared")], - "error": None, - "interrupts": [], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "1"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "11"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "11", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), }, - "values": { - "my_key": "value prepared", - "market": "DE", - }, - "metadata": { - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - "next": ["tool_two_slow"], - "tasks": [ - {"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()} - ], }, + "values": { + "my_key": "value", + "market": "DE", + }, + "metadata": { + "source": "loop", + "step": 0, + "writes": None, + }, + "next": ["prepare"], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], }, - ] - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "1a591be4-f85c-558f-8d00-1ccac0d1877f", + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread1, limit=2) - ][-1].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread1, debug=1) == { - "my_key": "value prepared slow finished", - "market": "DE", - } - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "1a591be4-f85c-558f-8d00-1ccac0d1877f", + "name": "prepare", + "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread1, limit=2) - ][-1].config, - ) + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "11"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "11", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared", + "market": "DE", + }, + "metadata": { + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + "next": ["tool_two_slow"], + "tasks": [{"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()}], + }, + }, + ] + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ + -1 + ].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread1, debug=1) == { + "my_key": "value prepared slow finished", + "market": "DE", + } + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ + -1 + ].config, + ) - thread2 = {"configurable": {"thread_id": "2"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value prepared", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value prepared", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread2, limit=2) - ][-1].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread2, debug=1) == { - "my_key": "value prepared fast finished", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value prepared fast finished", "market": "US"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread2, limit=2) - ][-1].config, - ) + thread2 = {"configurable": {"thread_id": "12"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value prepared", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value prepared", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ + -1 + ].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread2, debug=1) == { + "my_key": "value prepared fast finished", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value prepared fast finished", "market": "US"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ + -1 + ].config, + ) - async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: - tool_two = tool_two_graph.compile( - checkpointer=saver, interrupt_after=["prepare"] - ) + tool_two = tool_two_graph.compile( + checkpointer=checkpointer, interrupt_after=["prepare"] + ) - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - await tool_two.ainvoke({"my_key": "value", "market": "DE"}) + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { - "my_key": "value prepared", - "market": "DE", - } - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread1, limit=2) - ][-1].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread1, debug=1) == { - "my_key": "value prepared slow finished", - "market": "DE", - } - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread1, limit=2) - ][-1].config, - ) + thread1 = {"configurable": {"thread_id": "21"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { + "my_key": "value prepared", + "market": "DE", + } + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ + -1 + ].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread1, debug=1) == { + "my_key": "value prepared slow finished", + "market": "DE", + } + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ + -1 + ].config, + ) - thread2 = {"configurable": {"thread_id": "2"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value prepared", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value prepared", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread2, limit=2) - ][-1].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread2, debug=1) == { - "my_key": "value prepared fast finished", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value prepared fast finished", "market": "US"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread2, limit=2) - ][-1].config, - ) + thread2 = {"configurable": {"thread_id": "22"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value prepared", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value prepared", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ + -1 + ].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread2, debug=1) == { + "my_key": "value prepared fast finished", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value prepared fast finished", "market": "US"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ + -1 + ].config, + ) - thread3 = {"configurable": {"thread_id": "3"}} - # update an empty thread before first run - uconfig = await tool_two.aupdate_state( - thread3, {"my_key": "key", "market": "DE"} - ) - # check current state - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "key", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "prepare"),), - next=("prepare",), - config=uconfig, - created_at=AnyStr(), - metadata={ - "source": "update", - "step": 0, - "writes": {START: {"my_key": "key", "market": "DE"}}, - }, - parent_config=None, - ) - # run from this point - assert await tool_two.ainvoke(None, thread3) == { - "my_key": "key prepared", - "market": "DE", - } - # get state after first node - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "key prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=(await tool_two.checkpointer.aget_tuple(thread3)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=uconfig, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread3, debug=1) == { - "my_key": "key prepared slow finished", - "market": "DE", - } - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "key prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread3)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ - "ts" - ], - metadata={ - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[ - c async for c in tool_two.checkpointer.alist(thread3, limit=2) - ][-1].config, - ) + thread3 = {"configurable": {"thread_id": "23"}} + # update an empty thread before first run + uconfig = await tool_two.aupdate_state(thread3, {"my_key": "key", "market": "DE"}) + # check current state + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "key", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "prepare"),), + next=("prepare",), + config=uconfig, + created_at=AnyStr(), + metadata={ + "source": "update", + "step": 0, + "writes": {START: {"my_key": "key", "market": "DE"}}, + }, + parent_config=None, + ) + # run from this point + assert await tool_two.ainvoke(None, thread3) == { + "my_key": "key prepared", + "market": "DE", + } + # get state after first node + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "key prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=(await tool_two.checkpointer.aget_tuple(thread3)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=uconfig, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread3, debug=1) == { + "my_key": "key prepared slow finished", + "market": "DE", + } + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "key prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread3)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], + metadata={ + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[c async for c in tool_two.checkpointer.alist(thread3, limit=2)][ + -1 + ].config, + ) async def test_in_one_fan_out_state_graph_waiting_edge() -> None: From 4935cf52bf841e216511cb631365d872910b1511 Mon Sep 17 00:00:00 2001 From: vbarda Date: Thu, 22 Aug 2024 16:52:16 -0400 Subject: [PATCH 083/287] lint --- libs/langgraph/langgraph/pregel/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 0096f7f8b..5ee96dde5 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -67,7 +67,6 @@ from langgraph.constants import ( ERROR, INTERRUPT, SEND_CHECKPOINT_NAMESPACE_SEPARATOR, - Interrupt, ) from langgraph.errors import GraphInterrupt, GraphRecursionError, InvalidUpdateError from langgraph.managed.base import ManagedValueSpec From 70746042049e684d751e8f150803589a69306a74 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 14:23:48 -0700 Subject: [PATCH 084/287] Try to improve async stack traces for exceptions in tasks (#1442) * Try to improve async stack traces for exceptions in tasks * Lint --- libs/langgraph/langgraph/pregel/executor.py | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/executor.py b/libs/langgraph/langgraph/pregel/executor.py index 5da78a26c..3e4243c61 100644 --- a/libs/langgraph/langgraph/pregel/executor.py +++ b/libs/langgraph/langgraph/pregel/executor.py @@ -120,14 +120,14 @@ class AsyncBackgroundExecutor(AsyncContextManager): def done(self, task: asyncio.Task) -> None: try: - task.result() - except GraphInterrupt: - # This exception is an interruption signal, not an error - # so we don't want to re-raise it on exit - self.tasks.pop(task) - except BaseException: - pass - else: + if exc := task.exception(): + # This exception is an interruption signal, not an error + # so we don't want to re-raise it on exit + if isinstance(exc, GraphInterrupt): + self.tasks.pop(task) + else: + self.tasks.pop(task) + except asyncio.CancelledError: self.tasks.pop(task) async def __aenter__(self) -> Submit: @@ -146,12 +146,13 @@ class AsyncBackgroundExecutor(AsyncContextManager): # wait for all tasks to finish if self.tasks: await asyncio.wait(self.tasks) - # re-raise the first exception that occurred in a task + # if there's already an exception being raised, don't raise another one if exc_type is None: - # if there's already an exception being raised, don't raise another one + # re-raise the first exception that occurred in a task for task in self.tasks: try: - task.result() + if exc := task.exception(): + raise exc except asyncio.CancelledError: pass From 72893d9abb9a813bd882d84f9f64cbe89227a938 Mon Sep 17 00:00:00 2001 From: vbarda Date: Thu, 22 Aug 2024 17:33:17 -0400 Subject: [PATCH 085/287] code review --- libs/langgraph/langgraph/pregel/__init__.py | 57 ++++----------------- libs/langgraph/langgraph/pregel/types.py | 2 +- libs/langgraph/langgraph/pregel/utils.py | 36 +++++++++++++ 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 5ee96dde5..7f15d01a4 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -88,7 +88,10 @@ from langgraph.pregel.types import ( StateSnapshot, StreamMode, ) -from langgraph.pregel.utils import get_new_channel_versions +from langgraph.pregel.utils import ( + assemble_state_snapshot_hierarchy, + get_new_channel_versions, +) from langgraph.pregel.validate import validate_graph, validate_keys from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry from langgraph.store.base import BaseStore @@ -205,40 +208,6 @@ def _get_subgraph(graph: Pregel, checkpoint_ns: str) -> Pregel: return subgraph_node.bound -def _assemble_state_snapshot_hierarchy( - root_checkpoint_ns: str, - checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot], -) -> StateSnapshot: - checkpoint_ns_list_to_visit = sorted( - checkpoint_ns_to_state_snapshots.keys(), - key=lambda x: len(x.split(CHECKPOINT_NAMESPACE_SEPARATOR)), - ) - while checkpoint_ns_list_to_visit: - checkpoint_ns = checkpoint_ns_list_to_visit.pop() - state_snapshot = checkpoint_ns_to_state_snapshots[checkpoint_ns] - *path, subgraph_node = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) - parent_checkpoint_ns = CHECKPOINT_NAMESPACE_SEPARATOR.join(path) - if subgraph_node and ( - parent_state_snapshot := checkpoint_ns_to_state_snapshots.get( - parent_checkpoint_ns - ) - ): - parent_subgraph_snapshots = { - **(parent_state_snapshot.subgraph_state_snapshots or {}), - subgraph_node: state_snapshot, - } - checkpoint_ns_to_state_snapshots[ - parent_checkpoint_ns - ] = checkpoint_ns_to_state_snapshots[parent_checkpoint_ns]._replace( - subgraph_state_snapshots=parent_subgraph_snapshots - ) - - state_snapshot = checkpoint_ns_to_state_snapshots.pop(root_checkpoint_ns, None) - if state_snapshot is None: - raise ValueError(f"Missing checkpoint for checkpoint NS '{root_checkpoint_ns}'") - return state_snapshot - - def _has_nested_interrupts( graph: Pregel, ) -> bool: @@ -497,7 +466,7 @@ class Pregel( tasks=(), ) - state_snapshot = _assemble_state_snapshot_hierarchy( + state_snapshot = assemble_state_snapshot_hierarchy( checkpoint_ns, checkpoint_ns_to_state_snapshots ) return state_snapshot @@ -573,7 +542,7 @@ class Pregel( tasks=(), ) - state_snapshot = _assemble_state_snapshot_hierarchy( + state_snapshot = assemble_state_snapshot_hierarchy( checkpoint_ns, checkpoint_ns_to_state_snapshots ) return state_snapshot @@ -1113,7 +1082,8 @@ class Pregel( ) if not done: break # timed out - for fut, task in zip(done, [futures.pop(fut) for fut in done]): + for fut in done: + task = futures.pop(fut) if exc := _exception(fut): # save error to checkpointer if isinstance(exc, GraphInterrupt): @@ -1364,7 +1334,8 @@ class Pregel( if not done: break # timed out - for fut, task in zip(done, [futures.pop(fut) for fut in done]): + for fut in done: + task = futures.pop(fut) if exc := _exception(fut): # save error to checkpointer if isinstance(exc, GraphInterrupt): @@ -1581,11 +1552,3 @@ def _panic_or_proceed( inflight.pop().cancel() # raise timeout error raise timeout_exc_cls(f"Timed out at step {step}") - - -def _with_mode(mode: StreamMode, on: bool, iter: Iterator[Any]) -> Iterator[Any]: - if on: - for chunk in iter: - yield (mode, chunk) - else: - yield from iter diff --git a/libs/langgraph/langgraph/pregel/types.py b/libs/langgraph/langgraph/pregel/types.py index a8d66cb20..d4881452b 100644 --- a/libs/langgraph/langgraph/pregel/types.py +++ b/libs/langgraph/langgraph/pregel/types.py @@ -93,7 +93,7 @@ class StateSnapshot(NamedTuple): tasks: tuple[PregelTask, ...] """Tasks to execute in this step. If already attempted, may contain an error.""" subgraph_state_snapshots: Optional[dict[str, "StateSnapshot"]] = None - """State snapshots of subgraphs represented as a mapping from thread ID suffix to snapshot.""" + """State snapshots of subgraphs represented as a mapping from checkpoint namespace (`checkpoint_ns`) to snapshot.""" All = Literal["*"] diff --git a/libs/langgraph/langgraph/pregel/utils.py b/libs/langgraph/langgraph/pregel/utils.py index d3d0d989f..9c76e5f7e 100644 --- a/libs/langgraph/langgraph/pregel/utils.py +++ b/libs/langgraph/langgraph/pregel/utils.py @@ -1,4 +1,6 @@ from langgraph.checkpoint.base import ChannelVersions +from langgraph.constants import CHECKPOINT_NAMESPACE_SEPARATOR +from langgraph.pregel.types import StateSnapshot def get_new_channel_versions( @@ -17,3 +19,37 @@ def get_new_channel_versions( new_versions = current_versions return new_versions + + +def assemble_state_snapshot_hierarchy( + root_checkpoint_ns: str, + checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot], +) -> StateSnapshot: + checkpoint_ns_list_to_visit = sorted( + checkpoint_ns_to_state_snapshots.keys(), + key=lambda x: len(x.split(CHECKPOINT_NAMESPACE_SEPARATOR)), + ) + while checkpoint_ns_list_to_visit: + checkpoint_ns = checkpoint_ns_list_to_visit.pop() + state_snapshot = checkpoint_ns_to_state_snapshots[checkpoint_ns] + *path, subgraph_node = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) + parent_checkpoint_ns = CHECKPOINT_NAMESPACE_SEPARATOR.join(path) + if subgraph_node and ( + parent_state_snapshot := checkpoint_ns_to_state_snapshots.get( + parent_checkpoint_ns + ) + ): + parent_subgraph_snapshots = { + **(parent_state_snapshot.subgraph_state_snapshots or {}), + subgraph_node: state_snapshot, + } + checkpoint_ns_to_state_snapshots[ + parent_checkpoint_ns + ] = checkpoint_ns_to_state_snapshots[parent_checkpoint_ns]._replace( + subgraph_state_snapshots=parent_subgraph_snapshots + ) + + state_snapshot = checkpoint_ns_to_state_snapshots.pop(root_checkpoint_ns, None) + if state_snapshot is None: + raise ValueError(f"Missing checkpoint for checkpoint NS '{root_checkpoint_ns}'") + return state_snapshot From 9f6e57d2a7e7c8ff506ecf9f296002e75cfc785a Mon Sep 17 00:00:00 2001 From: vbarda Date: Thu, 22 Aug 2024 17:36:24 -0400 Subject: [PATCH 086/287] more code review --- libs/langgraph/langgraph/pregel/__init__.py | 2 +- libs/langgraph/langgraph/pregel/algo.py | 19 ++--------- libs/langgraph/langgraph/pregel/get_state.py | 36 ++++++++++++++++++++ libs/langgraph/langgraph/pregel/utils.py | 36 -------------------- 4 files changed, 39 insertions(+), 54 deletions(-) create mode 100644 libs/langgraph/langgraph/pregel/get_state.py diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 7f15d01a4..477f5a6d1 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -77,6 +77,7 @@ from langgraph.pregel.debug import ( print_step_writes, tasks_w_writes, ) +from langgraph.pregel.get_state import assemble_state_snapshot_hierarchy from langgraph.pregel.io import read_channels from langgraph.pregel.loop import AsyncPregelLoop, SyncPregelLoop from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager @@ -89,7 +90,6 @@ from langgraph.pregel.types import ( StreamMode, ) from langgraph.pregel.utils import ( - assemble_state_snapshot_hierarchy, get_new_channel_versions, ) from langgraph.pregel.validate import validate_graph, validate_keys diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 27777bbab..e3e699518 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -283,9 +283,9 @@ def prepare_next_tasks( "langgraph_task_idx": len(tasks), } checkpoint_ns = ( - f"{parent_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{packet.node}" + f"{parent_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{packet.node}:{packet.id}" if parent_ns - else packet.node + else f"{packet.node}:{packet.id}" ) task_id = str( uuid5(UUID(checkpoint["id"]), json.dumps((checkpoint_ns, metadata))) @@ -293,21 +293,6 @@ def prepare_next_tasks( if for_execution: proc = processes[packet.node] if node := proc.get_node(): - triggers = [TASKS] - metadata = { - "langgraph_step": step, - "langgraph_node": packet.node, - "langgraph_triggers": triggers, - "langgraph_task_idx": len(tasks), - } - checkpoint_ns = ( - f"{parent_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{packet.node}:{packet.id}" - if parent_ns - else f"{packet.node}:{packet.id}" - ) - task_id = str( - uuid5(UUID(checkpoint["id"]), json.dumps((checkpoint_ns, metadata))) - ) writes = deque() tasks.append( PregelExecutableTask( diff --git a/libs/langgraph/langgraph/pregel/get_state.py b/libs/langgraph/langgraph/pregel/get_state.py new file mode 100644 index 000000000..b8efbe6a2 --- /dev/null +++ b/libs/langgraph/langgraph/pregel/get_state.py @@ -0,0 +1,36 @@ +from langgraph.constants import CHECKPOINT_NAMESPACE_SEPARATOR +from langgraph.pregel.types import StateSnapshot + + +def assemble_state_snapshot_hierarchy( + root_checkpoint_ns: str, + checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot], +) -> StateSnapshot: + checkpoint_ns_list_to_visit = sorted( + checkpoint_ns_to_state_snapshots.keys(), + key=lambda x: len(x.split(CHECKPOINT_NAMESPACE_SEPARATOR)), + ) + while checkpoint_ns_list_to_visit: + checkpoint_ns = checkpoint_ns_list_to_visit.pop() + state_snapshot = checkpoint_ns_to_state_snapshots[checkpoint_ns] + *path, subgraph_node = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) + parent_checkpoint_ns = CHECKPOINT_NAMESPACE_SEPARATOR.join(path) + if subgraph_node and ( + parent_state_snapshot := checkpoint_ns_to_state_snapshots.get( + parent_checkpoint_ns + ) + ): + parent_subgraph_snapshots = { + **(parent_state_snapshot.subgraph_state_snapshots or {}), + subgraph_node: state_snapshot, + } + checkpoint_ns_to_state_snapshots[ + parent_checkpoint_ns + ] = checkpoint_ns_to_state_snapshots[parent_checkpoint_ns]._replace( + subgraph_state_snapshots=parent_subgraph_snapshots + ) + + state_snapshot = checkpoint_ns_to_state_snapshots.pop(root_checkpoint_ns, None) + if state_snapshot is None: + raise ValueError(f"Missing checkpoint for checkpoint NS '{root_checkpoint_ns}'") + return state_snapshot diff --git a/libs/langgraph/langgraph/pregel/utils.py b/libs/langgraph/langgraph/pregel/utils.py index 9c76e5f7e..d3d0d989f 100644 --- a/libs/langgraph/langgraph/pregel/utils.py +++ b/libs/langgraph/langgraph/pregel/utils.py @@ -1,6 +1,4 @@ from langgraph.checkpoint.base import ChannelVersions -from langgraph.constants import CHECKPOINT_NAMESPACE_SEPARATOR -from langgraph.pregel.types import StateSnapshot def get_new_channel_versions( @@ -19,37 +17,3 @@ def get_new_channel_versions( new_versions = current_versions return new_versions - - -def assemble_state_snapshot_hierarchy( - root_checkpoint_ns: str, - checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot], -) -> StateSnapshot: - checkpoint_ns_list_to_visit = sorted( - checkpoint_ns_to_state_snapshots.keys(), - key=lambda x: len(x.split(CHECKPOINT_NAMESPACE_SEPARATOR)), - ) - while checkpoint_ns_list_to_visit: - checkpoint_ns = checkpoint_ns_list_to_visit.pop() - state_snapshot = checkpoint_ns_to_state_snapshots[checkpoint_ns] - *path, subgraph_node = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) - parent_checkpoint_ns = CHECKPOINT_NAMESPACE_SEPARATOR.join(path) - if subgraph_node and ( - parent_state_snapshot := checkpoint_ns_to_state_snapshots.get( - parent_checkpoint_ns - ) - ): - parent_subgraph_snapshots = { - **(parent_state_snapshot.subgraph_state_snapshots or {}), - subgraph_node: state_snapshot, - } - checkpoint_ns_to_state_snapshots[ - parent_checkpoint_ns - ] = checkpoint_ns_to_state_snapshots[parent_checkpoint_ns]._replace( - subgraph_state_snapshots=parent_subgraph_snapshots - ) - - state_snapshot = checkpoint_ns_to_state_snapshots.pop(root_checkpoint_ns, None) - if state_snapshot is None: - raise ValueError(f"Missing checkpoint for checkpoint NS '{root_checkpoint_ns}'") - return state_snapshot From 8090ca67c5c78c50d1c5a620df57cd866dc1da29 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Thu, 22 Aug 2024 17:39:21 -0400 Subject: [PATCH 087/287] checkpoint-postgres: pass row_factory in cursor (#1433) --- examples/persistence_postgres.ipynb | 7 +- .../langgraph/checkpoint/postgres/__init__.py | 62 +++++++++-------- .../langgraph/checkpoint/postgres/aio.py | 66 ++++++++++--------- 3 files changed, 71 insertions(+), 64 deletions(-) diff --git a/examples/persistence_postgres.ipynb b/examples/persistence_postgres.ipynb index ef8be17cb..f43fa01d6 100644 --- a/examples/persistence_postgres.ipynb +++ b/examples/persistence_postgres.ipynb @@ -122,7 +122,7 @@ "metadata": {}, "outputs": [], "source": [ - "DB_URI = \"postgresql://postgres:postgres@localhost:5441/postgres?sslmode=disable\"" + "DB_URI = \"postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable\"" ] }, { @@ -137,7 +137,6 @@ "connection_kwargs ={\n", " \"autocommit\": True,\n", " \"prepare_threshold\": 0,\n", - " \"row_factory\": dict_row,\n", "}" ] }, @@ -551,9 +550,9 @@ ], "metadata": { "kernelspec": { - "display_name": "langgraph-postgres", + "display_name": "langgraph", "language": "python", - "name": "langgraph-postgres" + "name": "langgraph" }, "language_info": { "codemirror_mode": { diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py index 433bc231d..cb4a79c35 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py @@ -66,7 +66,7 @@ class PostgresSaver(BasePostgresSaver): the first time checkpointer is used. """ with self.lock: - with self.conn.cursor(binary=True) as cur: + with self.conn.cursor(binary=True, row_factory=dict_row) as cur: try: version = cur.execute( "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1" @@ -127,31 +127,33 @@ class PostgresSaver(BasePostgresSaver): if limit: query += f" LIMIT {limit}" # if we change this to use .stream() we need to make sure to close the cursor - for value in self.conn.execute(query, args, binary=True): - yield CheckpointTuple( - { - "configurable": { - "thread_id": value["thread_id"], - "checkpoint_ns": value["checkpoint_ns"], - "checkpoint_id": value["checkpoint_id"], + with self._cursor() as cur: + cur.execute(query, args, binary=True) + for value in cur: + yield CheckpointTuple( + { + "configurable": { + "thread_id": value["thread_id"], + "checkpoint_ns": value["checkpoint_ns"], + "checkpoint_id": value["checkpoint_id"], + } + }, + { + **self._load_checkpoint(value["checkpoint"]), + "channel_values": self._load_blobs(value["channel_values"]), + }, + self._load_metadata(value["metadata"]), + { + "configurable": { + "thread_id": value["thread_id"], + "checkpoint_ns": value["checkpoint_ns"], + "checkpoint_id": value["parent_checkpoint_id"], + } } - }, - { - **self._load_checkpoint(value["checkpoint"]), - "channel_values": self._load_blobs(value["channel_values"]), - }, - self._load_metadata(value["metadata"]), - { - "configurable": { - "thread_id": value["thread_id"], - "checkpoint_ns": value["checkpoint_ns"], - "checkpoint_id": value["parent_checkpoint_id"], - } - } - if value["parent_checkpoint_id"] - else None, - self._load_writes(value["pending_writes"]), - ) + if value["parent_checkpoint_id"] + else None, + self._load_writes(value["pending_writes"]), + ) def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: """Get a checkpoint tuple from the database. @@ -199,7 +201,7 @@ class PostgresSaver(BasePostgresSaver): where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1" with self._cursor() as cur: - cur = self.conn.execute( + cur.execute( self.SELECT_SQL + where, args, binary=True, @@ -336,7 +338,7 @@ class PostgresSaver(BasePostgresSaver): # in multiple threads/coroutines, but only one cursor can be # used at a time try: - with self.conn.cursor(binary=True) as cur: + with self.conn.cursor(binary=True, row_factory=dict_row) as cur: yield cur finally: if pipeline: @@ -344,8 +346,10 @@ class PostgresSaver(BasePostgresSaver): elif pipeline: # a connection not in pipeline mode can only be used by one # thread/coroutine at a time, so we acquire a lock - with self.lock, self.conn.pipeline(), self.conn.cursor(binary=True) as cur: + with self.lock, self.conn.pipeline(), self.conn.cursor( + binary=True, row_factory=dict_row + ) as cur: yield cur else: - with self.lock, self.conn.cursor(binary=True) as cur: + with self.lock, self.conn.cursor(binary=True, row_factory=dict_row) as cur: yield cur diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py index 79ae1ddf7..569159d91 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py @@ -64,7 +64,7 @@ class AsyncPostgresSaver(BasePostgresSaver): the first time checkpointer is used. """ async with self.lock: - async with self.conn.cursor(binary=True) as cur: + async with self.conn.cursor(binary=True, row_factory=dict_row) as cur: try: results = await cur.execute( "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1" @@ -110,33 +110,35 @@ class AsyncPostgresSaver(BasePostgresSaver): if limit: query += f" LIMIT {limit}" # if we change this to use .stream() we need to make sure to close the cursor - async for value in await self.conn.execute(query, args, binary=True): - yield CheckpointTuple( - { - "configurable": { - "thread_id": value["thread_id"], - "checkpoint_ns": value["checkpoint_ns"], - "checkpoint_id": value["checkpoint_id"], + async with self._cursor() as cur: + await cur.execute(query, args, binary=True) + async for value in cur: + yield CheckpointTuple( + { + "configurable": { + "thread_id": value["thread_id"], + "checkpoint_ns": value["checkpoint_ns"], + "checkpoint_id": value["checkpoint_id"], + } + }, + { + **self._load_checkpoint(value["checkpoint"]), + "channel_values": await asyncio.to_thread( + self._load_blobs, value["channel_values"] + ), + }, + self._load_metadata(value["metadata"]), + { + "configurable": { + "thread_id": value["thread_id"], + "checkpoint_ns": value["checkpoint_ns"], + "checkpoint_id": value["parent_checkpoint_id"], + } } - }, - { - **self._load_checkpoint(value["checkpoint"]), - "channel_values": await asyncio.to_thread( - self._load_blobs, value["channel_values"] - ), - }, - self._load_metadata(value["metadata"]), - { - "configurable": { - "thread_id": value["thread_id"], - "checkpoint_ns": value["checkpoint_ns"], - "checkpoint_id": value["parent_checkpoint_id"], - } - } - if value["parent_checkpoint_id"] - else None, - await asyncio.to_thread(self._load_writes, value["pending_writes"]), - ) + if value["parent_checkpoint_id"] + else None, + await asyncio.to_thread(self._load_writes, value["pending_writes"]), + ) async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: """Get a checkpoint tuple from the database asynchronously. @@ -163,7 +165,7 @@ class AsyncPostgresSaver(BasePostgresSaver): where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1" async with self._cursor() as cur: - cur = await self.conn.execute( + await cur.execute( self.SELECT_SQL + where, args, binary=True, @@ -293,7 +295,7 @@ class AsyncPostgresSaver(BasePostgresSaver): # in multiple threads/coroutines, but only one cursor can be # used at a time try: - async with self.conn.cursor(binary=True) as cur: + async with self.conn.cursor(binary=True, row_factory=dict_row) as cur: yield cur finally: if pipeline: @@ -302,9 +304,11 @@ class AsyncPostgresSaver(BasePostgresSaver): # a connection not in pipeline mode can only be used by one # thread/coroutine at a time, so we acquire a lock async with self.lock, self.conn.pipeline(), self.conn.cursor( - binary=True + binary=True, row_factory=dict_row ) as cur: yield cur else: - async with self.lock, self.conn.cursor(binary=True) as cur: + async with self.lock, self.conn.cursor( + binary=True, row_factory=dict_row + ) as cur: yield cur From 75dec9b924d1c91818a4593c292286c19a359b9f Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Fri, 23 Aug 2024 00:18:06 +0200 Subject: [PATCH 088/287] fix(sdk-js): support sending end events --- libs/sdk-js/src/client.mts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/libs/sdk-js/src/client.mts b/libs/sdk-js/src/client.mts index bf98dccb4..fbf2dd12c 100644 --- a/libs/sdk-js/src/client.mts +++ b/libs/sdk-js/src/client.mts @@ -571,6 +571,7 @@ export class RunsClient extends BaseClient { ); let parser: EventSourceParser; + let onEndEvent: () => void; const textDecoder = new TextDecoder(); const stream: ReadableStream<{ event: string; data: any }> = ( @@ -594,9 +595,17 @@ export class RunsClient extends BaseClient { }); } }); + onEndEvent = () => { + ctrl.enqueue({ event: "end", data: undefined }); + }; }, async transform(chunk) { - parser.feed(textDecoder.decode(chunk)); + const payload = textDecoder.decode(chunk); + parser.feed(payload); + + // eventsource-parser will ignore events + // that are not terminated by a newline + if (payload.trim() === "event: end") onEndEvent(); }, }), ); From 0b7f451b40e0593feaf3e016265fa02d4ac3ccd4 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Fri, 23 Aug 2024 00:18:28 +0200 Subject: [PATCH 089/287] Bump to 0.0.6 --- 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 db509e9c2..a10bcdab8 100644 --- a/libs/sdk-js/package.json +++ b/libs/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-sdk", - "version": "0.0.5", + "version": "0.0.6", "description": "Client library for interacting with the LangGraph API", "type": "module", "packageManager": "yarn@1.22.19", From 15c3105748e4a2b0c28ea2d03ccd84f568ce13ab Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 15:36:48 -0700 Subject: [PATCH 090/287] cli0.1.51 --- libs/cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml index 508571f06..02c373e95 100644 --- a/libs/cli/pyproject.toml +++ b/libs/cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-cli" -version = "0.1.50" +version = "0.1.51" description = "CLI for interacting with LangGraph API" authors = [] license = "MIT" From c72acc91455ac81b9ddab661f6a8cb9ac21b8204 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Fri, 23 Aug 2024 01:38:53 +0200 Subject: [PATCH 091/287] Add missing status --- libs/sdk-js/src/schema.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/libs/sdk-js/src/schema.ts b/libs/sdk-js/src/schema.ts index 9c2df19a9..1d772b7eb 100644 --- a/libs/sdk-js/src/schema.ts +++ b/libs/sdk-js/src/schema.ts @@ -2,6 +2,16 @@ import type { JSONSchema7 } from "json-schema"; type Optional = T | null | undefined; +type RunStatus = + | "pending" + | "running" + | "error" + | "success" + | "timeout" + | "interrupted"; + +type ThreadStatus = "idle" | "busy" | "interrupted"; + export interface Config { /** * Tags for this call and any sub-calls (eg. a Chain calling an LLM). @@ -80,6 +90,7 @@ export interface Thread { created_at: string; updated_at: string; metadata: Metadata; + status: ThreadStatus; } export interface Cron { @@ -112,12 +123,6 @@ export interface Run { assistant_id: string; created_at: string; updated_at: string; - status: - | "pending" - | "running" - | "error" - | "success" - | "timeout" - | "interrupted"; + status: RunStatus; metadata: Metadata; } From 19b382335f931920633045319102ee75b7da7c64 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 17:03:11 -0700 Subject: [PATCH 092/287] sdk: Add on_disconnect arg to create/wait streaming run --- libs/sdk-py/langgraph_sdk/client.py | 21 +++++++++++++++++---- libs/sdk-py/langgraph_sdk/schema.py | 2 ++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index e3a83c543..fb8435b5c 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -25,6 +25,7 @@ from langgraph_sdk.schema import ( Assistant, Config, Cron, + DisconnectMode, GraphSchema, Metadata, MultitaskStrategy, @@ -963,6 +964,8 @@ class RunsClient: interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, feedback_keys: Optional[list[str]] = None, + on_disconnect: Optional[DisconnectMode] = None, + webhook: Optional[str] = None, multitask_strategy: Optional[MultitaskStrategy] = None, ) -> AsyncIterator[StreamPart]: ... @@ -980,6 +983,8 @@ class RunsClient: interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, feedback_keys: Optional[list[str]] = None, + on_disconnect: Optional[DisconnectMode] = None, + webhook: Optional[str] = None, ) -> AsyncIterator[StreamPart]: ... @@ -996,6 +1001,7 @@ class RunsClient: interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, feedback_keys: Optional[list[str]] = None, + on_disconnect: Optional[DisconnectMode] = None, webhook: Optional[str] = None, multitask_strategy: Optional[MultitaskStrategy] = None, ) -> AsyncIterator[StreamPart]: @@ -1019,6 +1025,8 @@ class RunsClient: webhook: Webhook to call after LangGraph API call is done. multitask_strategy: Multitask strategy to use. Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + disconnect_mode: The disconnect mode to use. + Must be one of 'cancel' or 'continue'. Returns: AsyncIterator[StreamPart]: Asynchronous iterator of stream results. @@ -1061,6 +1069,7 @@ class RunsClient: "webhook": webhook, "checkpoint_id": checkpoint_id, "multitask_strategy": multitask_strategy, + "on_disconnect": on_disconnect, } endpoint = ( f"/threads/{thread_id}/runs/stream" @@ -1129,9 +1138,7 @@ class RunsClient: config: The configuration for the assistant. checkpoint_id: The checkpoint to start streaming from. interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - webhook: Webhook to call after LangGraph API call is done. multitask_strategy: Multitask strategy to use. Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. @@ -1242,6 +1249,8 @@ class RunsClient: checkpoint_id: Optional[str] = None, interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, + webhook: Optional[str] = None, + on_disconnect: Optional[DisconnectMode] = None, multitask_strategy: Optional[MultitaskStrategy] = None, ) -> Union[list[dict], dict[str, Any]]: ... @@ -1257,6 +1266,8 @@ class RunsClient: config: Optional[Config] = None, interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, + webhook: Optional[str] = None, + on_disconnect: Optional[DisconnectMode] = None, ) -> Union[list[dict], dict[str, Any]]: ... @@ -1272,6 +1283,7 @@ class RunsClient: interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, webhook: Optional[str] = None, + on_disconnect: Optional[DisconnectMode] = None, multitask_strategy: Optional[MultitaskStrategy] = None, ) -> Union[list[dict], dict[str, Any]]: """Create a run, wait until it finishes and return the final state. @@ -1286,12 +1298,12 @@ class RunsClient: config: The configuration for the assistant. checkpoint_id: The checkpoint to start streaming from. interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - webhook: Webhook to call after LangGraph API call is done. multitask_strategy: Multitask strategy to use. Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + on_disconnect: The disconnect mode to use. + Must be one of 'cancel' or 'continue'. Returns: Union[list[dict], dict[str, Any]]: The output of the run. @@ -1351,6 +1363,7 @@ class RunsClient: "webhook": webhook, "checkpoint_id": checkpoint_id, "multitask_strategy": multitask_strategy, + "on_disconnect": on_disconnect, } endpoint = ( f"/threads/{thread_id}/runs/wait" if thread_id is not None else "/runs/wait" diff --git a/libs/sdk-py/langgraph_sdk/schema.py b/libs/sdk-py/langgraph_sdk/schema.py index a0fac51a0..c3232c88e 100644 --- a/libs/sdk-py/langgraph_sdk/schema.py +++ b/libs/sdk-py/langgraph_sdk/schema.py @@ -9,6 +9,8 @@ ThreadStatus = Literal["idle", "busy", "interrupted"] StreamMode = Literal["values", "messages", "updates", "events", "debug"] +DisconnectMode = Literal["cancel", "continue"] + MultitaskStrategy = Literal["reject", "interrupt", "rollback", "enqueue"] OnConflictBehavior = Literal["raise", "do_nothing"] From 6ece7124ed251812bc78f307d464f92d6565b3db Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 17:21:01 -0700 Subject: [PATCH 093/287] Fix param name in docstring --- libs/sdk-py/langgraph_sdk/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index fb8435b5c..24b2ef3b0 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -1025,7 +1025,7 @@ class RunsClient: webhook: Webhook to call after LangGraph API call is done. multitask_strategy: Multitask strategy to use. Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. - disconnect_mode: The disconnect mode to use. + on_disconnect: The disconnect mode to use. Must be one of 'cancel' or 'continue'. Returns: From 4162be81196c8468b8177b08ca590b6faacd62e3 Mon Sep 17 00:00:00 2001 From: vbarda Date: Thu, 22 Aug 2024 20:03:36 -0400 Subject: [PATCH 094/287] optimize subgraph state lookups --- .../langgraph/checkpoint/postgres/base.py | 3 + .../langgraph/checkpoint/sqlite/utils.py | 3 + .../langgraph/checkpoint/memory/__init__.py | 7 +- libs/langgraph/langgraph/pregel/__init__.py | 218 +++++++++--------- 4 files changed, 116 insertions(+), 115 deletions(-) diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py index 4b665fa64..ff2ec6681 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py @@ -255,6 +255,9 @@ class BasePostgresSaver(BaseCheckpointSaver): if config: wheres.append("thread_id = %s ") param_values.append(config["configurable"]["thread_id"]) + if checkpoint_id := get_checkpoint_id(config): + wheres.append("checkpoint_id = %s ") + param_values.append(checkpoint_id) # construct predicate for metadata filter if filter: diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py index 56034ea34..26b8594d6 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py @@ -70,6 +70,9 @@ def search_where( if config is not None: wheres.append("thread_id = ?") param_values.append(config["configurable"]["thread_id"]) + if checkpoint_id := get_checkpoint_id(config): + wheres.append("checkpoint_id = ?") + param_values.append(checkpoint_id) # construct predicate for metadata filter if filter: diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 5d73f4ba5..0a84326d7 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -177,6 +177,7 @@ class MemorySaver( Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples. """ thread_ids = (config["configurable"]["thread_id"],) if config else self.storage + config_checkpoint_id = get_checkpoint_id(config) if config else None for thread_id in thread_ids: for checkpoint_ns in self.storage[thread_id].keys(): for checkpoint_id, ( @@ -188,7 +189,11 @@ class MemorySaver( key=lambda x: x[0], reverse=True, ): - # filter by checkpoint ID + # filter by checkpoint ID from config + if config_checkpoint_id and checkpoint_id != config_checkpoint_id: + continue + + # filter by checkpoint ID from `before` config if ( before and (before_checkpoint_id := get_checkpoint_id(before)) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 477f5a6d1..039d3bbef 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -178,34 +178,31 @@ class Channel: ) -def _get_subgraph(graph: Pregel, checkpoint_ns: str) -> Pregel: - if checkpoint_ns == "": - return graph - - path = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) - nodes = graph.nodes - for subgraph_node_name in path: - # if we have this separator it means we have a node that was triggered by Send - if SEND_CHECKPOINT_NAMESPACE_SEPARATOR in subgraph_node_name: - name_parts = subgraph_node_name.split(SEND_CHECKPOINT_NAMESPACE_SEPARATOR) - if len(name_parts) != 2: - raise ValueError(f"Malformed node name '{subgraph_node_name}'") - - subgraph_node_name = name_parts[0] - if subgraph_node_name not in nodes: - raise ValueError(f"Couldn't find node '{subgraph_node_name}'.") - - subgraph_node = nodes[subgraph_node_name] - if isinstance(subgraph_node.bound, Pregel): - nodes = subgraph_node.bound.nodes - elif isinstance(subgraph_node.bound, RunnableSequence): - for runnable in subgraph_node.bound.steps: +def _get_checkpoint_ns_to_graph( + graph: Pregel, checkpoint_ns_to_graph: dict[str, Pregel] = {}, checkpoint_ns="" +) -> Pregel: + for node_name, node in graph.nodes.items(): + if isinstance(node.bound, Pregel): + _get_checkpoint_ns_to_graph( + node.bound, + checkpoint_ns_to_graph, + f"{checkpoint_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{node_name}" + if checkpoint_ns + else node_name, + ) + elif isinstance(node.bound, RunnableSequence): + for runnable in node.bound.steps: if isinstance(runnable, Pregel): - nodes = runnable.nodes - break - else: - continue - return subgraph_node.bound + _get_checkpoint_ns_to_graph( + node.bound, + checkpoint_ns_to_graph, + f"{checkpoint_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{node_name}" + if checkpoint_ns + else node_name, + ) + + checkpoint_ns_to_graph[checkpoint_ns] = graph + return checkpoint_ns_to_graph def _has_nested_interrupts( @@ -401,59 +398,54 @@ class Pregel( saved = self.checkpointer.get_tuple(config) checkpoint_config = saved.config if saved else config checkpoint_ns = checkpoint_config["configurable"].get("checkpoint_ns", "") - checkpoint_id = checkpoint_config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} - checkpoint_ns_to_graph: dict[str, Pregel] = {} - for saved in self.checkpointer.list(config): + checkpoint_ns_to_graph: dict[str, Pregel] = _get_checkpoint_ns_to_graph(self) + + # we only lookup subgraph checkpoints if we actually have subgraphs + if len(set(checkpoint_ns_to_graph)) == 1: + checkpoint_tuples = (saved,) + else: + checkpoint_tuples = self.checkpointer.list(saved.config) + + for saved in checkpoint_tuples: saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] - if checkpoint_id != saved_checkpoint_id: + + graph_checkpoint_ns = saved_checkpoint_ns.split( + SEND_CHECKPOINT_NAMESPACE_SEPARATOR + )[0] + graph = checkpoint_ns_to_graph.get(graph_checkpoint_ns) + if graph is None: continue - existing_checkpoint_id = checkpoint_ns_to_checkpoint_id.get( - saved_checkpoint_ns - ) - # keep only most recent checkpoint_id - if ( - existing_checkpoint_id is None - or saved_checkpoint_id > existing_checkpoint_id + with ChannelsManager( + graph.channels, saved.checkpoint, saved.config, skip_context=True + ) as ( + channels, + managed, ): - if saved_checkpoint_ns not in checkpoint_ns_to_graph: - checkpoint_ns_to_graph[saved_checkpoint_ns] = _get_subgraph( - self, saved_checkpoint_ns - ) - - graph = checkpoint_ns_to_graph[saved_checkpoint_ns] - with ChannelsManager( - graph.channels, saved.checkpoint, saved.config, skip_context=True - ) as ( + next_tasks = prepare_next_tasks( + saved.checkpoint, + graph.nodes, channels, managed, - ): - next_tasks = prepare_next_tasks( - saved.checkpoint, - graph.nodes, - channels, - managed, - saved.config, - saved.metadata.get("step", -1) + 1, - for_execution=False, - ) - state_snapshot = StateSnapshot( - read_channels(channels, graph.stream_channels_asis), - tuple(t.name for t in next_tasks), - saved.config, - saved.metadata, - saved.checkpoint["ts"], - saved.parent_config, - tasks_w_writes(next_tasks, saved.pending_writes), - ) + saved.config, + saved.metadata.get("step", -1) + 1, + for_execution=False, + ) + state_snapshot = StateSnapshot( + read_channels(channels, graph.stream_channels_asis), + tuple(t.name for t in next_tasks), + saved.config, + saved.metadata, + saved.checkpoint["ts"], + saved.parent_config, + tasks_w_writes(next_tasks, saved.pending_writes), + ) - checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot - checkpoint_ns_to_checkpoint_id[ - saved_checkpoint_ns - ] = saved_checkpoint_id + checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot + checkpoint_ns_to_checkpoint_id[saved_checkpoint_ns] = saved_checkpoint_id if not checkpoint_ns_to_state_snapshots: return StateSnapshot( @@ -479,57 +471,55 @@ class Pregel( saved = await self.checkpointer.aget_tuple(config) checkpoint_config = saved.config if saved else config checkpoint_ns = checkpoint_config["configurable"].get("checkpoint_ns", "") - checkpoint_id = checkpoint_config["configurable"].get("checkpoint_id") checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} - checkpoint_ns_to_graph: dict[str, Pregel] = {} - async for saved in self.checkpointer.alist(config): + checkpoint_ns_to_graph: dict[str, Pregel] = _get_checkpoint_ns_to_graph(self) + + # we only lookup subgraph checkpoints if we actually have subgraphs + if len(set(checkpoint_ns_to_graph)) == 1: + + async def list_checkpoints(): + yield saved + + checkpoint_tuples = list_checkpoints() + else: + checkpoint_tuples = self.checkpointer.alist(saved.config) + + async for saved in checkpoint_tuples: saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] - if checkpoint_id != saved_checkpoint_id: + + graph_checkpoint_ns = saved_checkpoint_ns.split( + SEND_CHECKPOINT_NAMESPACE_SEPARATOR + )[0] + graph = checkpoint_ns_to_graph.get(graph_checkpoint_ns) + if graph is None: continue - existing_checkpoint_id = checkpoint_ns_to_checkpoint_id.get( - saved_checkpoint_ns - ) + async with AsyncChannelsManager( + graph.channels, saved.checkpoint, saved.config, skip_context=True + ) as (channels, managed): + next_tasks = prepare_next_tasks( + saved.checkpoint, + graph.nodes, + channels, + managed, + saved.config, + saved.metadata.get("step", -1) + 1, + for_execution=False, + ) + state_snapshot = StateSnapshot( + read_channels(channels, graph.stream_channels_asis), + tuple(t.name for t in next_tasks), + saved.config, + saved.metadata, + saved.checkpoint["ts"], + saved.parent_config, + tasks_w_writes(next_tasks, saved.pending_writes), + ) - # keep only most recent checkpoint_id - if ( - existing_checkpoint_id is None - or saved_checkpoint_id > existing_checkpoint_id - ): - if saved_checkpoint_ns not in checkpoint_ns_to_graph: - checkpoint_ns_to_graph[saved_checkpoint_ns] = _get_subgraph( - self, saved_checkpoint_ns - ) - - graph = checkpoint_ns_to_graph[saved_checkpoint_ns] - async with AsyncChannelsManager( - graph.channels, saved.checkpoint, saved.config, skip_context=True - ) as (channels, managed): - next_tasks = prepare_next_tasks( - saved.checkpoint, - graph.nodes, - channels, - managed, - saved.config, - saved.metadata.get("step", -1) + 1, - for_execution=False, - ) - state_snapshot = StateSnapshot( - read_channels(channels, graph.stream_channels_asis), - tuple(t.name for t in next_tasks), - saved.config, - saved.metadata, - saved.checkpoint["ts"], - saved.parent_config, - tasks_w_writes(next_tasks, saved.pending_writes), - ) - - checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot - checkpoint_ns_to_checkpoint_id[ - saved_checkpoint_ns - ] = saved_checkpoint_id + checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot + checkpoint_ns_to_checkpoint_id[saved_checkpoint_ns] = saved_checkpoint_id if not checkpoint_ns_to_state_snapshots: return StateSnapshot( From 065055e5870fd1120eed6a98dfb893ad5108421c Mon Sep 17 00:00:00 2001 From: vbarda Date: Thu, 22 Aug 2024 20:37:48 -0400 Subject: [PATCH 095/287] small change --- libs/langgraph/langgraph/pregel/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 039d3bbef..2ee00bb29 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -194,7 +194,7 @@ def _get_checkpoint_ns_to_graph( for runnable in node.bound.steps: if isinstance(runnable, Pregel): _get_checkpoint_ns_to_graph( - node.bound, + runnable, checkpoint_ns_to_graph, f"{checkpoint_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{node_name}" if checkpoint_ns From dec7eb6f585f2632fcac7845af81029826ec3ed5 Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:54:42 -0700 Subject: [PATCH 096/287] [Docs] Use injected RunnableConfig (#1444) * Pass via type * Format --- .../agent-simulation-evaluation.ipynb | 5 +- .../information-gather-prompting.ipynb | 21 ++++-- examples/create-react-agent-hitl.ipynb | 2 +- .../customer-support/customer-support.ipynb | 24 ++++--- .../human_in_the_loop/review-tool-calls.ipynb | 65 ++++++++++--------- examples/input_output_schema.ipynb | 4 ++ examples/llm-compiler/LLMCompiler.ipynb | 10 ++- examples/many-tools.ipynb | 6 +- examples/pass-run-time-values-to-tools.ipynb | 1 + examples/pass_private_state.ipynb | 4 +- examples/persistence.ipynb | 2 +- examples/persistence_mongodb.ipynb | 14 ++-- examples/persistence_postgres.ipynb | 6 +- examples/persistence_redis.ipynb | 12 +++- examples/reflection/reflection.ipynb | 2 +- examples/reflexion/reflexion.ipynb | 1 + examples/streaming-content.ipynb | 4 +- .../tutorials/rag-agent-testing-local.ipynb | 19 +++--- .../tutorials/tool-calling-agent-local.ipynb | 4 ++ 19 files changed, 127 insertions(+), 79 deletions(-) diff --git a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb index c41653c50..02f8782eb 100644 --- a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb +++ b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb @@ -235,7 +235,7 @@ " # Call the chat bot\n", " chat_bot_response = my_chat_bot(messages)\n", " # Respond with an AI Message\n", - " return {\"messages\":[AIMessage(content=chat_bot_response[\"content\"])]}" + " return {\"messages\": [AIMessage(content=chat_bot_response[\"content\"])]}" ] }, { @@ -270,7 +270,7 @@ " # Call the simulated user\n", " response = simulated_user.invoke({\"messages\": new_messages})\n", " # This response is an AI message - we need to flip this to be a human message\n", - " return {\"messages\":[HumanMessage(content=response.content)]}" + " return {\"messages\": [HumanMessage(content=response.content)]}" ] }, { @@ -331,6 +331,7 @@ "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "graph_builder = StateGraph(State)\n", "graph_builder.add_node(\"user\", simulated_user_node)\n", "graph_builder.add_node(\"chat_bot\", chat_bot_node)\n", diff --git a/examples/chatbots/information-gather-prompting.ipynb b/examples/chatbots/information-gather-prompting.ipynb index ad4cdfe35..ad375f02f 100644 --- a/examples/chatbots/information-gather-prompting.ipynb +++ b/examples/chatbots/information-gather-prompting.ipynb @@ -79,7 +79,7 @@ "\n", "\n", "def info_chain(state):\n", - " messages = get_messages_info(state['messages'])\n", + " messages = get_messages_info(state[\"messages\"])\n", " response = llm_with_tool.invoke(messages)\n", " return {\"messages\": [response]}" ] @@ -126,7 +126,7 @@ "\n", "\n", "def prompt_gen_chain(state):\n", - " messages = get_prompt_messages(state['messages'])\n", + " messages = get_prompt_messages(state[\"messages\"])\n", " response = llm.invoke(messages)\n", " return {\"messages\": [response]}" ] @@ -158,7 +158,7 @@ "\n", "\n", "def get_state(state) -> Literal[\"add_tool_message\", \"info\", \"__end__\"]:\n", - " messages = state['messages']\n", + " messages = state[\"messages\"]\n", " if isinstance(messages[-1], AIMessage) and messages[-1].tool_calls:\n", " return \"add_tool_message\"\n", " elif not isinstance(messages[-1], HumanMessage):\n", @@ -190,9 +190,11 @@ "from typing import Annotated\n", "from typing_extensions import TypedDict\n", "\n", + "\n", "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "memory = MemorySaver()\n", "workflow = StateGraph(State)\n", "workflow.add_node(\"info\", info_chain)\n", @@ -201,9 +203,14 @@ "\n", "@workflow.add_node\n", "def add_tool_message(state: State):\n", - " return {\"messages\": [ToolMessage(\n", - " content=\"Prompt generated!\", tool_call_id=state['messages'][-1].tool_calls[0][\"id\"]\n", - " )]}\n", + " return {\n", + " \"messages\": [\n", + " ToolMessage(\n", + " content=\"Prompt generated!\",\n", + " tool_call_id=state[\"messages\"][-1].tool_calls[0][\"id\"],\n", + " )\n", + " ]\n", + " }\n", "\n", "\n", "workflow.add_conditional_edges(\"info\", get_state)\n", @@ -364,7 +371,7 @@ " for output in graph.stream(\n", " {\"messages\": [HumanMessage(content=user)]}, config=config, stream_mode=\"updates\"\n", " ):\n", - " last_message = next(iter(output.values()))['messages'][-1]\n", + " last_message = next(iter(output.values()))[\"messages\"][-1]\n", " last_message.pretty_print()\n", "\n", " if output and \"prompt\" in output:\n", diff --git a/examples/create-react-agent-hitl.ipynb b/examples/create-react-agent-hitl.ipynb index 39392f205..2bc6d306f 100644 --- a/examples/create-react-agent-hitl.ipynb +++ b/examples/create-react-agent-hitl.ipynb @@ -239,7 +239,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.12.2" } }, "nbformat": 4, diff --git a/examples/customer-support/customer-support.ipynb b/examples/customer-support/customer-support.ipynb index cf451188f..07927338c 100644 --- a/examples/customer-support/customer-support.ipynb +++ b/examples/customer-support/customer-support.ipynb @@ -225,7 +225,14 @@ "\n", "Define the (`fetch_user_flight_information`) tool to let the agent see the current user's flight information. Then define tools to search for flights and manage the passenger's bookings stored in the SQL database.\n", "\n", - "We use `ensure_config` to pass in the `passenger_id` in via configurable parameters. The LLM never has to provide these explicitly, they are provided for a given invocation of the graph so that each user cannot access other passengers' booking information." + "We the can [access the RunnableConfig](https://python.langchain.com/v0.2/docs/how_to/tool_configure/#inferring-by-parameter-type) for a given run to check the `passenger_id` of the user accessing this application. The LLM never has to provide these explicitly, they are provided for a given invocation of the graph so that each user cannot access other passengers' booking information.\n", + "\n", + "
\n", + "

Compatibility

\n", + "

\n", + " This tutorial expects `langchain-core>=0.2.16` to use the injected RunnableConfig. Prior to that, you'd use `ensure_config` to collect the config from context.\n", + "

\n", + "
\n" ] }, { @@ -240,18 +247,17 @@ "from typing import Optional\n", "\n", "import pytz\n", - "from langchain_core.runnables import ensure_config\n", + "from langchain_core.runnables import RunnableConfig\n", "\n", "\n", "@tool\n", - "def fetch_user_flight_information() -> list[dict]:\n", + "def fetch_user_flight_information(config: RunnableConfig) -> list[dict]:\n", " \"\"\"Fetch all tickets for the user along with corresponding flight information and seat assignments.\n", "\n", " Returns:\n", " A list of dictionaries where each dictionary contains the ticket details,\n", " associated flight details, and the seat assignments for each ticket belonging to the user.\n", " \"\"\"\n", - " config = ensure_config() # Fetch from the context\n", " configuration = config.get(\"configurable\", {})\n", " passenger_id = configuration.get(\"passenger_id\", None)\n", " if not passenger_id:\n", @@ -328,9 +334,10 @@ "\n", "\n", "@tool\n", - "def update_ticket_to_new_flight(ticket_no: str, new_flight_id: int) -> str:\n", + "def update_ticket_to_new_flight(\n", + " ticket_no: str, new_flight_id: int, *, config: RunnableConfig\n", + ") -> str:\n", " \"\"\"Update the user's ticket to a new valid flight.\"\"\"\n", - " config = ensure_config()\n", " configuration = config.get(\"configurable\", {})\n", " passenger_id = configuration.get(\"passenger_id\", None)\n", " if not passenger_id:\n", @@ -396,9 +403,8 @@ "\n", "\n", "@tool\n", - "def cancel_ticket(ticket_no: str) -> str:\n", + "def cancel_ticket(ticket_no: str, *, config: RunnableConfig) -> str:\n", " \"\"\"Cancel the user's ticket and remove it from the database.\"\"\"\n", - " config = ensure_config()\n", " configuration = config.get(\"configurable\", {})\n", " passenger_id = configuration.get(\"passenger_id\", None)\n", " if not passenger_id:\n", @@ -4407,7 +4413,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.12.2" } }, "nbformat": 4, diff --git a/examples/human_in_the_loop/review-tool-calls.ipynb b/examples/human_in_the_loop/review-tool-calls.ipynb index 5a4aeaf56..84373b90b 100644 --- a/examples/human_in_the_loop/review-tool-calls.ipynb +++ b/examples/human_in_the_loop/review-tool-calls.ipynb @@ -141,15 +141,18 @@ " print(\"----\")\n", " return \"Sunny!\"\n", "\n", - "model = ChatAnthropic(model_name=\"claude-3-5-sonnet-20240620\").bind_tools([weather_search])\n", + "\n", + "model = ChatAnthropic(model_name=\"claude-3-5-sonnet-20240620\").bind_tools(\n", + " [weather_search]\n", + ")\n", + "\n", "\n", "class State(MessagesState):\n", " \"\"\"Simple state.\"\"\"\n", "\n", + "\n", "def call_llm(state):\n", - " return {\n", - " \"messages\": [model.invoke(state['messages'])]\n", - " }\n", + " return {\"messages\": [model.invoke(state[\"messages\"])]}\n", "\n", "\n", "def human_review_node(state):\n", @@ -159,28 +162,30 @@ "def run_tool(state):\n", " new_messages = []\n", " tools = {\"weather_search\": weather_search}\n", - " tool_calls = state['messages'][-1].tool_calls\n", + " tool_calls = state[\"messages\"][-1].tool_calls\n", " for tool_call in tool_calls:\n", - " tool = tools[tool_call['name']]\n", - " result = tool.invoke(tool_call['args'])\n", - " new_messages.append({\n", - " \"role\": \"tool\",\n", - " \"name\": tool_call['name'],\n", - " \"content\": result,\n", - " \"tool_call_id\": tool_call['id']\n", - " })\n", + " tool = tools[tool_call[\"name\"]]\n", + " result = tool.invoke(tool_call[\"args\"])\n", + " new_messages.append(\n", + " {\n", + " \"role\": \"tool\",\n", + " \"name\": tool_call[\"name\"],\n", + " \"content\": result,\n", + " \"tool_call_id\": tool_call[\"id\"],\n", + " }\n", + " )\n", " return {\"messages\": new_messages}\n", "\n", "\n", "def route_after_llm(state) -> Literal[END, \"human_review_node\"]:\n", - " if len(state['messages'][-1].tool_calls) == 0:\n", + " if len(state[\"messages\"][-1].tool_calls) == 0:\n", " return END\n", " else:\n", " return \"human_review_node\"\n", "\n", "\n", "def route_after_human(state) -> Literal[\"run_tool\", \"call_llm\"]:\n", - " if isinstance(state['messages'][-1], AIMessage):\n", + " if isinstance(state[\"messages\"][-1], AIMessage):\n", " return \"run_tool\"\n", " else:\n", " return \"call_llm\"\n", @@ -460,35 +465,35 @@ "print(\"Current State:\")\n", "print(state.values)\n", "print(\"\\nCurrent Tool Call ID:\")\n", - "current_content = state.values['messages'][-1].content\n", - "current_id = state.values['messages'][-1].id\n", - "tool_call_id = state.values['messages'][-1].tool_calls[0]['id']\n", + "current_content = state.values[\"messages\"][-1].content\n", + "current_id = state.values[\"messages\"][-1].id\n", + "tool_call_id = state.values[\"messages\"][-1].tool_calls[0][\"id\"]\n", "print(tool_call_id)\n", "\n", "# We now need to construct a replacement tool call.\n", "# We will change the argument to be `San Francisco, USA`\n", "# Note that we could change any number of arguments or tool names - it just has to be a valid one\n", "new_message = {\n", - " \"role\": \"assistant\", \n", + " \"role\": \"assistant\",\n", " \"content\": current_content,\n", " \"tool_calls\": [\n", " {\n", " \"id\": tool_call_id,\n", " \"name\": \"weather_search\",\n", - " \"args\": {\"city\": \"San Francisco, USA\"}\n", + " \"args\": {\"city\": \"San Francisco, USA\"},\n", " }\n", " ],\n", " # This is important - this needs to be the same as the message you replacing!\n", " # Otherwise, it will show up as a separate message\n", - " \"id\": current_id\n", + " \"id\": current_id,\n", "}\n", "graph.update_state(\n", " # This is the config which represents this thread\n", - " thread, \n", + " thread,\n", " # This is the updated value we want to push\n", - " {\"messages\": [new_message]}, \n", + " {\"messages\": [new_message]},\n", " # We push this update acting as our human_review_node\n", - " as_node=\"human_review_node\"\n", + " as_node=\"human_review_node\",\n", ")\n", "\n", "# Let's now continue executing from here\n", @@ -595,26 +600,26 @@ "print(\"Current State:\")\n", "print(state.values)\n", "print(\"\\nCurrent Tool Call ID:\")\n", - "tool_call_id = state.values['messages'][-1].tool_calls[0]['id']\n", + "tool_call_id = state.values[\"messages\"][-1].tool_calls[0][\"id\"]\n", "print(tool_call_id)\n", "\n", "# We now need to construct a replacement tool call.\n", "# We will change the argument to be `San Francisco, USA`\n", "# Note that we could change any number of arguments or tool names - it just has to be a valid one\n", "new_message = {\n", - " \"role\": \"tool\", \n", + " \"role\": \"tool\",\n", " # This is our natural language feedback\n", " \"content\": \"User requested changes: pass in the country as well\",\n", " \"name\": \"weather_search\",\n", - " \"tool_call_id\": tool_call_id\n", + " \"tool_call_id\": tool_call_id,\n", "}\n", "graph.update_state(\n", " # This is the config which represents this thread\n", - " thread, \n", + " thread,\n", " # This is the updated value we want to push\n", - " {\"messages\": [new_message]}, \n", + " {\"messages\": [new_message]},\n", " # We push this update acting as our human_review_node\n", - " as_node=\"human_review_node\"\n", + " as_node=\"human_review_node\",\n", ")\n", "\n", "# Let's now continue executing from here\n", diff --git a/examples/input_output_schema.ipynb b/examples/input_output_schema.ipynb index 16779ba6e..4837b40a8 100644 --- a/examples/input_output_schema.ipynb +++ b/examples/input_output_schema.ipynb @@ -33,15 +33,19 @@ "from langgraph.graph import StateGraph, START, END\n", "from typing import TypedDict\n", "\n", + "\n", "class InputState(TypedDict):\n", " question: str\n", "\n", + "\n", "class OutputState(TypedDict):\n", " answer: str\n", "\n", + "\n", "def answer_node(state: InputState):\n", " return {\"answer\": \"bye\"}\n", "\n", + "\n", "graph = StateGraph(input=InputState, output=OutputState)\n", "graph.add_node(answer_node)\n", "graph.add_edge(START, \"answer_node\")\n", diff --git a/examples/llm-compiler/LLMCompiler.ipynb b/examples/llm-compiler/LLMCompiler.ipynb index bfe5736d8..2f2aa81ed 100644 --- a/examples/llm-compiler/LLMCompiler.ipynb +++ b/examples/llm-compiler/LLMCompiler.ipynb @@ -526,7 +526,7 @@ " \"tasks\": tasks,\n", " }\n", " )\n", - " return {\"messages\":[scheduled_tasks]}" + " return {\"messages\": [scheduled_tasks]}" ] }, { @@ -653,7 +653,7 @@ " )\n", " ]\n", " else:\n", - " return {\"messages\":response + [AIMessage(content=decision.action.response)]}\n", + " return {\"messages\": response + [AIMessage(content=decision.action.response)]}\n", "\n", "\n", "def select_recent_messages(state) -> dict:\n", @@ -726,9 +726,11 @@ "from langgraph.graph.message import add_messages\n", "from typing import Annotated\n", "\n", + "\n", "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "graph_builder = StateGraph(State)\n", "\n", "# 1. Define vertices\n", @@ -794,7 +796,9 @@ } ], "source": [ - "for step in chain.stream({\"messages\":[HumanMessage(content=\"What's the GDP of New York?\")]}):\n", + "for step in chain.stream(\n", + " {\"messages\": [HumanMessage(content=\"What's the GDP of New York?\")]}\n", + "):\n", " print(step)\n", " print(\"---\")" ] diff --git a/examples/many-tools.ipynb b/examples/many-tools.ipynb index b415f5065..107c9921f 100644 --- a/examples/many-tools.ipynb +++ b/examples/many-tools.ipynb @@ -328,9 +328,9 @@ " \"set more_information_needed False and populate a blank string for the query.\"\n", " )\n", " input_messages = [system] + state[\"messages\"]\n", - " response = llm.bind_tools(\n", - " [QueryForTools], tool_choice=True\n", - " ).invoke(input_messages)\n", + " response = llm.bind_tools([QueryForTools], tool_choice=True).invoke(\n", + " input_messages\n", + " )\n", " query = response.tool_calls[0][\"args\"][\"query\"]\n", " tool_documents = vector_store.similarity_search(query)\n", " if hack_remove_tool_condition:\n", diff --git a/examples/pass-run-time-values-to-tools.ipynb b/examples/pass-run-time-values-to-tools.ipynb index 3c74bd06d..ab6680230 100644 --- a/examples/pass-run-time-values-to-tools.ipynb +++ b/examples/pass-run-time-values-to-tools.ipynb @@ -329,6 +329,7 @@ "\n", "tools = [get_context, cite_context_sources]\n", "\n", + "\n", "# Define the function that calls the model\n", "def call_model(state, config):\n", " messages = state[\"messages\"]\n", diff --git a/examples/pass_private_state.ipynb b/examples/pass_private_state.ipynb index 2e60d805e..d34ecc7e1 100644 --- a/examples/pass_private_state.ipynb +++ b/examples/pass_private_state.ipynb @@ -72,12 +72,12 @@ "# Node to retrieve documents\n", "def retrieve_documents(state: QueryOutputState) -> DocumentOutputState:\n", " # Replace this with real logic\n", - " return {\"docs\": [state['query']] * 2}\n", + " return {\"docs\": [state[\"query\"]] * 2}\n", "\n", "\n", "# Node to generate answer\n", "def generate(state: GenerateInputState) -> OverallState:\n", - " return {\"answer\": \"\\n\\n\".join(state['docs'] + [state['question']])}\n", + " return {\"answer\": \"\\n\\n\".join(state[\"docs\"] + [state[\"question\"]])}\n", "\n", "\n", "graph = StateGraph(OverallState)\n", diff --git a/examples/persistence.ipynb b/examples/persistence.ipynb index 869043cf1..715f7c0b5 100644 --- a/examples/persistence.ipynb +++ b/examples/persistence.ipynb @@ -587,7 +587,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.2" } }, "nbformat": 4, diff --git a/examples/persistence_mongodb.ipynb b/examples/persistence_mongodb.ipynb index 99ce73057..54dfdc638 100644 --- a/examples/persistence_mongodb.ipynb +++ b/examples/persistence_mongodb.ipynb @@ -630,7 +630,7 @@ " upsert=True,\n", " )\n", " )\n", - " await self.db[\"checkpoint_writes\"].bulk_write(operations)\n" + " await self.db[\"checkpoint_writes\"].bulk_write(operations)" ] }, { @@ -685,7 +685,9 @@ "metadata": {}, "outputs": [], "source": [ - "with MongoDBSaver.from_conn_info(host=\"localhost\", port=27017, db_name=\"checkpoints\") as checkpointer:\n", + "with MongoDBSaver.from_conn_info(\n", + " host=\"localhost\", port=27017, db_name=\"checkpoints\"\n", + ") as checkpointer:\n", " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", " config = {\"configurable\": {\"thread_id\": \"1\"}}\n", " res = graph.invoke({\"messages\": [(\"human\", \"what's the weather in sf\")]}, config)\n", @@ -796,10 +798,14 @@ "metadata": {}, "outputs": [], "source": [ - "async with AsyncMongoDBSaver.from_conn_info(host=\"localhost\", port=27017, db_name=\"checkpoints\") as checkpointer:\n", + "async with AsyncMongoDBSaver.from_conn_info(\n", + " host=\"localhost\", port=27017, db_name=\"checkpoints\"\n", + ") as checkpointer:\n", " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", " config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - " res = await graph.ainvoke({\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config)\n", + " res = await graph.ainvoke(\n", + " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n", + " )\n", "\n", " latest_checkpoint = await checkpointer.aget(config)\n", " latest_checkpoint_tuple = await checkpointer.aget_tuple(config)\n", diff --git a/examples/persistence_postgres.ipynb b/examples/persistence_postgres.ipynb index f43fa01d6..5ea838cee 100644 --- a/examples/persistence_postgres.ipynb +++ b/examples/persistence_postgres.ipynb @@ -134,7 +134,7 @@ "source": [ "from psycopg.rows import dict_row\n", "\n", - "connection_kwargs ={\n", + "connection_kwargs = {\n", " \"autocommit\": True,\n", " \"prepare_threshold\": 0,\n", "}" @@ -165,7 +165,7 @@ " # Example configuration\n", " conninfo=DB_URI,\n", " max_size=20,\n", - " kwargs=connection_kwargs\n", + " kwargs=connection_kwargs,\n", ")\n", "\n", "with pool.connection() as conn:\n", @@ -393,7 +393,7 @@ " # Example configuration\n", " conninfo=DB_URI,\n", " max_size=20,\n", - " kwargs=connection_kwargs\n", + " kwargs=connection_kwargs,\n", ") as pool, pool.connection() as conn:\n", " checkpointer = AsyncPostgresSaver(conn)\n", "\n", diff --git a/examples/persistence_redis.ipynb b/examples/persistence_redis.ipynb index 3b2ad1170..34d78a53f 100644 --- a/examples/persistence_redis.ipynb +++ b/examples/persistence_redis.ipynb @@ -530,7 +530,9 @@ "\n", " @classmethod\n", " @asynccontextmanager\n", - " async def from_conn_info(cls, *, host: str, port: int, db: int) -> AsyncIterator[\"AsyncRedisSaver\"]:\n", + " async def from_conn_info(\n", + " cls, *, host: str, port: int, db: int\n", + " ) -> AsyncIterator[\"AsyncRedisSaver\"]:\n", " conn = None\n", " try:\n", " conn = AsyncRedis(host=host, port=port, db=db)\n", @@ -887,10 +889,14 @@ "metadata": {}, "outputs": [], "source": [ - "async with AsyncRedisSaver.from_conn_info(host=\"localhost\", port=6379, db=0) as checkpointer:\n", + "async with AsyncRedisSaver.from_conn_info(\n", + " host=\"localhost\", port=6379, db=0\n", + ") as checkpointer:\n", " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", " config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - " res = await graph.ainvoke({\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config)\n", + " res = await graph.ainvoke(\n", + " {\"messages\": [(\"human\", \"what's the weather in nyc\")]}, config\n", + " )\n", "\n", " latest_checkpoint = await checkpointer.aget(config)\n", " latest_checkpoint_tuple = await checkpointer.aget_tuple(config)\n", diff --git a/examples/reflection/reflection.ipynb b/examples/reflection/reflection.ipynb index 810990e38..ca42160bd 100644 --- a/examples/reflection/reflection.ipynb +++ b/examples/reflection/reflection.ipynb @@ -269,7 +269,7 @@ "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", - " \n", + "\n", "async def generation_node(state: Sequence[BaseMessage]):\n", " return await generate.ainvoke({\"messages\": state})\n", "\n", diff --git a/examples/reflexion/reflexion.ipynb b/examples/reflexion/reflexion.ipynb index 670e6eb5b..8c183c817 100644 --- a/examples/reflexion/reflexion.ipynb +++ b/examples/reflexion/reflexion.ipynb @@ -392,6 +392,7 @@ "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", "\n", + "\n", "MAX_ITERATIONS = 5\n", "builder = StateGraph(State)\n", "builder.add_node(\"draft\", first_responder.respond)\n", diff --git a/examples/streaming-content.ipynb b/examples/streaming-content.ipynb index ff182ca21..8c2c7ab36 100644 --- a/examples/streaming-content.ipynb +++ b/examples/streaming-content.ipynb @@ -68,7 +68,9 @@ " # It's completely optional, but useful if you have many functions with similar names\n", " gen = RunnableGenerator(my_generator).with_config(\n", " tags=[\"should_stream\"],\n", - " callbacks=config.get(\"callbacks\", []) # <-- Propagate callbacks (Python <= 3.10)\n", + " callbacks=config.get(\n", + " \"callbacks\", []\n", + " ), # <-- Propagate callbacks (Python <= 3.10)\n", " )\n", " async for message in gen.astream(state):\n", " messages.append(message)\n", diff --git a/examples/tutorials/rag-agent-testing-local.ipynb b/examples/tutorials/rag-agent-testing-local.ipynb index f9e89e56d..3105d3342 100644 --- a/examples/tutorials/rag-agent-testing-local.ipynb +++ b/examples/tutorials/rag-agent-testing-local.ipynb @@ -169,9 +169,7 @@ "from langchain_core.output_parsers import JsonOutputParser\n", "\n", "# JSON\n", - "llm = ChatOllama(model=\"llama3.1\", \n", - " format=\"json\", \n", - " temperature=0)\n", + "llm = ChatOllama(model=\"llama3.1\", format=\"json\", temperature=0)\n", "\n", "\n", "prompt = PromptTemplate(\n", @@ -210,6 +208,7 @@ "from IPython.display import Image, display\n", "from langgraph.graph import START, END, StateGraph\n", "\n", + "\n", "class GraphState(TypedDict):\n", " \"\"\"\n", " Represents the state of our graph.\n", @@ -381,21 +380,22 @@ "metadata": {}, "outputs": [], "source": [ - "import uuid \n", + "import uuid\n", + "\n", "\n", "def predict_custom_agent_answer(example: dict):\n", - " \n", " config = {\"configurable\": {\"thread_id\": str(uuid.uuid4())}}\n", - " \n", + "\n", " state_dict = custom_graph.invoke(\n", " {\"question\": example[\"input\"], \"steps\": []}, config\n", " )\n", - " \n", + "\n", " return {\"response\": state_dict[\"generation\"], \"steps\": state_dict[\"steps\"]}\n", "\n", + "\n", "example = {\"input\": \"What are the types of agent memory?\"}\n", - "#response = predict_custom_agent_answer(example)\n", - "#response" + "# response = predict_custom_agent_answer(example)\n", + "# response" ] }, { @@ -544,6 +544,7 @@ " \"generate_answer\",\n", "]\n", "\n", + "\n", "def check_trajectory_custom(root_run: Run, example: Example) -> dict:\n", " \"\"\"\n", " Check if all expected tools are called in exact order and without any additional tool calls.\n", diff --git a/examples/tutorials/tool-calling-agent-local.ipynb b/examples/tutorials/tool-calling-agent-local.ipynb index 5c60336e8..12c43038f 100644 --- a/examples/tutorials/tool-calling-agent-local.ipynb +++ b/examples/tutorials/tool-calling-agent-local.ipynb @@ -134,6 +134,7 @@ " for d in web_results\n", " ]\n", "\n", + "\n", "# Tool list\n", "tools = [retrieve_documents, web_search]" ] @@ -152,9 +153,11 @@ "from langgraph.graph.message import AnyMessage, add_messages\n", "from typing_extensions import TypedDict\n", "\n", + "\n", "class State(TypedDict):\n", " messages: Annotated[list[AnyMessage], add_messages]\n", "\n", + "\n", "class Assistant:\n", " def __init__(self, runnable: Runnable):\n", " \"\"\"\n", @@ -291,6 +294,7 @@ "source": [ "import uuid\n", "\n", + "\n", "def predict_react_agent_answer(example: dict):\n", " \"\"\"Use this for answer evaluation\"\"\"\n", "\n", From 82db3831995ec3bf1897e5f79a97826b8cf03cb7 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 22 Aug 2024 21:36:27 -0700 Subject: [PATCH 097/287] Don't try to serialize exceptions - Store their string repr instead --- .../langgraph/checkpoint/serde/jsonplus.py | 5 ++- libs/langgraph/tests/any_str.py | 18 -------- libs/langgraph/tests/memory_assert.py | 2 - libs/langgraph/tests/test_pregel.py | 10 ++--- libs/langgraph/tests/test_pregel_async.py | 42 +++++++++---------- 5 files changed, 30 insertions(+), 47 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py index 5465b2802..608a077cd 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py @@ -7,6 +7,7 @@ import re from collections import deque from datetime import date, datetime, time, timedelta, timezone from enum import Enum +from inspect import isclass from ipaddress import ( IPv4Address, IPv4Interface, @@ -111,7 +112,7 @@ class JsonPlusSerializer(SerializerProtocol): obj.__class__, method="fromhex", args=[obj.hex()] ) elif isinstance(obj, BaseException): - return self._encode_constructor_args(obj.__class__, args=obj.args) + return repr(obj) else: raise TypeError( f"Object of type {obj.__class__.__name__} is not JSON serializable" @@ -135,6 +136,8 @@ class JsonPlusSerializer(SerializerProtocol): method = getattr(cls, value["method"]) else: method = cls + if isclass(method) and issubclass(method, BaseException): + return None if value["args"] and value["kwargs"]: return method(*value["args"], **value["kwargs"]) elif value["args"]: diff --git a/libs/langgraph/tests/any_str.py b/libs/langgraph/tests/any_str.py index a98962cdc..836cf9371 100644 --- a/libs/langgraph/tests/any_str.py +++ b/libs/langgraph/tests/any_str.py @@ -23,24 +23,6 @@ class AnyVersion: return hash(str(self)) -class ExceptionLike: - def __init__(self, exc: Exception) -> None: - self.exc = exc - - def __eq__(self, value: object) -> bool: - return ( - isinstance(value, Exception) - and self.exc.__class__ == value.__class__ - and str(self.exc) == str(value) - ) - - def __hash__(self) -> int: - return hash((self.exc.__class__, str(self.exc))) - - def __repr__(self) -> str: - return str(self.exc) - - class UnsortedSequence: def __init__(self, *values: Any) -> None: self.seq = values diff --git a/libs/langgraph/tests/memory_assert.py b/libs/langgraph/tests/memory_assert.py index 0b0bcf62f..624a711c7 100644 --- a/libs/langgraph/tests/memory_assert.py +++ b/libs/langgraph/tests/memory_assert.py @@ -24,8 +24,6 @@ class NoopSerializer(SerializerProtocol): class MemorySaverAssertImmutable(MemorySaver): - serde = NoopSerializer() - storage_for_copies: defaultdict[str, dict[str, dict[str, Checkpoint]]] def __init__( diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 06a747a17..7325aa41d 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -64,7 +64,7 @@ from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore -from tests.any_str import AnyStr, AnyVersion, ExceptionLike, UnsortedSequence +from tests.any_str import AnyStr, AnyVersion, UnsortedSequence from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -1307,7 +1307,7 @@ def test_invoke_checkpoint_two( assert checkpoint_tup is not None assert checkpoint_tup.checkpoint["channel_values"].get("total") == 7 assert checkpoint_tup.pending_writes == [ - (AnyStr(), ERROR, ExceptionLike(ValueError("Input is too large"))) + (AnyStr(), ERROR, "ValueError('Input is too large')") ] # on a new thread, total starts out as 0, so output is 0+5=5 assert app.invoke(5, {"configurable": {"thread_id": "2"}}) == 5 @@ -1374,7 +1374,7 @@ def test_pending_writes_resume( assert state.next == ("one", "two") assert state.tasks == ( PregelTask(AnyStr(), "one"), - PregelTask(AnyStr(), "two", ExceptionLike(ConnectionError("I'm not good"))), + PregelTask(AnyStr(), "two", 'ConnectionError("I\'m not good")'), ) assert state.metadata == {"source": "loop", "step": 0, "writes": None} # should contain pending write of "one" @@ -1384,7 +1384,7 @@ def test_pending_writes_resume( expected_writes = [ (AnyStr(), "one", "one"), (AnyStr(), "value", 2), - (AnyStr(), ERROR, ExceptionLike(ConnectionError("I'm not good"))), + (AnyStr(), ERROR, 'ConnectionError("I\'m not good")'), ] assert len(checkpoint.pending_writes) == 3 assert all(w in expected_writes for w in checkpoint.pending_writes) @@ -1518,7 +1518,7 @@ def test_pending_writes_resume( pending_writes=UnsortedSequence( (AnyStr(), "one", "one"), (AnyStr(), "value", 2), - (AnyStr(), "__error__", ExceptionLike(ConnectionError("I'm not good"))), + (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), (AnyStr(), "two", "two"), (AnyStr(), "value", 3), ), diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 4eed26017..a35bf7b00 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -61,7 +61,7 @@ from langgraph.pregel import Channel, GraphRecursionError, Pregel, StateSnapshot from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore -from tests.any_str import AnyStr, AnyVersion, ExceptionLike, UnsortedSequence +from tests.any_str import AnyStr, AnyVersion, UnsortedSequence from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -1593,7 +1593,7 @@ async def test_pending_writes_resume( assert state.next == ("one", "two") assert state.tasks == ( PregelTask(AnyStr(), "one"), - PregelTask(AnyStr(), "two", ExceptionLike(ValueError("I'm not good"))), + PregelTask(AnyStr(), "two", 'ValueError("I\'m not good")'), ) assert state.metadata == {"source": "loop", "step": 0, "writes": None} # should contain pending write of "one" @@ -1603,7 +1603,7 @@ async def test_pending_writes_resume( expected_writes = [ (AnyStr(), "one", "one"), (AnyStr(), "value", 2), - (AnyStr(), ERROR, ExceptionLike(ValueError("I'm not good"))), + (AnyStr(), ERROR, 'ValueError("I\'m not good")'), ] assert len(checkpoint.pending_writes) == 3 assert all(w in expected_writes for w in checkpoint.pending_writes) @@ -1741,7 +1741,7 @@ async def test_pending_writes_resume( pending_writes=UnsortedSequence( (AnyStr(), "one", "one"), (AnyStr(), "value", 2), - (AnyStr(), "__error__", ExceptionLike(ValueError("I'm not good"))), + (AnyStr(), "__error__", 'ValueError("I\'m not good")'), (AnyStr(), "two", "two"), (AnyStr(), "value", 3), ), @@ -3108,7 +3108,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: observation = {t.name: t for t in tools}[agent_action.tool].invoke( agent_action.tool_input ) - return {"intermediate_steps": [(agent_action, observation)]} + return {"intermediate_steps": [[agent_action, observation]]} # Define decision-making logic def should_continue(data: AgentState) -> str: @@ -3140,22 +3140,22 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: assert await app.ainvoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ), - ( + ], + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], "agent_outcome": AgentFinish( return_values={"answer": "answer"}, log="finish:answer" @@ -3176,14 +3176,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:query", ), "result for query", - ) + ] ], } }, @@ -3199,14 +3199,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="another", log="tool:search_api:another", ), "result for another", - ), + ], ], } }, @@ -3361,14 +3361,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -3401,14 +3401,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: log="finish:a really nice answer", ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], }, tasks=(), @@ -3537,14 +3537,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: { "tools": { "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], } }, @@ -3576,14 +3576,14 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: log="finish:a really nice answer", ), "intermediate_steps": [ - ( + [ AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), "result for query", - ) + ] ], }, tasks=(), From 1333d8b4781ecad144b220c6427dbe1f8fb27d50 Mon Sep 17 00:00:00 2001 From: vbarda Date: Fri, 23 Aug 2024 14:26:49 -0400 Subject: [PATCH 099/287] cleanup --- libs/langgraph/langgraph/pregel/__init__.py | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 2ee00bb29..f6ce5aefb 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -179,26 +179,28 @@ class Channel: def _get_checkpoint_ns_to_graph( - graph: Pregel, checkpoint_ns_to_graph: dict[str, Pregel] = {}, checkpoint_ns="" + graph: Pregel, + checkpoint_ns_to_graph: Optional[dict[str, Pregel]] = None, + checkpoint_ns: str = "", ) -> Pregel: + if checkpoint_ns_to_graph is None: + checkpoint_ns_to_graph = {} + for node_name, node in graph.nodes.items(): + new_checkpoint_ns = ( + f"{checkpoint_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{node_name}" + if checkpoint_ns + else node_name + ) if isinstance(node.bound, Pregel): _get_checkpoint_ns_to_graph( - node.bound, - checkpoint_ns_to_graph, - f"{checkpoint_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{node_name}" - if checkpoint_ns - else node_name, + node.bound, checkpoint_ns_to_graph, new_checkpoint_ns ) elif isinstance(node.bound, RunnableSequence): for runnable in node.bound.steps: if isinstance(runnable, Pregel): _get_checkpoint_ns_to_graph( - runnable, - checkpoint_ns_to_graph, - f"{checkpoint_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{node_name}" - if checkpoint_ns - else node_name, + runnable, checkpoint_ns_to_graph, new_checkpoint_ns ) checkpoint_ns_to_graph[checkpoint_ns] = graph From 1f29925034d788b3b8b8b8f26bcd0eadee3da286 Mon Sep 17 00:00:00 2001 From: vbarda Date: Fri, 23 Aug 2024 14:44:42 -0400 Subject: [PATCH 100/287] add max recursion depth --- libs/langgraph/langgraph/pregel/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index f6ce5aefb..33bae3330 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -182,10 +182,16 @@ def _get_checkpoint_ns_to_graph( graph: Pregel, checkpoint_ns_to_graph: Optional[dict[str, Pregel]] = None, checkpoint_ns: str = "", + max_depth: int = 10, ) -> Pregel: if checkpoint_ns_to_graph is None: checkpoint_ns_to_graph = {} + if max_depth <= 0: + raise RecursionError( + f"Reached maximum recursion depth while building checkpoint NS -> graph mapping." + ) + for node_name, node in graph.nodes.items(): new_checkpoint_ns = ( f"{checkpoint_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{node_name}" @@ -194,13 +200,16 @@ def _get_checkpoint_ns_to_graph( ) if isinstance(node.bound, Pregel): _get_checkpoint_ns_to_graph( - node.bound, checkpoint_ns_to_graph, new_checkpoint_ns + node.bound, checkpoint_ns_to_graph, new_checkpoint_ns, max_depth - 1 ) elif isinstance(node.bound, RunnableSequence): for runnable in node.bound.steps: if isinstance(runnable, Pregel): _get_checkpoint_ns_to_graph( - runnable, checkpoint_ns_to_graph, new_checkpoint_ns + runnable, + checkpoint_ns_to_graph, + new_checkpoint_ns, + max_depth - 1, ) checkpoint_ns_to_graph[checkpoint_ns] = graph From 71442916e56247199ff6e15bf6b435d1fb5a30cd Mon Sep 17 00:00:00 2001 From: vbarda Date: Fri, 23 Aug 2024 14:52:31 -0400 Subject: [PATCH 101/287] lint --- libs/langgraph/langgraph/pregel/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 33bae3330..79f7afa05 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -189,7 +189,7 @@ def _get_checkpoint_ns_to_graph( if max_depth <= 0: raise RecursionError( - f"Reached maximum recursion depth while building checkpoint NS -> graph mapping." + "Reached maximum recursion depth while building checkpoint NS -> graph mapping." ) for node_name, node in graph.nodes.items(): From bc86757e734e37df548e30c6066dedc1f0bc6e48 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 12:46:31 -0700 Subject: [PATCH 102/287] lib: Context values never stored in checkpoints - Convert Context to a ManagedValue - Add shim for old Context constructor - Add `runtime` flag for managed values, which, prior to serialization, replaces the value with a placeholder, and replaces it back with the actual value on resuming from checkpoint --- libs/langgraph/langgraph/channels/context.py | 125 +----------------- libs/langgraph/langgraph/constants.py | 2 + libs/langgraph/langgraph/graph/state.py | 33 +++-- libs/langgraph/langgraph/managed/base.py | 38 +++++- libs/langgraph/langgraph/managed/context.py | 85 ++++++++++++ .../langgraph/managed/shared_value.py | 1 - libs/langgraph/langgraph/pregel/__init__.py | 51 +++++-- libs/langgraph/langgraph/pregel/algo.py | 30 ++++- libs/langgraph/langgraph/pregel/manager.py | 44 +++--- libs/langgraph/langgraph/pregel/read.py | 10 +- libs/langgraph/tests/test_channels.py | 84 +----------- libs/langgraph/tests/test_pregel.py | 16 ++- libs/langgraph/tests/test_pregel_async.py | 13 +- 13 files changed, 264 insertions(+), 268 deletions(-) create mode 100644 libs/langgraph/langgraph/managed/context.py diff --git a/libs/langgraph/langgraph/channels/context.py b/libs/langgraph/langgraph/channels/context.py index b48260b40..3b4e26805 100644 --- a/libs/langgraph/langgraph/channels/context.py +++ b/libs/langgraph/langgraph/channels/context.py @@ -1,124 +1,5 @@ -from contextlib import asynccontextmanager, contextmanager -from inspect import signature -from typing import ( - Any, - AsyncContextManager, - AsyncGenerator, - ContextManager, - Generator, - Generic, - Optional, - Sequence, - Type, - Union, -) +from langgraph.managed.context import Context as ContextManagedValue -from langchain_core.runnables import RunnableConfig -from typing_extensions import Self +Context = ContextManagedValue.of -from langgraph.channels.base import BaseChannel, Value -from langgraph.errors import EmptyChannelError, InvalidUpdateError - - -class Context(Generic[Value], BaseChannel[Value, None, None]): - """Exposes the value of a context manager, for the duration of an invocation. - Context manager is entered before the first step, and exited after the last step. - Optionally, provide an equivalent async context manager, which will be used - instead for async invocations. - - ```python - import httpx - - client = Channels.Context(httpx.Client, httpx.AsyncClient) - ``` - """ - - value: Value - - def __init__( - self, - ctx: Union[ - None, Type[ContextManager[Value]], Type[AsyncContextManager[Value]] - ] = None, - actx: Optional[Type[AsyncContextManager[Value]]] = None, - ) -> None: - if ctx is None and actx is None: - raise ValueError("Must provide either sync or async context manager.") - self.ctx = ctx - self.actx = actx - - def __eq__(self, value: object) -> bool: - return ( - isinstance(value, Context) - and value.ctx == self.ctx - and value.actx == self.actx - ) - - @property - def ValueType(self) -> Any: - """The type of the value stored in the channel.""" - return None - - @property - def UpdateType(self) -> Type[None]: - """The type of the update received by the channel.""" - return None - - def checkpoint(self) -> None: - raise EmptyChannelError() - - @contextmanager - def from_checkpoint( - self, checkpoint: None, config: RunnableConfig - ) -> Generator[Self, None, None]: - if self.ctx is None: - raise ValueError("Cannot enter sync context manager.") - - empty = self.__class__(ctx=self.ctx, actx=self.actx) - ctx = ( - self.ctx(config) - if signature(self.ctx).parameters.get("config") - else self.ctx() - ) - with ctx as value: - empty.value = value - yield empty - - @asynccontextmanager - async def afrom_checkpoint( - self, checkpoint: None, config: RunnableConfig - ) -> AsyncGenerator[Self, None]: - empty = self.__class__(ctx=self.ctx, actx=self.actx) - if self.actx is not None: - ctx = ( - self.actx(config) - if signature(self.actx).parameters.get("config") - else self.actx() - ) - else: - ctx = ( - self.ctx(config) - if signature(self.ctx).parameters.get("config") - else self.ctx() - ) - if hasattr(ctx, "__aenter__"): - async with ctx as value: - empty.value = value - yield empty - else: - with ctx as value: - empty.value = value - yield empty - - def update(self, values: Sequence[None]) -> bool: - if values: - raise InvalidUpdateError( - f"At key '{self.key}': Context channel does not accept writes." - ) - return False - - def get(self) -> Value: - try: - return self.value - except AttributeError: - raise EmptyChannelError() +__all__ = ["Context"] diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index 21844a565..e2621f4f6 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -11,6 +11,7 @@ CONFIG_KEY_TASK_ID = "__pregel_task_id" INTERRUPT = "__interrupt__" ERROR = "__error__" TASKS = "__pregel_tasks" +RUNTIME_PLACEHOLDER = "__pregel_runtime_placeholder__" RESERVED = { INTERRUPT, ERROR, @@ -22,6 +23,7 @@ RESERVED = { CONFIG_KEY_RESUMING, CONFIG_KEY_TASK_ID, INPUT, + RUNTIME_PLACEHOLDER, } TAG_HIDDEN = "langsmith:hidden" diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 7228c5fa2..94de80284 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -6,6 +6,7 @@ from functools import partial from inspect import isclass, isfunction, signature from typing import ( Any, + Callable, NamedTuple, Optional, Sequence, @@ -25,7 +26,6 @@ from langchain_core.runnables.utils import ( from langgraph.channels.base import BaseChannel from langgraph.channels.binop import BinaryOperatorAggregate -from langgraph.channels.context import Context from langgraph.channels.dynamic_barrier_value import DynamicBarrierValue, WaitForNames from langgraph.channels.ephemeral_value import EphemeralValue from langgraph.channels.last_value import LastValue @@ -425,16 +425,14 @@ class StateGraph(Graph): else [ key for key, val in self.schemas[self.output].items() - if not isinstance(val, Context) and not is_managed_value(val) + if not is_managed_value(val) ] ) stream_channels = ( "__root__" if len(self.channels) == 1 and "__root__" in self.channels else [ - key - for key, val in self.channels.items() - if not isinstance(val, Context) and not is_managed_value(val) + key for key, val in self.channels.items() if not is_managed_value(val) ] ) @@ -502,7 +500,6 @@ class CompiledStateGraph(CompiledGraph): k: (self.channels[k].UpdateType, None) for k in self.builder.schemas[self.builder.input] if isinstance(self.channels[k], BaseChannel) - and not isinstance(self.channels[k], Context) }, ) @@ -523,7 +520,7 @@ class CompiledStateGraph(CompiledGraph): output_keys = [ k for k, v in self.builder.schemas[self.builder.input].items() - if not isinstance(v, Context) and not is_managed_value(v) + if not is_managed_value(v) ] else: output_keys = list(self.builder.channels) + [ @@ -650,7 +647,14 @@ class CompiledStateGraph(CompiledGraph): return ChannelWrite(writes, tags=[TAG_HIDDEN]) # attach branch publisher - self.nodes[start] |= branch.run(branch_writer, _get_state_reader(self.builder)) + schema = ( + self.builder.nodes[start].input + if start in self.builder.nodes + else self.builder.schema + ) + self.nodes[start] |= branch.run( + branch_writer, _get_state_reader(self.builder, schema) + ) # attach branch subscribers ends = ( @@ -676,16 +680,17 @@ class CompiledStateGraph(CompiledGraph): ) -def _get_state_reader(graph: StateGraph) -> ChannelRead: - state_keys = list(graph.channels) +def _get_state_reader( + builder: StateGraph, schema: Type[Any] +) -> Callable[[RunnableConfig], Any]: + state_keys = list(builder.channels) + select = list(builder.schemas[schema]) return partial( ChannelRead.do_read, - channel=state_keys[0] if state_keys == ["__root__"] else state_keys, + select=select[0] if select == ["__root__"] else select, fresh=True, # coerce state dict to schema class (eg. pydantic model) - mapper=( - None if state_keys == ["__root__"] else partial(_coerce_state, graph.schema) - ), + mapper=(None if state_keys == ["__root__"] else partial(_coerce_state, schema)), ) diff --git a/libs/langgraph/langgraph/managed/base.py b/libs/langgraph/langgraph/managed/base.py index bebca1be2..c5516ca1e 100644 --- a/libs/langgraph/langgraph/managed/base.py +++ b/libs/langgraph/langgraph/managed/base.py @@ -16,11 +16,16 @@ from typing import ( from langchain_core.runnables import RunnableConfig from typing_extensions import Self, TypeGuard +from langgraph.constants import RUNTIME_PLACEHOLDER + V = TypeVar("V") U = TypeVar("U") class ManagedValue(ABC, Generic[V]): + runtime: bool = False + """Whether the managed value is always created at runtime, ie. never stored.""" + def __init__(self, config: RunnableConfig) -> None: self.config = config @@ -74,8 +79,6 @@ class ConfiguredManagedValue(NamedTuple): ManagedValueSpec = Union[Type[ManagedValue], ConfiguredManagedValue] -ManagedValueMapping = dict[str, ManagedValue] - def is_managed_value(value: Any) -> TypeGuard[ManagedValueSpec]: return (isclass(value) and issubclass(value, ManagedValue)) or isinstance( @@ -103,3 +106,34 @@ def is_writable_managed_value(value: Any) -> TypeGuard[Type[WritableManagedValue ChannelKeyPlaceholder = object() ChannelTypePlaceholder = object() + + +class ManagedValueMapping(dict[str, ManagedValue]): + def replace_runtime_values(self, step: int, values: Union[dict[str, Any], Any]): + print("replace_runtime_values", values) + if isinstance(values, dict): + for key, value in values.items(): + for chan, mv in self.items(): + print("chan", chan, "mv", mv, "v", mv(step), "value", value) + print(mv, mv.runtime, mv(step) is value) + if mv.runtime and mv(step) is value: + values[key] = {RUNTIME_PLACEHOLDER: chan} + elif hasattr(values, "__dir__") and callable(values.__dir__): + for key in dir(values): + value = getattr(values, key) + for chan, mv in self.items(): + if mv.runtime and mv(step) is value: + setattr(values, key, {RUNTIME_PLACEHOLDER: chan}) + + def replace_runtime_placeholders( + self, step: int, values: Union[dict[str, Any], Any] + ): + if isinstance(values, dict): + for key, value in values.items(): + if isinstance(value, dict) and RUNTIME_PLACEHOLDER in value: + values[key] = self[value[RUNTIME_PLACEHOLDER]](step) + elif hasattr(values, "__dir__") and callable(values.__dir__): + for key in dir(values): + value = getattr(values, key) + if isinstance(value, dict) and RUNTIME_PLACEHOLDER in value: + setattr(values, key, self[value[RUNTIME_PLACEHOLDER]](step)) diff --git a/libs/langgraph/langgraph/managed/context.py b/libs/langgraph/langgraph/managed/context.py new file mode 100644 index 000000000..381257a87 --- /dev/null +++ b/libs/langgraph/langgraph/managed/context.py @@ -0,0 +1,85 @@ +from contextlib import asynccontextmanager, contextmanager +from inspect import signature +from typing import ( + Any, + AsyncContextManager, + AsyncIterator, + ContextManager, + Iterator, + Optional, + Self, + Type, + Union, +) + +from langchain_core.runnables import RunnableConfig + +from langgraph.managed.base import ConfiguredManagedValue, ManagedValue, V + + +class Context(ManagedValue): + runtime = True + + value: V + + @staticmethod + def of( + ctx: Union[None, Type[ContextManager[V]], Type[AsyncContextManager[V]]] = None, + actx: Optional[Type[AsyncContextManager[V]]] = None, + ) -> ConfiguredManagedValue: + if ctx is None and actx is None: + raise ValueError("Must provide either sync or async context manager.") + return ConfiguredManagedValue(Context, {"ctx": ctx, "actx": actx}) + + @classmethod + @contextmanager + def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]: + with super().enter(config, **kwargs) as self: + if self.ctx is None: + raise ValueError("Cannot enter sync context manager.") + ctx = ( + self.ctx(config) + if signature(self.ctx).parameters.get("config") + else self.ctx() + ) + with ctx as v: + self.value = v + yield self + + @classmethod + @asynccontextmanager + async def aenter(cls, config: RunnableConfig, **kwargs: Any) -> AsyncIterator[Self]: + async with super().aenter(config, **kwargs) as self: + if self.actx is not None: + ctx = ( + self.actx(config) + if signature(self.actx).parameters.get("config") + else self.actx() + ) + else: + ctx = ( + self.ctx(config) + if signature(self.ctx).parameters.get("config") + else self.ctx() + ) + if hasattr(ctx, "__aenter__"): + async with ctx as v: + self.value = v + yield self + else: + with ctx as v: + self.value = v + yield self + + def __init__( + self, + config: RunnableConfig, + *, + ctx: Union[None, Type[ContextManager[V]], Type[AsyncContextManager[V]]] = None, + actx: Optional[Type[AsyncContextManager[V]]] = None, + ) -> None: + self.ctx = ctx + self.actx = actx + + def __call__(self, step: int) -> V: + return self.value diff --git a/libs/langgraph/langgraph/managed/shared_value.py b/libs/langgraph/langgraph/managed/shared_value.py index 7bb6e23b7..f5e0561bd 100644 --- a/libs/langgraph/langgraph/managed/shared_value.py +++ b/libs/langgraph/langgraph/managed/shared_value.py @@ -81,7 +81,6 @@ class SharedValue(WritableManagedValue[Value, Update]): ): raise ValueError("SharedValue must be a dict") self.scope = scope - self.config = config self.value: Value = {} self.store: BaseStore = config["configurable"].get(CONFIG_KEY_STORE) if self.store is None: diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index c06f2bce3..73db1bfb8 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -51,7 +51,6 @@ from typing_extensions import Self from langgraph.channels.base import ( BaseChannel, ) -from langgraph.channels.context import Context from langgraph.checkpoint.base import ( BaseCheckpointSaver, copy_checkpoint, @@ -68,7 +67,12 @@ from langgraph.constants import ( ) from langgraph.errors import GraphInterrupt, GraphRecursionError, InvalidUpdateError from langgraph.managed.base import ManagedValueSpec -from langgraph.pregel.algo import apply_writes, local_read, prepare_next_tasks +from langgraph.pregel.algo import ( + apply_writes, + local_read, + local_write, + prepare_next_tasks, +) from langgraph.pregel.debug import ( print_step_checkpoint, print_step_tasks, @@ -330,10 +334,7 @@ class Pregel( @property def stream_channels_asis(self) -> Union[str, Sequence[str]]: return self.stream_channels or [ - k - for k in self.channels - if isinstance(self.channels[k], BaseChannel) - and not isinstance(self.channels[k], Context) + k for k in self.channels if isinstance(self.channels[k], BaseChannel) ] def get_state(self, config: RunnableConfig) -> StateSnapshot: @@ -564,7 +565,7 @@ class Pregel( # update channels with ChannelsManager(self.channels, checkpoint, config) as ( channels, - _, + managed, ): # create task to run all writers of the chosen node writers = self.nodes[as_node].get_writers() @@ -588,9 +589,22 @@ class Pregel( run_name=self.name + "UpdateState", configurable={ # deque.extend is thread-safe - CONFIG_KEY_SEND: task.writes.extend, + CONFIG_KEY_SEND: partial( + local_write, + step + 1, + task.writes.extend, + self.nodes, + channels, + managed, + ), CONFIG_KEY_READ: partial( - local_read, checkpoint, channels, task, config + local_read, + step + 1, + checkpoint, + channels, + managed, + task, + config, ), }, ), @@ -682,7 +696,7 @@ class Pregel( # update channels, acting as the chosen node async with AsyncChannelsManager(self.channels, checkpoint, config) as ( channels, - _, + managed, ): # create task to run all writers of the chosen node writers = self.nodes[as_node].get_writers() @@ -706,9 +720,22 @@ class Pregel( run_name=self.name + "UpdateState", configurable={ # deque.extend is thread-safe - CONFIG_KEY_SEND: task.writes.extend, + CONFIG_KEY_SEND: partial( + local_write, + step + 1, + task.writes.extend, + self.nodes, + channels, + managed, + ), CONFIG_KEY_READ: partial( - local_read, checkpoint, channels, task, config + local_read, + step + 1, + checkpoint, + channels, + managed, + task, + config, ), }, ), diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 1d3f57040..a1dd5d536 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -24,7 +24,6 @@ from langchain_core.runnables.config import ( ) from langgraph.channels.base import BaseChannel -from langgraph.channels.context import Context from langgraph.checkpoint.base import ( BaseCheckpointSaver, Checkpoint, @@ -95,28 +94,37 @@ def should_interrupt( def local_read( + step: int, checkpoint: Checkpoint, channels: Mapping[str, BaseChannel], + managed: ManagedValueMapping, task: WritesProtocol, config: RunnableConfig, select: Union[list[str], str], fresh: bool = False, ) -> Union[dict[str, Any], Any]: + if isinstance(select, str): + managed_keys = [] + else: + managed_keys = [k for k in select if k in managed] + select = [k for k in select if k not in managed] if fresh: new_checkpoint = create_checkpoint(copy_checkpoint(checkpoint), channels, -1) - context_channels = {k: v for k, v in channels.items() if isinstance(v, Context)} with ChannelsManager(channels, new_checkpoint, config, skip_context=True) as ( channels, _, ): - all_channels = {**channels, **context_channels} - apply_writes(new_checkpoint, all_channels, [task], None) - return read_channels(all_channels, select) + apply_writes(new_checkpoint, channels, [task], None) + values = read_channels(channels, select) else: - return read_channels(channels, select) + values = read_channels(channels, select) + if managed_keys: + values.update({k: managed[k](step) for k in managed_keys}) + return values def local_write( + step: int, commit: Callable[[Sequence[tuple[str, Any]]], None], processes: Mapping[str, PregelNode], channels: Mapping[str, BaseChannel], @@ -131,6 +139,9 @@ def local_write( ) if value.node not in processes: raise InvalidUpdateError(f"Invalid node name {value.node} in packet") + # replace any runtime values with placeholders + managed.replace_runtime_values(step, value.arg) + print("after replace", value) elif chan not in channels and chan not in managed: logger.warning(f"Skipping write for channel '{chan}' which has no readers") commit(writes) @@ -293,6 +304,7 @@ def prepare_next_tasks( if for_execution: proc = processes[packet.node] if node := proc.get_node(): + managed.replace_runtime_placeholders(step, packet.arg) writes = deque() tasks.append( PregelExecutableTask( @@ -317,6 +329,7 @@ def prepare_next_tasks( # deque.extend is thread-safe CONFIG_KEY_SEND: partial( local_write, + step, writes.extend, processes, channels, @@ -324,8 +337,10 @@ def prepare_next_tasks( ), CONFIG_KEY_READ: partial( local_read, + step, checkpoint, channels, + managed, PregelTaskWrites(packet.node, writes, triggers), config, ), @@ -412,6 +427,7 @@ def prepare_next_tasks( # deque.extend is thread-safe CONFIG_KEY_SEND: partial( local_write, + step, writes.extend, processes, channels, @@ -419,8 +435,10 @@ def prepare_next_tasks( ), CONFIG_KEY_READ: partial( local_read, + step, checkpoint, channels, + managed, PregelTaskWrites(name, writes, triggers), config, ), diff --git a/libs/langgraph/langgraph/pregel/manager.py b/libs/langgraph/langgraph/pregel/manager.py index 849395c50..ccfc8de8a 100644 --- a/libs/langgraph/langgraph/pregel/manager.py +++ b/libs/langgraph/langgraph/pregel/manager.py @@ -5,8 +5,6 @@ from typing import AsyncIterator, Iterator, Mapping, Optional, Union from langchain_core.runnables import RunnableConfig, patch_config from langgraph.channels.base import BaseChannel -from langgraph.channels.context import Context -from langgraph.channels.last_value import LastValue from langgraph.checkpoint.base import Checkpoint from langgraph.constants import CONFIG_KEY_STORE from langgraph.managed.base import ( @@ -14,6 +12,7 @@ from langgraph.managed.base import ( ManagedValueMapping, ManagedValueSpec, ) +from langgraph.managed.context import Context from langgraph.store.base import BaseStore @@ -31,10 +30,12 @@ def ChannelsManager( channel_specs: Mapping[str, BaseChannel] = {} managed_specs: Mapping[str, ManagedValueSpec] = {} for k, v in specs.items(): - if skip_context and isinstance(v, Context): - channel_specs[k] = LastValue(None) - elif isinstance(v, BaseChannel): + if isinstance(v, BaseChannel): channel_specs[k] = v + elif ( + skip_context and isinstance(v, ConfiguredManagedValue) and v.cls is Context + ): + managed_specs[k] = Context.of(noop_context) else: managed_specs[k] = v with ExitStack() as stack: @@ -45,14 +46,16 @@ def ChannelsManager( ) for k, v in channel_specs.items() }, - { - key: stack.enter_context( - value.cls.enter(config_for_managed, **value.kwargs) - if isinstance(value, ConfiguredManagedValue) - else value.enter(config_for_managed) - ) - for key, value in managed_specs.items() - }, + ManagedValueMapping( + { + key: stack.enter_context( + value.cls.enter(config_for_managed, **value.kwargs) + if isinstance(value, ConfiguredManagedValue) + else value.enter(config_for_managed) + ) + for key, value in managed_specs.items() + } + ), ) @@ -70,10 +73,12 @@ async def AsyncChannelsManager( channel_specs: Mapping[str, BaseChannel] = {} managed_specs: Mapping[str, ManagedValueSpec] = {} for k, v in specs.items(): - if skip_context and isinstance(v, Context): - channel_specs[k] = LastValue(None) - elif isinstance(v, BaseChannel): + if isinstance(v, BaseChannel): channel_specs[k] = v + elif ( + skip_context and isinstance(v, ConfiguredManagedValue) and v.cls is Context + ): + managed_specs[k] = Context.of(noop_context) else: managed_specs[k] = v async with AsyncExitStack() as stack: @@ -102,5 +107,10 @@ async def AsyncChannelsManager( for k, v in channel_specs.items() }, # managed: build mapping from spec to result - {tasks[task]: task.result() for task in done}, + ManagedValueMapping({tasks[task]: task.result() for task in done}), ) + + +@contextmanager +def noop_context() -> Iterator[None]: + yield None diff --git a/libs/langgraph/langgraph/pregel/read.py b/libs/langgraph/langgraph/pregel/read.py index b5c971e4a..163665e2e 100644 --- a/libs/langgraph/langgraph/pregel/read.py +++ b/libs/langgraph/langgraph/pregel/read.py @@ -67,19 +67,19 @@ class ChannelRead(RunnableCallable): def _read(self, _: Any, config: RunnableConfig) -> Any: return self.do_read( - config, channel=self.channel, fresh=self.fresh, mapper=self.mapper + config, select=self.channel, fresh=self.fresh, mapper=self.mapper ) async def _aread(self, _: Any, config: RunnableConfig) -> Any: return self.do_read( - config, channel=self.channel, fresh=self.fresh, mapper=self.mapper + config, select=self.channel, fresh=self.fresh, mapper=self.mapper ) @staticmethod def do_read( config: RunnableConfig, *, - channel: Union[str, list[str]], + select: Union[str, list[str]], fresh: bool = False, mapper: Optional[Callable[[Any], Any]] = None, ) -> Any: @@ -91,9 +91,9 @@ class ChannelRead(RunnableCallable): "Make sure to call in the context of a Pregel process" ) if mapper: - return mapper(read(channel, fresh)) + return mapper(read(select, fresh)) else: - return read(channel, fresh) + return read(select, fresh) DEFAULT_BOUND: RunnablePassthrough = RunnablePassthrough() diff --git a/libs/langgraph/tests/test_channels.py b/libs/langgraph/tests/test_channels.py index b7d936878..3e0c924ae 100644 --- a/libs/langgraph/tests/test_channels.py +++ b/libs/langgraph/tests/test_channels.py @@ -1,14 +1,9 @@ import operator -from contextlib import asynccontextmanager, contextmanager -from typing import AsyncGenerator, Generator, Sequence, Union +from typing import Sequence, Union -import httpx import pytest -from langchain_core.runnables import RunnableConfig -from pytest_mock import MockerFixture from langgraph.channels.binop import BinaryOperatorAggregate -from langgraph.channels.context import Context from langgraph.channels.last_value import LastValue from langgraph.channels.topic import Topic from langgraph.errors import EmptyChannelError, InvalidUpdateError @@ -257,80 +252,3 @@ async def test_binop_async() -> None: checkpoint, {} ) as channel: assert channel.get() == 10 - - -def test_ctx_manager(mocker: MockerFixture) -> None: - setup = mocker.Mock() - cleanup = mocker.Mock() - - @contextmanager - def an_int() -> Generator[int, None, None]: - setup() - try: - yield 5 - finally: - cleanup() - - with Context(an_int, None).from_checkpoint(None, {}) as channel: - assert setup.call_count == 1 - assert cleanup.call_count == 0 - - assert channel.ValueType is None - assert channel.UpdateType is None - - assert channel.get() == 5 - - with pytest.raises(InvalidUpdateError): - channel.update([5]) # type: ignore - - assert setup.call_count == 1 - assert cleanup.call_count == 1 - - -def test_ctx_manager_ctx(mocker: MockerFixture) -> None: - with Context(httpx.Client).from_checkpoint(None, {}) as channel: - assert channel.ValueType is None - assert channel.UpdateType is None - - assert isinstance(channel.get(), httpx.Client) - - with pytest.raises(InvalidUpdateError): - channel.update([5]) # type: ignore - - with pytest.raises(EmptyChannelError): - channel.checkpoint() - - -async def test_ctx_manager_async(mocker: MockerFixture) -> None: - setup = mocker.Mock() - cleanup = mocker.Mock() - - @contextmanager - def an_int_sync(config: RunnableConfig) -> Generator[int, None, None]: - try: - yield 5 - finally: - pass - - @asynccontextmanager - async def an_int() -> AsyncGenerator[int, None]: - setup() - try: - yield 5 - finally: - cleanup() - - async with Context(an_int_sync, an_int).afrom_checkpoint(None, {}) as channel: - assert setup.call_count == 1 - assert cleanup.call_count == 0 - - assert channel.ValueType is None - assert channel.UpdateType is None - - assert channel.get() == 5 - - with pytest.raises(InvalidUpdateError): - channel.update([5]) # type: ignore - - assert setup.call_count == 1 - assert cleanup.call_count == 1 diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 7325aa41d..ad13e52c0 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -4042,7 +4042,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: ["memory", "sqlite", "postgres", "postgres_pipe"], ) def test_state_graph_packets( - request: pytest.FixtureRequest, checkpointer_name: str + request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture ) -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, @@ -4062,6 +4062,7 @@ def test_state_graph_packets( class AgentState(TypedDict): messages: Annotated[list[BaseMessage], add_messages] + session: Annotated[httpx.Client, Context(httpx.Client)] @tool() def search_api(query: str) -> str: @@ -4117,11 +4118,19 @@ def test_state_graph_packets( ), "nodes can pass extra data to their cond edges, which isn't saved in state" # Logic to decide whether to continue in the loop or exit if tool_calls := data["messages"][-1].tool_calls: - return [Send("tools", tool_call) for tool_call in tool_calls] + return [ + Send("tools", {"call": tool_call, "my_session": data["session"]}) + for tool_call in tool_calls + ] else: return END - def tools_node(tool_call: ToolCall, config: RunnableConfig) -> AgentState: + class ToolInput(TypedDict): + call: ToolCall + my_session: httpx.Client + + def tools_node(input: ToolInput, config: RunnableConfig) -> AgentState: + tool_call = input["call"] time.sleep(tool_call["args"].get("idx", 0) / 10) output = tools_by_name[tool_call["name"]].invoke(tool_call["args"], config) return { @@ -7492,7 +7501,6 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1( return {"answer": ",".join(data.docs)} def decider(data: State) -> str: - print("decider", data) assert isinstance(data, State) return "retriever_two" diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index a35bf7b00..6d0436bb5 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -3892,6 +3892,7 @@ async def test_state_graph_packets() -> None: class AgentState(TypedDict): messages: Annotated[list[BaseMessage], add_messages] + session: Annotated[httpx.Client, Context(httpx.Client)] @tool() def search_api(query: str) -> str: @@ -3938,11 +3939,19 @@ async def test_state_graph_packets() -> None: def should_continue(data: AgentState) -> str: # Logic to decide whether to continue in the loop or exit if tool_calls := data["messages"][-1].tool_calls: - return [Send("tools", tool_call) for tool_call in tool_calls] + return [ + Send("tools", {"call": tool_call, "my_session": data["session"]}) + for tool_call in tool_calls + ] else: return END - async def tools_node(tool_call: ToolCall, config: RunnableConfig) -> AgentState: + class ToolInput(TypedDict): + call: ToolCall + my_session: httpx.Client + + async def tools_node(input: ToolInput, config: RunnableConfig) -> AgentState: + tool_call = input["call"] await asyncio.sleep(tool_call["args"].get("idx", 0) / 10) output = await tools_by_name[tool_call["name"]].ainvoke( tool_call["args"], config From e0898409b9aa4c3f4d8cf10cae57eeb9c716f10b Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 12:57:31 -0700 Subject: [PATCH 103/287] Lint --- libs/langgraph/langgraph/managed/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/managed/context.py b/libs/langgraph/langgraph/managed/context.py index 381257a87..ecea1ce6b 100644 --- a/libs/langgraph/langgraph/managed/context.py +++ b/libs/langgraph/langgraph/managed/context.py @@ -7,12 +7,12 @@ from typing import ( ContextManager, Iterator, Optional, - Self, Type, Union, ) from langchain_core.runnables import RunnableConfig +from typing_extensions import Self from langgraph.managed.base import ConfiguredManagedValue, ManagedValue, V From 1e6da192577060912cc37f2016412bc8cc5135fc Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 13:08:56 -0700 Subject: [PATCH 104/287] Update libs/langgraph/langgraph/managed/context.py Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com> --- libs/langgraph/langgraph/managed/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/managed/context.py b/libs/langgraph/langgraph/managed/context.py index ecea1ce6b..16dadd93b 100644 --- a/libs/langgraph/langgraph/managed/context.py +++ b/libs/langgraph/langgraph/managed/context.py @@ -36,7 +36,7 @@ class Context(ManagedValue): def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]: with super().enter(config, **kwargs) as self: if self.ctx is None: - raise ValueError("Cannot enter sync context manager.") + raise ValueError("Synchronous context manager not found. Please initialize Context value with a sync context manager, or invoke your graph asynchronously.") ctx = ( self.ctx(config) if signature(self.ctx).parameters.get("config") From beafddf7c86eddc3db078fe3016916f792b75d38 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 13:09:16 -0700 Subject: [PATCH 105/287] Lint --- libs/langgraph/langgraph/managed/base.py | 3 --- libs/langgraph/tests/test_pregel.py | 3 +++ libs/langgraph/tests/test_pregel_async.py | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/langgraph/langgraph/managed/base.py b/libs/langgraph/langgraph/managed/base.py index c5516ca1e..ca206464e 100644 --- a/libs/langgraph/langgraph/managed/base.py +++ b/libs/langgraph/langgraph/managed/base.py @@ -110,12 +110,9 @@ ChannelTypePlaceholder = object() class ManagedValueMapping(dict[str, ManagedValue]): def replace_runtime_values(self, step: int, values: Union[dict[str, Any], Any]): - print("replace_runtime_values", values) if isinstance(values, dict): for key, value in values.items(): for chan, mv in self.items(): - print("chan", chan, "mv", mv, "v", mv(step), "value", value) - print(mv, mv.runtime, mv(step) is value) if mv.runtime and mv(step) is value: values[key] = {RUNTIME_PLACEHOLDER: chan} elif hasattr(values, "__dir__") and callable(values.__dir__): diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index ad13e52c0..9f6a0c0e5 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -4106,6 +4106,7 @@ def test_state_graph_packets( ) def agent(data: AgentState) -> AgentState: + assert isinstance(data["session"], httpx.Client) return { "messages": model.invoke(data["messages"]), "something_extra": "hi there", @@ -4113,6 +4114,7 @@ def test_state_graph_packets( # Define decision-making logic def should_continue(data: AgentState) -> str: + assert isinstance(data["session"], httpx.Client) assert ( data["something_extra"] == "hi there" ), "nodes can pass extra data to their cond edges, which isn't saved in state" @@ -4130,6 +4132,7 @@ def test_state_graph_packets( my_session: httpx.Client def tools_node(input: ToolInput, config: RunnableConfig) -> AgentState: + assert isinstance(input["my_session"], httpx.Client) tool_call = input["call"] time.sleep(tool_call["args"].get("idx", 0) / 10) output = tools_by_name[tool_call["name"]].invoke(tool_call["args"], config) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 6d0436bb5..4995addb6 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -3892,7 +3892,7 @@ async def test_state_graph_packets() -> None: class AgentState(TypedDict): messages: Annotated[list[BaseMessage], add_messages] - session: Annotated[httpx.Client, Context(httpx.Client)] + session: Annotated[httpx.AsyncClient, Context(httpx.AsyncClient)] @tool() def search_api(query: str) -> str: @@ -3937,6 +3937,7 @@ async def test_state_graph_packets() -> None: # Define decision-making logic def should_continue(data: AgentState) -> str: + assert isinstance(data["session"], httpx.AsyncClient) # Logic to decide whether to continue in the loop or exit if tool_calls := data["messages"][-1].tool_calls: return [ @@ -3951,6 +3952,7 @@ async def test_state_graph_packets() -> None: my_session: httpx.Client async def tools_node(input: ToolInput, config: RunnableConfig) -> AgentState: + assert isinstance(input["my_session"], httpx.AsyncClient) tool_call = input["call"] await asyncio.sleep(tool_call["args"].get("idx", 0) / 10) output = await tools_by_name[tool_call["name"]].ainvoke( From e8c553c41e0d573458361f503c7791f6d564db3a Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 13:12:46 -0700 Subject: [PATCH 106/287] Lint --- libs/langgraph/langgraph/managed/context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/managed/context.py b/libs/langgraph/langgraph/managed/context.py index 16dadd93b..43cff5e67 100644 --- a/libs/langgraph/langgraph/managed/context.py +++ b/libs/langgraph/langgraph/managed/context.py @@ -36,7 +36,9 @@ class Context(ManagedValue): def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]: with super().enter(config, **kwargs) as self: if self.ctx is None: - raise ValueError("Synchronous context manager not found. Please initialize Context value with a sync context manager, or invoke your graph asynchronously.") + raise ValueError( + "Synchronous context manager not found. Please initialize Context value with a sync context manager, or invoke your graph asynchronously." + ) ctx = ( self.ctx(config) if signature(self.ctx).parameters.get("config") From 155e0c66d5644242ceb8a29370c5f43489cfdd27 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Fri, 23 Aug 2024 13:34:33 -0700 Subject: [PATCH 107/287] docs: add how-to for dynamic interrupts (#1446) --------- Co-authored-by: vbarda --- docs/_scripts/copy_notebooks.py | 1 + docs/docs/how-tos/index.md | 1 + docs/mkdocs.yml | 1 + .../dynamic_breakpoints.ipynb | 417 ++++++++++++++++++ 4 files changed, 420 insertions(+) create mode 100644 examples/human_in_the_loop/dynamic_breakpoints.ipynb diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 2440185f9..caf2c6781 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -56,6 +56,7 @@ _MANUAL = { "create-react-agent-memory.ipynb", "create-react-agent-hitl.ipynb", "human_in_the_loop/breakpoints.ipynb", + "human_in_the_loop/dynamic_breakpoints.ipynb", "human_in_the_loop/time-travel.ipynb", "human_in_the_loop/edit-graph-state.ipynb", "human_in_the_loop/wait-user-input.ipynb", diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 09d5f2e8f..43b0c9265 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -35,6 +35,7 @@ One of LangGraph's main benefits is that it makes human-in-the-loop workflows ea These guides cover common examples of that. - [How to add breakpoints](human_in_the_loop/breakpoints.ipynb) +- [How to add dynamic breakpoints](human_in_the_loop/dynamic_breakpoints.ipynb) - [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb) - [How to wait for user input](human_in_the_loop/wait-user-input.ipynb) - [How to view and update past graph state](human_in_the_loop/time-travel.ipynb) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 3bc3f3d24..1719b1cca 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -139,6 +139,7 @@ nav: - Create custom checkpointer using Redis: how-tos/persistence_redis.ipynb - Human-in-the-loop: - Add breakpoints: how-tos/human_in_the_loop/breakpoints.ipynb + - Add dynamic breakpoints: how-tos/human_in_the_loop/dynamic_breakpoints.ipynb - Wait for user input: how-tos/human_in_the_loop/wait-user-input.ipynb - View and update past graph state: how-tos/human_in_the_loop/time-travel.ipynb - Edit graph state: how-tos/human_in_the_loop/edit-graph-state.ipynb diff --git a/examples/human_in_the_loop/dynamic_breakpoints.ipynb b/examples/human_in_the_loop/dynamic_breakpoints.ipynb new file mode 100644 index 000000000..f43049d28 --- /dev/null +++ b/examples/human_in_the_loop/dynamic_breakpoints.ipynb @@ -0,0 +1,417 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ee54cde3-7e4d-43f4-b921-e7141ea0f19e", + "metadata": {}, + "source": [ + "# How to add dynamic breakpoints" + ] + }, + { + "cell_type": "markdown", + "id": "607849c6-4b8c-4e06-ad9c-758bb5a08e86", + "metadata": {}, + "source": [ + "Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). [Breakpoints](https://langchain-ai.github.io/langgraph/concepts/low_level/#breakpoints) are a common HIL interaction pattern, allowing the graph to stop at specific steps and seek human approval before proceeding (e.g., for sensitive actions).\n", + "\n", + "In LangGraph you can add breakpoints before / after a node is executed. But oftentimes it may be helpful to **dynamically** interrupt the graph from inside a given node based on some condition. When doing so, it may also be helpful to include information about **why** that interrupt was raised.\n", + "\n", + "This guide shows how you can dynamically interrupt the graph using `NodeInterupt` -- a special exception that can be raised from inside a node. Let's see it in action!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2013d058-c245-498e-ba05-5af99b9b8a1b", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -U langgraph" + ] + }, + { + "cell_type": "markdown", + "id": "e9aa244f-1dd9-450e-9526-b1a28b30f84f", + "metadata": {}, + "source": [ + "### Define the graph" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9a14c8b2-5c25-4201-93ea-e5358ee99bcb", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from typing import TypedDict\n", + "from IPython.display import Image, display\n", + "\n", + "from langgraph.graph import StateGraph, START, END\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.errors import NodeInterrupt\n", + "\n", + "\n", + "class State(TypedDict):\n", + " input: str\n", + "\n", + "\n", + "def step_1(state: State) -> State:\n", + " print(\"---Step 1---\")\n", + " return state\n", + "\n", + "\n", + "def step_2(state: State) -> State:\n", + " # Let's optionally raise a NodeInterrupt\n", + " # if the length of the input is longer than 5 characters\n", + " if len(state['input']) > 5:\n", + " raise NodeInterrupt(f\"Received input that is longer than 5 characters: {state['input']}\")\n", + " \n", + " print(\"---Step 2---\")\n", + " return state\n", + "\n", + "def step_3(state: State) -> State:\n", + " print(\"---Step 3---\")\n", + " return state\n", + "\n", + "\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"step_1\", step_1)\n", + "builder.add_node(\"step_2\", step_2)\n", + "builder.add_node(\"step_3\", step_3)\n", + "builder.add_edge(START, \"step_1\")\n", + "builder.add_edge(\"step_1\", \"step_2\")\n", + "builder.add_edge(\"step_2\", \"step_3\")\n", + "builder.add_edge(\"step_3\", END)\n", + "\n", + "# Set up memory\n", + "memory = MemorySaver()\n", + "\n", + "# Compile the graph with memory\n", + "graph = builder.compile(checkpointer=memory)\n", + "\n", + "# View\n", + "display(Image(graph.get_graph().draw_mermaid_png()))" + ] + }, + { + "cell_type": "markdown", + "id": "ad5521e1-0e58-42c5-9282-ff96f24ee6f6", + "metadata": {}, + "source": [ + "### Run the graph with dynamic interrupt" + ] + }, + { + "cell_type": "markdown", + "id": "83692c63-5c65-4562-9c65-5ad1935e339f", + "metadata": {}, + "source": [ + "First, let's run the graph with an input that <= 5 characters long. This should safely ignore the interrupt condition we defined and return the original input at the end of the graph execution." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b2d281f1-3349-4378-8918-7665fa7a7457", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'input': 'hello'}\n", + "---Step 1---\n", + "{'input': 'hello'}\n", + "---Step 2---\n", + "{'input': 'hello'}\n", + "---Step 3---\n", + "{'input': 'hello'}\n" + ] + } + ], + "source": [ + "initial_input = {\"input\": \"hello\"}\n", + "thread_config = {\"configurable\": {\"thread_id\": \"1\"}}\n", + "\n", + "for event in graph.stream(initial_input, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "id": "2b66b926-47eb-401b-b37b-d80269d7214c", + "metadata": {}, + "source": [ + "If we inspect the graph at this point, we can see that there are no more tasks left to run and that the graph indeed finished execution." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4eac1455-e7ef-4a32-8c14-0d5789409689", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "()\n", + "()\n" + ] + } + ], + "source": [ + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.tasks)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f8e03817-2135-4fb3-b881-fd6d2c378ccf", + "metadata": {}, + "source": [ + "Now, let's run the graph with an input that's longer than 5 characters. This should trigger the dynamic interrupt we defined via raising a `NodeInterrupt` error inside the `step_2` node." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c06192ad-13a4-4d2e-8e30-f1c08578fe77", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'input': 'hello world'}\n", + "---Step 1---\n", + "{'input': 'hello world'}\n" + ] + } + ], + "source": [ + "initial_input = {\"input\": \"hello world\"}\n", + "thread_config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "\n", + "# Run the graph until the first interruption\n", + "for event in graph.stream(initial_input, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "markdown", + "id": "173fd4f1-db97-44bb-a9e5-435ed042e3a3", + "metadata": {}, + "source": [ + "We can see that the graph now stopped while executing `step_2`. If we inspect the graph state at this point, we can see the information on what node is set to execute next (`step_2`), as well as what node raised the interrupt (also `step_2`), and additional information about the interrupt." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2058593c-178e-4a23-a4c4-860d4a9c2198", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('step_2',)\n", + "(PregelTask(id='365d4518-bcff-5abd-8ef5-8a0de7f510b0', name='step_2', error=None, interrupts=(Interrupt(value='Received input that is longer than 5 characters: hello world', when='during'),)),)\n" + ] + } + ], + "source": [ + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.tasks)" + ] + }, + { + "cell_type": "markdown", + "id": "fc36d1be-ae2e-49c8-a17f-2b27be09618a", + "metadata": {}, + "source": [ + "If we try to resume the graph from the breakpoint, we will simply interrupt again as our inputs & graph state haven't changed." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "872e7a69-9784-4f81-90c6-6b6af2fa6480", + "metadata": {}, + "outputs": [], + "source": [ + "# NOTE: to resume the graph from a dynamic interrupt we use the same syntax as with regular interrupts -- we pass None as the input\n", + "for event in graph.stream(None, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3275f899-7039-4029-8814-0bb5c33fabfe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('step_2',)\n", + "(PregelTask(id='365d4518-bcff-5abd-8ef5-8a0de7f510b0', name='step_2', error=None, interrupts=(Interrupt(value='Received input that is longer than 5 characters: hello world', when='during'),)),)\n" + ] + } + ], + "source": [ + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.tasks)" + ] + }, + { + "cell_type": "markdown", + "id": "a5862dea-2af2-48cb-9889-979b6c6af6aa", + "metadata": {}, + "source": [ + "### Update the graph state" + ] + }, + { + "cell_type": "markdown", + "id": "c8724ef6-877a-44b9-b96a-ae81efa2d9e4", + "metadata": {}, + "source": [ + "To get around it, we can do several things. \n", + "\n", + "First, we could simply run the graph on a different thread with a shorter input, like we did in the beginning. Alternatively, if we want to resume the graph execution from the breakpoint, we can update the state to have an input that's shorter than 5 characters (the condition for our interrupt)." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "2ba8dc8d-b90e-45f5-92cd-2192fc66f270", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---Step 2---\n", + "{'input': 'foo'}\n", + "---Step 3---\n", + "{'input': 'foo'}\n", + "()\n", + "{'input': 'foo'}\n" + ] + } + ], + "source": [ + "# NOTE: this update will be applied as of the last successful node before the interrupt, i.e. `step_1`, right before the node with an interrupt\n", + "graph.update_state(config=thread_config, values={\"input\": \"foo\"})\n", + "for event in graph.stream(None, thread_config, stream_mode=\"values\"):\n", + " print(event)\n", + "\n", + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.values)" + ] + }, + { + "cell_type": "markdown", + "id": "6f16980e-aef4-45c9-85eb-955568a93c5b", + "metadata": {}, + "source": [ + "You can also update the state **as node `step_2`** (interrupted node) which would skip over that node altogether" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "9a48e564-d979-4ac2-b815-c667345a9f07", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'input': 'hello world'}\n", + "---Step 1---\n", + "{'input': 'hello world'}\n" + ] + } + ], + "source": [ + "initial_input = {\"input\": \"hello world\"}\n", + "thread_config = {\"configurable\": {\"thread_id\": \"3\"}}\n", + "\n", + "# Run the graph until the first interruption\n", + "for event in graph.stream(initial_input, thread_config, stream_mode=\"values\"):\n", + " print(event)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "17f973ab-00ce-4f16-a452-641e76625fde", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---Step 3---\n", + "{'input': 'hello world'}\n", + "()\n", + "{'input': 'hello world'}\n" + ] + } + ], + "source": [ + "# NOTE: this update will skip the node `step_2` altogether\n", + "graph.update_state(config=thread_config, values=None, as_node=\"step_2\")\n", + "for event in graph.stream(None, thread_config, stream_mode=\"values\"):\n", + " print(event)\n", + "\n", + "state = graph.get_state(thread_config)\n", + "print(state.next)\n", + "print(state.values)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "langgraph", + "language": "python", + "name": "langgraph" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From bc482431c3158008bb50d2f687ace64d07788f0c Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 13:35:21 -0700 Subject: [PATCH 108/287] lib0.2.13 --- 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 bc0b67fb4..a447f6d3c 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.12" +version = "0.2.13" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From 610257665f6f29495b265370e78f40288707e280 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Aug 2024 20:36:31 +0000 Subject: [PATCH 109/287] Bump micromatch from 4.0.7 to 4.0.8 in /libs/sdk-js Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.7 to 4.0.8. - [Release notes](https://github.com/micromatch/micromatch/releases) - [Changelog](https://github.com/micromatch/micromatch/blob/4.0.8/CHANGELOG.md) - [Commits](https://github.com/micromatch/micromatch/compare/4.0.7...4.0.8) --- updated-dependencies: - dependency-name: micromatch dependency-type: indirect ... Signed-off-by: dependabot[bot] --- libs/sdk-js/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/sdk-js/yarn.lock b/libs/sdk-js/yarn.lock index 9c46e6ca6..b4e6f252a 100644 --- a/libs/sdk-js/yarn.lock +++ b/libs/sdk-js/yarn.lock @@ -1219,9 +1219,9 @@ micromark@^2.11.3, micromark@~2.11.0, micromark@~2.11.3: parse-entities "^2.0.0" micromatch@^4.0.4: - version "4.0.7" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" picomatch "^2.3.1" From 15692acef9527525b5ad9c997c76fdba50645938 Mon Sep 17 00:00:00 2001 From: vbarda Date: Fri, 23 Aug 2024 17:56:28 -0400 Subject: [PATCH 110/287] refactor to remove nested DB calls --- libs/langgraph/langgraph/pregel/__init__.py | 228 ++++++++++++++------ 1 file changed, 160 insertions(+), 68 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 79f7afa05..20410b21a 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -54,6 +54,7 @@ from langgraph.channels.base import ( from langgraph.channels.context import Context from langgraph.checkpoint.base import ( BaseCheckpointSaver, + CheckpointTuple, copy_checkpoint, create_checkpoint, empty_checkpoint, @@ -216,6 +217,131 @@ def _get_checkpoint_ns_to_graph( return checkpoint_ns_to_graph +def _prepare_state_snapshot( + config: RunnableConfig, + checkpoint_ns_to_graph: dict[str, Pregel], + checkpoint_tuples: Iterator[CheckpointTuple], +) -> StateSnapshot: + checkpoint_ns = config["configurable"].get("checkpoint_ns", "") + checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} + for saved in checkpoint_tuples: + saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] + saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] + if saved_checkpoint_id != config["configurable"]["checkpoint_id"]: + continue + + graph_checkpoint_ns = saved_checkpoint_ns.split( + SEND_CHECKPOINT_NAMESPACE_SEPARATOR + )[0] + graph = checkpoint_ns_to_graph.get(graph_checkpoint_ns) + if graph is None: + continue + + with ChannelsManager( + graph.channels, saved.checkpoint, saved.config, skip_context=True + ) as ( + channels, + managed, + ): + next_tasks = prepare_next_tasks( + saved.checkpoint, + graph.nodes, + channels, + managed, + saved.config, + saved.metadata.get("step", -1) + 1, + for_execution=False, + ) + state_snapshot = StateSnapshot( + read_channels(channels, graph.stream_channels_asis), + tuple(t.name for t in next_tasks), + saved.config, + saved.metadata, + saved.checkpoint["ts"], + saved.parent_config, + tasks_w_writes(next_tasks, saved.pending_writes), + ) + + checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot + + if not checkpoint_ns_to_state_snapshots: + return StateSnapshot( + values={}, + next=(), + config=config, + metadata=None, + created_at=None, + parent_config=None, + tasks=(), + ) + + state_snapshot = assemble_state_snapshot_hierarchy( + checkpoint_ns, checkpoint_ns_to_state_snapshots + ) + return state_snapshot + + +async def _prepare_state_snapshot_async( + config: RunnableConfig, + checkpoint_ns_to_graph: dict[str, Pregel], + checkpoint_tuples: AsyncIterator[CheckpointTuple], +) -> StateSnapshot: + checkpoint_ns = config["configurable"].get("checkpoint_ns", "") + checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} + async for saved in checkpoint_tuples: + saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] + saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] + if saved_checkpoint_id != config["configurable"]["checkpoint_id"]: + continue + + graph_checkpoint_ns = saved_checkpoint_ns.split( + SEND_CHECKPOINT_NAMESPACE_SEPARATOR + )[0] + graph = checkpoint_ns_to_graph.get(graph_checkpoint_ns) + if graph is None: + continue + + async with AsyncChannelsManager( + graph.channels, saved.checkpoint, saved.config, skip_context=True + ) as (channels, managed): + next_tasks = prepare_next_tasks( + saved.checkpoint, + graph.nodes, + channels, + managed, + saved.config, + saved.metadata.get("step", -1) + 1, + for_execution=False, + ) + state_snapshot = StateSnapshot( + read_channels(channels, graph.stream_channels_asis), + tuple(t.name for t in next_tasks), + saved.config, + saved.metadata, + saved.checkpoint["ts"], + saved.parent_config, + tasks_w_writes(next_tasks, saved.pending_writes), + ) + + checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot + + if not checkpoint_ns_to_state_snapshots: + return StateSnapshot( + values={}, + next=(), + config=config, + metadata=None, + created_at=None, + parent_config=None, + tasks=(), + ) + + state_snapshot = assemble_state_snapshot_hierarchy( + checkpoint_ns, checkpoint_ns_to_state_snapshots + ) + return state_snapshot + + def _has_nested_interrupts( graph: Pregel, ) -> bool: @@ -409,7 +535,6 @@ class Pregel( saved = self.checkpointer.get_tuple(config) checkpoint_config = saved.config if saved else config checkpoint_ns = checkpoint_config["configurable"].get("checkpoint_ns", "") - checkpoint_ns_to_checkpoint_id: dict[str, str] = {} checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} checkpoint_ns_to_graph: dict[str, Pregel] = _get_checkpoint_ns_to_graph(self) @@ -421,7 +546,6 @@ class Pregel( for saved in checkpoint_tuples: saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] - saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] graph_checkpoint_ns = saved_checkpoint_ns.split( SEND_CHECKPOINT_NAMESPACE_SEPARATOR @@ -456,7 +580,6 @@ class Pregel( ) checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot - checkpoint_ns_to_checkpoint_id[saved_checkpoint_ns] = saved_checkpoint_id if not checkpoint_ns_to_state_snapshots: return StateSnapshot( @@ -481,72 +604,21 @@ class Pregel( saved = await self.checkpointer.aget_tuple(config) checkpoint_config = saved.config if saved else config - checkpoint_ns = checkpoint_config["configurable"].get("checkpoint_ns", "") - checkpoint_ns_to_checkpoint_id: dict[str, str] = {} - checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} checkpoint_ns_to_graph: dict[str, Pregel] = _get_checkpoint_ns_to_graph(self) # we only lookup subgraph checkpoints if we actually have subgraphs if len(set(checkpoint_ns_to_graph)) == 1: - async def list_checkpoints(): + async def alist_checkpoints(): yield saved - checkpoint_tuples = list_checkpoints() + checkpoint_tuples = alist_checkpoints() else: checkpoint_tuples = self.checkpointer.alist(saved.config) - async for saved in checkpoint_tuples: - saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] - saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] - - graph_checkpoint_ns = saved_checkpoint_ns.split( - SEND_CHECKPOINT_NAMESPACE_SEPARATOR - )[0] - graph = checkpoint_ns_to_graph.get(graph_checkpoint_ns) - if graph is None: - continue - - async with AsyncChannelsManager( - graph.channels, saved.checkpoint, saved.config, skip_context=True - ) as (channels, managed): - next_tasks = prepare_next_tasks( - saved.checkpoint, - graph.nodes, - channels, - managed, - saved.config, - saved.metadata.get("step", -1) + 1, - for_execution=False, - ) - state_snapshot = StateSnapshot( - read_channels(channels, graph.stream_channels_asis), - tuple(t.name for t in next_tasks), - saved.config, - saved.metadata, - saved.checkpoint["ts"], - saved.parent_config, - tasks_w_writes(next_tasks, saved.pending_writes), - ) - - checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot - checkpoint_ns_to_checkpoint_id[saved_checkpoint_ns] = saved_checkpoint_id - - if not checkpoint_ns_to_state_snapshots: - return StateSnapshot( - values={}, - next=(), - config=config, - metadata=None, - created_at=None, - parent_config=None, - tasks=(), - ) - - state_snapshot = assemble_state_snapshot_hierarchy( - checkpoint_ns, checkpoint_ns_to_state_snapshots + return await _prepare_state_snapshot_async( + checkpoint_config, checkpoint_ns_to_graph, checkpoint_tuples ) - return state_snapshot def get_state_history( self, @@ -566,17 +638,24 @@ class Pregel( raise ValueError("Checkpointer does not support filtering") checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - for checkpoint_tuple in self.checkpointer.list( - config, before=before, limit=limit, filter=filter - ): + checkpoint_ns_to_graph = _get_checkpoint_ns_to_graph(self) + # find all matching checkpoint tuples for parent and subgraphs + checkpoint_tuples = [ + checkpoint_tuple + for checkpoint_tuple in self.checkpointer.list( + config, before=before, limit=limit, filter=filter + ) + ] + for checkpoint_tuple in checkpoint_tuples: if ( checkpoint_tuple.config["configurable"]["checkpoint_ns"] != checkpoint_ns ): - # only list root checkpoints here continue - state_snapshot = self.get_state(checkpoint_tuple.config) + state_snapshot = _prepare_state_snapshot( + checkpoint_tuple.config, checkpoint_ns_to_graph, iter(checkpoint_tuples) + ) yield state_snapshot async def aget_state_history( @@ -597,17 +676,30 @@ class Pregel( raise ValueError("Checkpointer does not support filtering") checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - async for checkpoint_tuple in self.checkpointer.alist( - config, before=before, limit=limit, filter=filter - ): + checkpoint_ns_to_graph = _get_checkpoint_ns_to_graph(self) + # find all matching checkpoint tuples for parent and subgraphs + checkpoint_tuples = [ + checkpoint_tuple + async for checkpoint_tuple in self.checkpointer.alist( + config, before=before, limit=limit, filter=filter + ) + ] + + # turn matching checkpoint tuples into an async iterator + async def alist_checkpoints() -> AsyncIterator[CheckpointTuple]: + for checkpoint_tuple in checkpoint_tuples: + yield checkpoint_tuple + + for checkpoint_tuple in checkpoint_tuples: if ( checkpoint_tuple.config["configurable"]["checkpoint_ns"] != checkpoint_ns ): - # only list root checkpoints here continue - state_snapshot = await self.aget_state(checkpoint_tuple.config) + state_snapshot = await _prepare_state_snapshot_async( + checkpoint_tuple.config, checkpoint_ns_to_graph, alist_checkpoints() + ) yield state_snapshot def update_state( From cb30f686428b4c95d85a408525106e8fd785d811 Mon Sep 17 00:00:00 2001 From: vbarda Date: Fri, 23 Aug 2024 18:01:35 -0400 Subject: [PATCH 111/287] remove more reused code --- libs/langgraph/langgraph/pregel/__init__.py | 55 +-------------------- 1 file changed, 2 insertions(+), 53 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 20410b21a..2173cc7d5 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -534,8 +534,6 @@ class Pregel( saved = self.checkpointer.get_tuple(config) checkpoint_config = saved.config if saved else config - checkpoint_ns = checkpoint_config["configurable"].get("checkpoint_ns", "") - checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} checkpoint_ns_to_graph: dict[str, Pregel] = _get_checkpoint_ns_to_graph(self) # we only lookup subgraph checkpoints if we actually have subgraphs @@ -544,58 +542,9 @@ class Pregel( else: checkpoint_tuples = self.checkpointer.list(saved.config) - for saved in checkpoint_tuples: - saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] - - graph_checkpoint_ns = saved_checkpoint_ns.split( - SEND_CHECKPOINT_NAMESPACE_SEPARATOR - )[0] - graph = checkpoint_ns_to_graph.get(graph_checkpoint_ns) - if graph is None: - continue - - with ChannelsManager( - graph.channels, saved.checkpoint, saved.config, skip_context=True - ) as ( - channels, - managed, - ): - next_tasks = prepare_next_tasks( - saved.checkpoint, - graph.nodes, - channels, - managed, - saved.config, - saved.metadata.get("step", -1) + 1, - for_execution=False, - ) - state_snapshot = StateSnapshot( - read_channels(channels, graph.stream_channels_asis), - tuple(t.name for t in next_tasks), - saved.config, - saved.metadata, - saved.checkpoint["ts"], - saved.parent_config, - tasks_w_writes(next_tasks, saved.pending_writes), - ) - - checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot - - if not checkpoint_ns_to_state_snapshots: - return StateSnapshot( - values={}, - next=(), - config=config, - metadata=None, - created_at=None, - parent_config=None, - tasks=(), - ) - - state_snapshot = assemble_state_snapshot_hierarchy( - checkpoint_ns, checkpoint_ns_to_state_snapshots + return _prepare_state_snapshot( + checkpoint_config, checkpoint_ns_to_graph, checkpoint_tuples ) - return state_snapshot async def aget_state(self, config: RunnableConfig) -> StateSnapshot: """Get the current state of the graph.""" From 904a1a34716a9b4c4e78468c5266b1d135b8bd72 Mon Sep 17 00:00:00 2001 From: vbarda Date: Fri, 23 Aug 2024 18:09:10 -0400 Subject: [PATCH 112/287] extra paranoia --- libs/langgraph/langgraph/pregel/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 2173cc7d5..496ec1b30 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -223,11 +223,12 @@ def _prepare_state_snapshot( checkpoint_tuples: Iterator[CheckpointTuple], ) -> StateSnapshot: checkpoint_ns = config["configurable"].get("checkpoint_ns", "") + checkpoint_id = config["configurable"].get("checkpoint_id") checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} for saved in checkpoint_tuples: saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] - if saved_checkpoint_id != config["configurable"]["checkpoint_id"]: + if checkpoint_id and saved_checkpoint_id != checkpoint_id: continue graph_checkpoint_ns = saved_checkpoint_ns.split( @@ -287,11 +288,12 @@ async def _prepare_state_snapshot_async( checkpoint_tuples: AsyncIterator[CheckpointTuple], ) -> StateSnapshot: checkpoint_ns = config["configurable"].get("checkpoint_ns", "") + checkpoint_id = config["configurable"].get("checkpoint_id") checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} async for saved in checkpoint_tuples: saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] - if saved_checkpoint_id != config["configurable"]["checkpoint_id"]: + if checkpoint_id and saved_checkpoint_id != checkpoint_id: continue graph_checkpoint_ns = saved_checkpoint_ns.split( From 134f8faf8c4d095fb3ca3df815d243a4ea806bc0 Mon Sep 17 00:00:00 2001 From: Andrew Nguonly Date: Fri, 23 Aug 2024 17:14:16 -0700 Subject: [PATCH 113/287] Add section about authentication to API reference. (#1458) --- docs/docs/cloud/reference/api/api_ref.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/docs/cloud/reference/api/api_ref.md b/docs/docs/cloud/reference/api/api_ref.md index 0cac27c63..7c2a085f2 100644 --- a/docs/docs/cloud/reference/api/api_ref.md +++ b/docs/docs/cloud/reference/api/api_ref.md @@ -3,3 +3,20 @@ The LangGraph Cloud API reference is available with each deployment at the `/docs` URL path (e.g. `http://localhost:8124/docs`). Click here to view the API reference. + +## Authentication + +For deployments to LangGraph Cloud, authentication is required. Pass the `X-Api-Key` header with each request to the LangGraph Cloud API. The value of the header should be set to a valid LangSmith API key for the organization where the API is deployed. + +Example `curl` command: +```shell +curl --request POST \ + --url http://localhost:8124/assistants/search \ + --header 'Content-Type: application/json' \ + --header 'X-Api-Key: LANGSMITH_API_KEY' \ + --data '{ + "metadata": {}, + "limit": 10, + "offset": 0 +}' +``` From 3a524e0e562207b342c98ca58ed8d9147c806c1c Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 17:55:14 -0700 Subject: [PATCH 114/287] Fix attributeerror --- libs/langgraph/langgraph/managed/base.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/libs/langgraph/langgraph/managed/base.py b/libs/langgraph/langgraph/managed/base.py index ca206464e..fbedf1f3e 100644 --- a/libs/langgraph/langgraph/managed/base.py +++ b/libs/langgraph/langgraph/managed/base.py @@ -117,10 +117,13 @@ class ManagedValueMapping(dict[str, ManagedValue]): values[key] = {RUNTIME_PLACEHOLDER: chan} elif hasattr(values, "__dir__") and callable(values.__dir__): for key in dir(values): - value = getattr(values, key) - for chan, mv in self.items(): - if mv.runtime and mv(step) is value: - setattr(values, key, {RUNTIME_PLACEHOLDER: chan}) + try: + value = getattr(values, key) + for chan, mv in self.items(): + if mv.runtime and mv(step) is value: + setattr(values, key, {RUNTIME_PLACEHOLDER: chan}) + except AttributeError: + pass def replace_runtime_placeholders( self, step: int, values: Union[dict[str, Any], Any] @@ -131,6 +134,9 @@ class ManagedValueMapping(dict[str, ManagedValue]): values[key] = self[value[RUNTIME_PLACEHOLDER]](step) elif hasattr(values, "__dir__") and callable(values.__dir__): for key in dir(values): - value = getattr(values, key) - if isinstance(value, dict) and RUNTIME_PLACEHOLDER in value: - setattr(values, key, self[value[RUNTIME_PLACEHOLDER]](step)) + try: + value = getattr(values, key) + if isinstance(value, dict) and RUNTIME_PLACEHOLDER in value: + setattr(values, key, self[value[RUNTIME_PLACEHOLDER]](step)) + except AttributeError: + pass From 3ac4cdf3d40b034cdf481da68809dd519c9cebb3 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 18:14:40 -0700 Subject: [PATCH 115/287] Fix pydantic model deserialization --- libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py index 608a077cd..d52bfc8c1 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py @@ -51,9 +51,13 @@ class JsonPlusSerializer(SerializerProtocol): if isinstance(obj, Serializable): return obj.to_json() elif hasattr(obj, "model_dump") and callable(obj.model_dump): - return self._encode_constructor_args(obj.__class__, kwargs=obj.model_dump()) + return self._encode_constructor_args( + obj.__class__, method="model_construct", kwargs=obj.model_dump() + ) elif hasattr(obj, "dict") and callable(obj.dict): - return self._encode_constructor_args(obj.__class__, kwargs=obj.dict()) + return self._encode_constructor_args( + obj.__class__, method="construct", kwargs=obj.dict() + ) elif isinstance(obj, pathlib.Path): return self._encode_constructor_args(pathlib.Path, args=obj.parts) elif isinstance(obj, re.Pattern): From a8d860273b35166ebfe2cbc2844b7e02522e33c0 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 18:14:59 -0700 Subject: [PATCH 116/287] Skip runtime value replacement when not needed --- libs/langgraph/langgraph/managed/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libs/langgraph/langgraph/managed/base.py b/libs/langgraph/langgraph/managed/base.py index fbedf1f3e..5ce15aec2 100644 --- a/libs/langgraph/langgraph/managed/base.py +++ b/libs/langgraph/langgraph/managed/base.py @@ -110,6 +110,10 @@ ChannelTypePlaceholder = object() class ManagedValueMapping(dict[str, ManagedValue]): def replace_runtime_values(self, step: int, values: Union[dict[str, Any], Any]): + if not self or not values: + return + if all(not mv.runtime for mv in self.values()): + return if isinstance(values, dict): for key, value in values.items(): for chan, mv in self.items(): @@ -128,6 +132,10 @@ class ManagedValueMapping(dict[str, ManagedValue]): def replace_runtime_placeholders( self, step: int, values: Union[dict[str, Any], Any] ): + if not self or not values: + return + if all(not mv.runtime for mv in self.values()): + return if isinstance(values, dict): for key, value in values.items(): if isinstance(value, dict) and RUNTIME_PLACEHOLDER in value: From 58e139e7fd4122c4656d0099d4a2938769f461d6 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 18:15:04 -0700 Subject: [PATCH 117/287] Lint --- libs/langgraph/langgraph/pregel/algo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index a1dd5d536..29adcef5e 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -141,7 +141,6 @@ def local_write( raise InvalidUpdateError(f"Invalid node name {value.node} in packet") # replace any runtime values with placeholders managed.replace_runtime_values(step, value.arg) - print("after replace", value) elif chan not in channels and chan not in managed: logger.warning(f"Skipping write for channel '{chan}' which has no readers") commit(writes) From 66a13b886512d1590b8f075ac4e4ef6cd2b94929 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 18:15:15 -0700 Subject: [PATCH 118/287] Add test for runtime value replacement with pydantic model --- libs/langgraph/tests/test_pregel_async.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 4995addb6..f59433a07 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -21,6 +21,9 @@ from uuid import UUID import httpx import pytest +from langchain_core.messages import ( + ToolCall, +) from langchain_core.runnables import ( RunnableConfig, RunnableLambda, @@ -3877,6 +3880,12 @@ async def test_prebuilt_tool_chat() -> None: ] +# defined outside to allow deserializer to see it +class ToolInput(BaseModel, arbitrary_types_allowed=True): + call: ToolCall + my_session: httpx.AsyncClient + + async def test_state_graph_packets() -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, @@ -3885,7 +3894,6 @@ async def test_state_graph_packets() -> None: AIMessage, BaseMessage, HumanMessage, - ToolCall, ToolMessage, ) from langchain_core.tools import tool @@ -3941,19 +3949,15 @@ async def test_state_graph_packets() -> None: # Logic to decide whether to continue in the loop or exit if tool_calls := data["messages"][-1].tool_calls: return [ - Send("tools", {"call": tool_call, "my_session": data["session"]}) + Send("tools", ToolInput(call=tool_call, my_session=data["session"])) for tool_call in tool_calls ] else: return END - class ToolInput(TypedDict): - call: ToolCall - my_session: httpx.Client - async def tools_node(input: ToolInput, config: RunnableConfig) -> AgentState: - assert isinstance(input["my_session"], httpx.AsyncClient) - tool_call = input["call"] + assert isinstance(input.my_session, httpx.AsyncClient) + tool_call = input.call await asyncio.sleep(tool_call["args"].get("idx", 0) / 10) output = await tools_by_name[tool_call["name"]].ainvoke( tool_call["args"], config From 3cda14b0694e953127300c93a3e1013986efa9f6 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 18:15:31 -0700 Subject: [PATCH 119/287] lib 0.2.14 --- 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 a447f6d3c..35164c2f4 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.13" +version = "0.2.14" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From c49692d7949b9c3c4d9d0882b7e3d7accd3fd483 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 18:15:48 -0700 Subject: [PATCH 120/287] checkpoint 1.0.5 --- libs/checkpoint/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml index 7ceea436e..cacd30eca 100644 --- a/libs/checkpoint/pyproject.toml +++ b/libs/checkpoint/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint" -version = "1.0.4" +version = "1.0.5" description = "Library with base interfaces for LangGraph checkpoint savers." authors = [] license = "MIT" From ef790a57c6cd67a70f3efceb20040e0efe40fa61 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 18:17:51 -0700 Subject: [PATCH 121/287] Lint --- libs/checkpoint/tests/test_jsonplus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint/tests/test_jsonplus.py b/libs/checkpoint/tests/test_jsonplus.py index d5a53d248..ce604ed06 100644 --- a/libs/checkpoint/tests/test_jsonplus.py +++ b/libs/checkpoint/tests/test_jsonplus.py @@ -122,7 +122,7 @@ def test_serde_jsonplus() -> None: assert dumped == ( "json", - b"""{"path": {"lc": 2, "type": "constructor", "id": ["pathlib", "Path"], "method": null, "args": ["foo", "bar"], "kwargs": {}}, "re": {"lc": 2, "type": "constructor", "id": ["re", "compile"], "method": null, "args": ["foo", 48], "kwargs": {}}, "decimal": {"lc": 2, "type": "constructor", "id": ["decimal", "Decimal"], "method": null, "args": ["1.10101"], "kwargs": {}}, "ip4": {"lc": 2, "type": "constructor", "id": ["ipaddress", "IPv4Address"], "method": null, "args": ["192.168.0.1"], "kwargs": {}}, "deque": {"lc": 2, "type": "constructor", "id": ["collections", "deque"], "method": null, "args": [[1, 2, 3]], "kwargs": {}}, "tzn": {"lc": 2, "type": "constructor", "id": ["zoneinfo", "ZoneInfo"], "method": null, "args": ["America/New_York"], "kwargs": {}}, "date": {"lc": 2, "type": "constructor", "id": ["datetime", "date"], "method": null, "args": [2024, 4, 19], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "time"], "method": null, "args": [23, 4, 57, 51022, {"lc": 2, "type": "constructor", "id": ["datetime", "timezone"], "method": null, "args": [{"lc": 2, "type": "constructor", "id": ["datetime", "timedelta"], "method": null, "args": [0, 86340, 0], "kwargs": {}}], "kwargs": {}}], "kwargs": {"fold": 0}}, "uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "timestamp": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""", + b"""{"path": {"lc": 2, "type": "constructor", "id": ["pathlib", "Path"], "method": null, "args": ["foo", "bar"], "kwargs": {}}, "re": {"lc": 2, "type": "constructor", "id": ["re", "compile"], "method": null, "args": ["foo", 48], "kwargs": {}}, "decimal": {"lc": 2, "type": "constructor", "id": ["decimal", "Decimal"], "method": null, "args": ["1.10101"], "kwargs": {}}, "ip4": {"lc": 2, "type": "constructor", "id": ["ipaddress", "IPv4Address"], "method": null, "args": ["192.168.0.1"], "kwargs": {}}, "deque": {"lc": 2, "type": "constructor", "id": ["collections", "deque"], "method": null, "args": [[1, 2, 3]], "kwargs": {}}, "tzn": {"lc": 2, "type": "constructor", "id": ["zoneinfo", "ZoneInfo"], "method": null, "args": ["America/New_York"], "kwargs": {}}, "date": {"lc": 2, "type": "constructor", "id": ["datetime", "date"], "method": null, "args": [2024, 4, 19], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "time"], "method": null, "args": [23, 4, 57, 51022, {"lc": 2, "type": "constructor", "id": ["datetime", "timezone"], "method": null, "args": [{"lc": 2, "type": "constructor", "id": ["datetime", "timedelta"], "method": null, "args": [0, 86340, 0], "kwargs": {}}], "kwargs": {}}], "kwargs": {"fold": 0}}, "uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "timestamp": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": "model_construct", "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": "construct", "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""", ) assert serde.loads_typed(dumped) == { From 4a4dd16535fe4669b76b3964194a8e24422843c4 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 23 Aug 2024 18:25:56 -0700 Subject: [PATCH 122/287] checkpoint 1.0.6 --- libs/checkpoint/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml index cacd30eca..0cdafc185 100644 --- a/libs/checkpoint/pyproject.toml +++ b/libs/checkpoint/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint" -version = "1.0.5" +version = "1.0.6" description = "Library with base interfaces for LangGraph checkpoint savers." authors = [] license = "MIT" From b7744aff9bf32807478b7207039257e9340f7750 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Sat, 24 Aug 2024 19:24:13 +0200 Subject: [PATCH 123/287] feat(sdk-js): add apiKey property --- libs/sdk-js/src/client.mts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/sdk-js/src/client.mts b/libs/sdk-js/src/client.mts index fbf2dd12c..96654f0f4 100644 --- a/libs/sdk-js/src/client.mts +++ b/libs/sdk-js/src/client.mts @@ -24,6 +24,7 @@ import { interface ClientConfig { apiUrl?: string; + apiKey?: string; callerOptions?: AsyncCallerParams; timeoutMs?: number; defaultHeaders?: Record; @@ -48,6 +49,9 @@ class BaseClient { this.timeoutMs = config?.timeoutMs || 12_000; this.apiUrl = config?.apiUrl || "http://localhost:8123"; this.defaultHeaders = config?.defaultHeaders || {}; + if (config?.apiKey != null) { + this.defaultHeaders["X-Api-Key"] = config.apiKey; + } } protected prepareFetchOptions( From 4b48e71d2c031bdb4fece0b2d412066433929d46 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Sat, 24 Aug 2024 19:24:39 +0200 Subject: [PATCH 124/287] Bump to 0.0.7 --- 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 a10bcdab8..659b087ca 100644 --- a/libs/sdk-js/package.json +++ b/libs/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-sdk", - "version": "0.0.6", + "version": "0.0.7", "description": "Client library for interacting with the LangGraph API", "type": "module", "packageManager": "yarn@1.22.19", From 03785c7d83f764bde65d067c26c5b6dcd4ef50b7 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Sat, 24 Aug 2024 21:34:35 -0700 Subject: [PATCH 125/287] sdk: Add on_completion param --- libs/sdk-py/langgraph_sdk/client.py | 10 ++++++++++ libs/sdk-py/langgraph_sdk/schema.py | 2 ++ 2 files changed, 12 insertions(+) diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index 24b2ef3b0..8578680d7 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -29,6 +29,7 @@ from langgraph_sdk.schema import ( GraphSchema, Metadata, MultitaskStrategy, + OnCompletionBehavior, OnConflictBehavior, Run, RunCreate, @@ -985,6 +986,7 @@ class RunsClient: feedback_keys: Optional[list[str]] = None, on_disconnect: Optional[DisconnectMode] = None, webhook: Optional[str] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> AsyncIterator[StreamPart]: ... @@ -1004,6 +1006,7 @@ class RunsClient: on_disconnect: Optional[DisconnectMode] = None, webhook: Optional[str] = None, multitask_strategy: Optional[MultitaskStrategy] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> AsyncIterator[StreamPart]: """Create a run and stream the results. @@ -1070,6 +1073,7 @@ class RunsClient: "checkpoint_id": checkpoint_id, "multitask_strategy": multitask_strategy, "on_disconnect": on_disconnect, + "on_completion": on_completion, } endpoint = ( f"/threads/{thread_id}/runs/stream" @@ -1092,6 +1096,7 @@ class RunsClient: interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, webhook: Optional[str] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> Run: ... @@ -1125,6 +1130,7 @@ class RunsClient: interrupt_after: Optional[list[str]] = None, webhook: Optional[str] = None, multitask_strategy: Optional[MultitaskStrategy] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> Run: """Create a background run. @@ -1221,6 +1227,7 @@ class RunsClient: "webhook": webhook, "checkpoint_id": checkpoint_id, "multitask_strategy": multitask_strategy, + "on_completion": on_completion, } payload = {k: v for k, v in payload.items() if v is not None} if thread_id: @@ -1268,6 +1275,7 @@ class RunsClient: interrupt_after: Optional[list[str]] = None, webhook: Optional[str] = None, on_disconnect: Optional[DisconnectMode] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> Union[list[dict], dict[str, Any]]: ... @@ -1285,6 +1293,7 @@ class RunsClient: webhook: Optional[str] = None, on_disconnect: Optional[DisconnectMode] = None, multitask_strategy: Optional[MultitaskStrategy] = None, + on_completion: Optional[OnCompletionBehavior] = None, ) -> Union[list[dict], dict[str, Any]]: """Create a run, wait until it finishes and return the final state. @@ -1364,6 +1373,7 @@ class RunsClient: "checkpoint_id": checkpoint_id, "multitask_strategy": multitask_strategy, "on_disconnect": on_disconnect, + "on_completion": on_completion, } endpoint = ( f"/threads/{thread_id}/runs/wait" if thread_id is not None else "/runs/wait" diff --git a/libs/sdk-py/langgraph_sdk/schema.py b/libs/sdk-py/langgraph_sdk/schema.py index c3232c88e..29fa903a9 100644 --- a/libs/sdk-py/langgraph_sdk/schema.py +++ b/libs/sdk-py/langgraph_sdk/schema.py @@ -15,6 +15,8 @@ MultitaskStrategy = Literal["reject", "interrupt", "rollback", "enqueue"] OnConflictBehavior = Literal["raise", "do_nothing"] +OnCompletionBehavior = Literal["delete", "keep"] + All = Literal["*"] From a2cfe694f189e654616ccb338055a28a3027cb53 Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Sun, 25 Aug 2024 20:47:05 -0700 Subject: [PATCH 126/287] Fix import (#1472) --- examples/docs/quickstart.ipynb | 143 ++++++++++++++++++++++++++------- 1 file changed, 113 insertions(+), 30 deletions(-) diff --git a/examples/docs/quickstart.ipynb b/examples/docs/quickstart.ipynb index e8d621a80..84d73c024 100644 --- a/examples/docs/quickstart.ipynb +++ b/examples/docs/quickstart.ipynb @@ -12,30 +12,55 @@ "execution_count": 13, "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain-openai"] + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain-openai" + ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 1, "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\nif not os.environ.get(\"OPENAI_API_KEY\"):\n os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "if not os.environ.get(\"OPENAI_API_KEY\"):\n", + " os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" + ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 2, "metadata": {}, "outputs": [], - "source": ["from langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.graph import END, MessageGraph\n\nmodel = ChatOpenAI(temperature=0)\n\ngraph = MessageGraph()\n\ngraph.add_node(\"oracle\", model)\ngraph.add_edge(\"oracle\", END)\n\ngraph.add_edge(START, \"oracle\")\n\nrunnable = graph.compile()"] + "source": [ + "from langchain_core.messages import BaseMessage, HumanMessage\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "from langgraph.graph import START, END, MessageGraph\n", + "\n", + "model = ChatOpenAI(temperature=0)\n", + "\n", + "graph = MessageGraph()\n", + "\n", + "graph.add_node(\"oracle\", model)\n", + "graph.add_edge(\"oracle\", END)\n", + "\n", + "graph.add_edge(START, \"oracle\")\n", + "\n", + "runnable = graph.compile()" + ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 3, "metadata": {}, "outputs": [ { "data": { - "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCADaAGIDASIAAhEBAxEB/8QAHQABAAIDAAMBAAAAAAAAAAAAAAYHBAUIAgMJAf/EAE4QAAEDAwEDBQsGCQoHAAAAAAECAwQABREGBxIhExZVlNEIFBciMUFRdJO04RU4cXWBsgk1NjdGVmGRoRgjJDIzQlJis9JTVHKDkpWx/8QAGgEBAAIDAQAAAAAAAAAAAAAAAAIDAQQFBv/EADcRAAIBAgIFCQYGAwAAAAAAAAABAgMRBBMhMVFSkQUSFBVBYXGhsSI0YnLB8DIzQmOB4VPC0f/aAAwDAQACEQMRAD8A+qK1pbSVKISlIySTgAVredVl6YgdZR201V+TF49Te+4aqywWC2LsVuUq3RFKMZsklhOT4o/ZVNevTw1NTmm7u2g3cPh8++m1i0+dVl6YgdZR2051WXpiB1lHbVd83rX0bD9gjspzetfRsP2COyuf1rh9yXFG51d8XkWJzqsvTEDrKO2nOqy9MQOso7arvm9a+jYfsEdlOb1r6Nh+wR2U61w+5Lih1d8XkWJzqsvTEDrKO2nOqy9MQOso7arvm9a+jYfsEdlOb1r6Nh+wR2U61w+5Lih1d8XkWJzqsvTEDrKO2nOqy9MQOso7arvm9a+jYfsEdlOb1r6Nh+wR2U61w+5Lih1d8XkWJzqsvTEDrKO2s2HPjXFouxJDUpoHdK2VhYz6Miqu5vWvo2H7BHZW72SR2orWqGmW0MtJu5whtISkf0WP5AK3cNi6WL5ygmmlfTbal9TWxGEyIc69ye0pStk55q9VfkxePU3vuGq709+ILb6s19wVYmqvyYvHqb33DVd6e/EFt9Wa+4K5PKvu8Pmfodnk79RsKUpXlTtEIibaNH3DUc+xRLquVc4JeQ+2xCfcQFtJKnW0uBBQtaQDlCVFWeGM1HtmndE2DXWz2Xqmc3KsrMLfVLQ9Ck8m2nlltt7jimkh4kIGQ3kgnBANRfSvyrYdt/eWkrPqe36buFwnP6hh3mAUW1C91RTLiPnzuuhJ3EqIIWSUoIrQ6cuGs9K7CZ2kLVYdRW3U1mmuJlSmLcVcpEXcFKdchOKBQ85yDhUkDJyDwyBW9lQtZd3b437DTzJXu+/s8C47btx0TdtLXzUUa9ZtdkSV3JTkR9t6KN3eytlSA4MjiPF4+bNRrWfdM6a07bLNPtyJt4iz7xHtipDVul8mEOHKnWlBkh7CeKQjO8TwJxiqcuelLpJtG2xNq0/rOREvuloqbc7fmZL8qa60X0rSOU3lpVlxO62oJVjJCd2rm222WenQ2j5dstMq4p0/fbZcpEC3slx/vdlYCw22OKlJBzujjwrOVSjJLXfv7l9RmVJRb2f9LStVzYvVsiz4vK97SmkvN8sytle6oZG8hYCknB8igCPOKy619hvKNQWiNcW4syEiQneDE+Oph9HEjx21AFJ4eQ1sK0XoZtrShWdsr/Sr64PusesGs7ZX+lX1wfdY9d/kf8dX5f8AaJzsf+UvEnVKUr0B541eqvyYvHqb33DVc2NpD+nLe24kLbXEbSpKhkEFAyDVpzYjc+G/FdBLT7am1gHBwRg//ahrOyS3R2UNN3a9IbQkJSkTeAA4AeStbFYZYqkoc6zTudDC4iNC/O7SsR3P+zMEEaA02CPOLWz/ALafyftmX6gab/8AVs/7atHwVQemL3134U8FUHpi99d+Fc7qyp/m9Tc6ZQ3fJGpYYbjMNssoS002kIQhAwEpAwAB6K9lbLwVQemL3134U8FUHpi99d+FV9T/ALq4Ms6wpbGa2lVp3KcWbtd2KWrU2ob3dHLpIlTGnFR5HJo3W5LjaMJA/wAKRVu+CqD0xe+u/CnU/wC6uDHWFLYyvb7sd0Lqi6v3O8aPsl0uL+7ysuXAacdcwkJG8opJOAAPoArBVsC2aLCQrQWnFBIwkG2M8BnOB4vpJ/fVoeCqD0xe+u/Cngqg9MXvrvwqxclzWhVvUh02g/0+SIzpzS9n0hbE26x2uJaICVFYiwmUtNhR8p3UgDJqRbK/0q+uD7rHr2eCqD0xe+u/Ct7pfSsTSUWSxEdkPd8vmQ65Kc5RallKU+X6EJH2Vu4PB9Ec5OfOclbt2p/Q1sTioVqfMijc0pSt05YpSlAKUpQClKUBzv3AnzZLD69cvfXq6IrnfuBPmyWH165e+vV0RQClKUApSlAKUpQClKUApSlAKUpQHO/cCfNksPr1y99eroiud+4E+bJYfXrl769XRFAKUpQClKUApSlAKUpQClYF7vsHT0Ey573ItZCUhKSta1HyJQhIKlKODwAJ4GoZJ2h3uWom3WNiMxw3XLnKKXD/ANttKsfarP7KtjTlJX1Lv0FsKU6n4UWFXEX4TvYWrV2gbftFtkcLuenR3tP3B4zkJa/FPpPJuKzj0OrJ8ldJnWers8I1lx/1PVg3u86h1JZp9puVusMy3TmFxpMdwvFLra0lKkn9hBIqWUt5cS7olbYfNz8HhsRc2pbc4l/ltL+Q9JKbubrg4BUoKzGbz6d9JX9DRHnr691zP3PmzOf3OehnNN2BFsmJfluTJE2WXOVeWrAGd0AAJQlKQBw4E+UmrO556u/5ayf+T1Mpby4jolbYWVSq4b11qhjxnrXapaQOKGZTjSj9GUKH78fZ5ak+nNawdQvKi7jsC5ISVqgygEuFIIBUggkLTxHFJOMjOCcVh0pJXVn4P7ZXOhUpq8kSClKVSUClKUArwddQw0txxQQ2gFSlKOAAPKTXnUY2nvLY2dakW2d1XeDwKh/dBQQT9gJNWU4Zk4w2uxlK7sQtma5qeb8uygcuhQhMqOQxHJ8XA8y1jdUo+XJCckJFZtfiUpQkJSAlIGAAMACqu2i6i1FctpWm9Dadu/NszYEq6zbsmM2+8GmlNtpaaS4CjJU7klSTgJ4eWqqk8yV/tI9OkqUUki0qVzQjafry6z9P6XY1AxFu7OsZumrhdkQW1CWw1DU+l0NkFKHN1SeCTjfTxBSSk5+1zWOrNLPvWvTesb5dr3ZbP39Njw7FCfQTlxSXZjq+TShKwnAQ1heEKUAc1XYjnKzdjoisQXeCbqbX37H+UwwJJhcqnlg0Vboc3M53d4Eb2MZGKpKza91btj1HBtdjvidGRI+nLde50mPDakvvPzEqUhpAeCkpbSEHJwVEkDI8te2TcX9IbdrjPucg3aTbNnaZEl9DQaMhTcpxSiEDITvbp4DOM0M5ielai86xp8ITWk7rq40hpXKMSWSA4w5ggLSfTxIwcggkEEEg867Ode7W9SSdI39VuvE21XpyO9OiSIVtZt0aI8kHlI7qJBkHcCkkb4JWAcpSTgdJ1JNxd1rJRkqi1Ex0bqJWpbKmQ8hDM5lxUeU02cpQ6k8cf5SMKGeOFDPGt7Vf7NFqTqDVTSf7LlIzpx/xC1uq+3dQj+FWBWzVSUtHbZ8Vc85WgoVHFClKVSUisW625m8WuZAkAliUythwDy7qklJ/gayqVlNp3QKjtC5DbK4M3hcYSjHkAnipSeAcH+VYwsfsV6Qaj2vNmUDXkq1z1XC5WO82sud53W0PJakNJcADiPHSpKkKwnIUk8UgjBFW3qnR7V/WiZGeEC7NI3G5YRvpUjidxxORvoySQMggk4IycwyTF1Da1FEvT78sDH8/bHW3W1ft3VKSsfRun6TVsqeY+dTtp7NXC+tHdpYmnUjabsyB2XYXp2w821RXrgX7JcpF3Eh58OOzZT7S23XZCinKyQ4Tw3eIT5hiv3VmxO0as1FcLsu63q1G6RW4V0iWyWGWbg0jeCEu+KVAgLUnKFIODjNTP5Qn/q5euqfGnyhP/Vy9dU+NR6PV2GxzqNrXRXa+56s7LFhVbdQahsdys9tRaG7rbpbbciRER/ZtPZbKFhPmO4FD01vEbI7SnUlivip91euFrtqrS4t+VyguMYg+JKCgeUwo7+eB3vKSOFZ2kNoUPX1iZvWnrddLta3lrbblR4uUKUhRQsDJ8ykkfZW6+UJ/6uXrqnxp0ersClRXaiE6J2I2/QFyiOWrUepBZ4SnDEsD1wC4EcKChuhO5vqSN47qVrUAcEDgKsGVJahRnZEhxLLDSC444s4SlIGSSfMAKx23bxKO7G0xdVrI4cslplP2lax/AGpFYdCyX5TU2/rZWWlhxi2xyVMoUDlK3FEAuKHlAwEpPHCiEqBUXHTUdl58CuWIpUo+y7mbs5s0i3WiTNmNLYmXOQZS2XP6zSN1KG0H0EIQkkeZSlfSZXSlYnLnyucCUnOTk+0UpSoERSlKAUpSgFKUoDnfuBPmyWH165e+vV0RXO/cCfNksPr1y99eroigFKUoBSlKAUpSgFKUoBSlKAUpSgOd+4E+bJYfXrl769XRFc79wJ82Sw+vXL316uiKAUpSgFKUoBSlKAUpWHNvNvtriW5c6NFWobwS88lBI9OCayk5OyBmUrV86rL0xA6yjtpzqsvTEDrKO2p5c91mbM2lc391l3XNy7l24WHe0Hzls92aXuXBN2715N9B8ZpSOQX/AHVIUDvDOVDHik1fnOqy9MQOso7aqXupdn2ntvexe+aZFzthuqUd+Wp1clscnLbBKOOeAUCpsnzBw0y57rFmcl9w/wB2RPg8ztj9u0Aq6vS7k9yl1RdtzkWXX1vOulrkTkNoUo43xvbnmzX0irgD8Gfsfg6Mtl72halcYt95mqVbLdGmuJbcaYSocs5uqOQVrSEjIBAbV5lV3XzqsvTEDrKO2mXPdYszaUrV86rL0xA6yjtpzqsvTEDrKO2mXPdYszaUrWt6ltDziG27rCW4shKUpkIJJPkAGa2VRcXHWjApSlRAqrNYwIs/aY+JMZmQE2iPu8q2FY/nn/JmrTqtNTfnMk/VEb/WkUm3GjUa2fVHO5RbWEm13eqMLm9a+jYfsEdlOb1r6Nh+wR2VsKV5zNqbz4ng+fLaa/m9a+jYfsEdlOb1r6Nh+wR2Vj6r1fZ9D2Zy63ye3b4KFJRyiwVFS1HCUISkFS1E+RKQSfMKjLO3fQjunJl9OoG2LbCksxJa5LDrLkZ11SUth1taAtsKKh4ykgYyc4BNSVSq9Kb8yxZsldX8yXc3rX0bD9gjspzetfRsP2COytDpvaxpTVce7vQLqEJtCA5PTOYdhrjNlJUHFpeShQQUpUQvG6QDg8Khth7oW0642rad05peU1cbVNts2ZKfdhyGXAW1MhotFwJCm1b7njAKB3RgjBzlSradL0eJJQrO+vRr1+JaHN619Gw/YI7Kc3rX0bD9gjsrYUqGbU3nxKefLaRzUNmt8WLDdZgxmnU3GDhaGUpI/pTXkIFXXVQao/F8X6xg+9tVb9d7DSlLCpyd/al6RPY8jtvDO+8/RClKVadwVWmpvzmSfqiN/rSKsuq01N+cyT9URv8AWkVGp+RV8Pqjm8o+6VP49UedKjWqdmmktcSmZOodNWq9yGUcm27cIbbykJzndBUDgZOa0v8AJ/2Z4A5g6cwOOPkxnH3a82ub2s8KlC2lvh/ZGO6S0ncr5H0bd4cK7XWBYrx33cIFikuMTVsqZcaLjKm1JWVoKwd1JBIKhUIv+iLfdtC3K66a01rNNzmX2ytSFakMx+XJYjzGnN9KH1rcS2gLcySE4wo+TjV/aU0BpnQolDTlgttiErdL4t8VDPK7ud3e3QM43lYz6TW/qxVOakl2GxHEOCUY6l/HbfSvE5u257PNRay1dtFYs1tkPCfo23tMrKChmW81OfdXHDhG7vqb8XGeAcGcA1ubLqGVr7bhoa7RdI6ksVtt1kuTEhd3tTkVtlxao261kjGfEVgjgceKTg4visC+WK3amtUi2XaDHudukAB6LLaDjbgBBG8k8DxAP2UzNFmvvUFiPZUWvtqzM+lQFGwHZo2cp0DpxJwRkWxkcCMEf1fRXut+w3Z3aZ8adC0Pp+JMjOpeYfZtzSVtrSQUqSQnIIIBBHoqv2dpRantfD+yQ6o/F8X6xg+9tVb9VBqj8XxfrGD721Vv138J7qvml6RPXcje7P5n6IUpSrzuiozqHQEDUV2FydlTokoMJjlUN/kwpCVKUARg+dav31JqVKMnHUYaUlZq6IT4KoPTF7678KeCqD0xe+u/CptSpZj7uCKsmluLgiE+CqD0xe+u/Cngqg9MXvrvwqbUpmPu4IZNLcXBEJ8FUHpi99d+FPBVB6YvfXfhU2pTMfdwQyaW4uCIT4KoPTF7678KeCqD0xe+u/CptSmY+7ghk0txcEQkbJ7YXWFu3G7SEsvNvht6XlBUhYWnIxxGUiptSlYlNyVmWRjGCtFWFKUqBI//2Q==", + "image/jpeg": "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", "text/plain": [ "" ] @@ -44,42 +69,90 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[HumanMessage(content='What is 1 + 1?', id='bb29f237-0e4d-4354-92e1-d46434c67fe7'),\n", - " AIMessage(content='1 + 1 equals 2.', response_metadata={'token_usage': {'completion_tokens': 8, 'prompt_tokens': 15, 'total_tokens': 23}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-2ff0112a-9402-44a1-a992-c44fb49fa894-0', usage_metadata={'input_tokens': 15, 'output_tokens': 8, 'total_tokens': 23})]" + "[HumanMessage(content='What is 1 + 1?', id='28f82989-8a35-4c1e-b12d-aa1b54c2b5ea'),\n", + " AIMessage(content='1 + 1 equals 2.', response_metadata={'token_usage': {'completion_tokens': 8, 'prompt_tokens': 15, 'total_tokens': 23}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-aebd1367-b64d-4c25-971e-db7c88d55aac-0', usage_metadata={'input_tokens': 15, 'output_tokens': 8, 'total_tokens': 23})]" ] }, - "execution_count": 17, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], - "source": ["runnable.invoke(HumanMessage(\"What is 1 + 1?\"))"] + "source": [ + "runnable.invoke(HumanMessage(\"What is 1 + 1?\"))" + ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 5, "metadata": {}, "outputs": [], - "source": ["from typing import Literal\n\nfrom langchain_core.tools import tool\n\nfrom langgraph.graph import END, START\nfrom langgraph.prebuilt import ToolNode\n\n\n@tool\ndef multiply(first_number: int, second_number: int):\n \"\"\"Multiplies two numbers together.\"\"\"\n return first_number * second_number\n\n\nmodel = ChatOpenAI(temperature=0)\nmodel_with_tools = model.bind_tools(tools=[multiply])\n\ngraph = MessageGraph()\n\ngraph.add_node(\"oracle\", model_with_tools)\n\ntool_node = ToolNode([multiply])\ngraph.add_node(\"multiply\", tool_node)\ngraph.add_edge(START, \"oracle\")\ngraph.add_edge(\"multiply\", END)\n\n\ndef router(state: list[BaseMessage]) -> Literal[\"multiply\", \"__end__\"]:\n tool_calls = state[-1].additional_kwargs.get(\"tool_calls\", [])\n if len(tool_calls):\n return \"multiply\"\n else:\n return END\n\n\ngraph.add_conditional_edges(\"oracle\", router)\nrunnable = graph.compile()"] + "source": [ + "from typing import Literal\n", + "\n", + "from langchain_core.tools import tool\n", + "\n", + "from langgraph.graph import END, START\n", + "from langgraph.prebuilt import ToolNode\n", + "\n", + "\n", + "@tool\n", + "def multiply(first_number: int, second_number: int):\n", + " \"\"\"Multiplies two numbers together.\"\"\"\n", + " return first_number * second_number\n", + "\n", + "\n", + "model = ChatOpenAI(temperature=0)\n", + "model_with_tools = model.bind_tools(tools=[multiply])\n", + "\n", + "graph = MessageGraph()\n", + "\n", + "graph.add_node(\"oracle\", model_with_tools)\n", + "\n", + "tool_node = ToolNode([multiply])\n", + "graph.add_node(\"multiply\", tool_node)\n", + "graph.add_edge(START, \"oracle\")\n", + "graph.add_edge(\"multiply\", END)\n", + "\n", + "\n", + "def router(state: list[BaseMessage]) -> Literal[\"multiply\", \"__end__\"]:\n", + " tool_calls = state[-1].additional_kwargs.get(\"tool_calls\", [])\n", + " if len(tool_calls):\n", + " return \"multiply\"\n", + " else:\n", + " return END\n", + "\n", + "\n", + "graph.add_conditional_edges(\"oracle\", router)\n", + "runnable = graph.compile()" + ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { - "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAEuAIkDASIAAhEBAxEB/8QAHQABAAIDAQEBAQAAAAAAAAAAAAYHBAUIAwkBAv/EAFEQAAEDBAADAgkGCAgNBQAAAAECAwQABQYRBxIhEzEIFBUWIkFVlNEXN1FhdZMkMkJWcYGy0gkjNVJzkbG0MzY4Q1NUYnKWo7PB1DSEkpWi/8QAGwEBAAIDAQEAAAAAAAAAAAAAAAEDAgQFBgf/xAAzEQACAQICBwUIAgMAAAAAAAAAAQIDEQSREhMVITFBURRSYaHRBUJiY3Gx4fAiMzKywf/aAAwDAQACEQMRAD8A+qdKUoBSlKAV4ypjEFhT0l9uOynW3HVhKR9HU17VCuLrSHsTYbcQlxtVzghSVDYI8Zb6EVnBJySZnCOlJR6kg86rJ7Yge9I+NPOqye2IHvSPjVeeb9r9mw/uE/Cnm/a/ZsP7hPwrjbVw/clmjrbO+LyLD86rJ7Yge9I+NPOqye2IHvSPjVeeb9r9mw/uE/Cnm/a/ZsP7hPwptXD9yWaGzvi8iw/Oqye2IHvSPjTzqsntiB70j41Xnm/a/ZsP7hPwp5v2v2bD+4T8KbVw/clmhs74vIsPzqsntiB70j4086rJ7Yge9I+NV55v2v2bD+4T8Keb9r9mw/uE/Cm1cP3JZobO+LyLD86rJ7Yge9I+NPOqye2IHvSPjVeeb9r9mw/uE/Cnm/a/ZsP7hPwptXD9yWaGzvi8i1WXm5DSHWlpdaWApK0HaVD1EEd9f3UV4VgDhrjAHQeTmP2BUqrtVI6E5Q6M47VnYUpSqyBSlKAVDOLH+K8b7Ug/3lupnUM4sf4rxvtSD/eW6zh/ki2l/ZH6o1VKwL3frZjNseuV4uMS1W9nl7SXOfSy0jZCRzLUQBskAbPeRUXTxy4cLOk8QMWJ0ToXqN3Dv/Lr58oylvSPWOSXFkpvl5iY5Zbhdp7vYwYEdyVId0TyNoSVKOh1OgDVP5l4Qs1jgtk2ZWLEr5Cfgwm5UI3qI2hp5DoJQ8OV70kADahsKAKfR61MpPF/Br5FkW+05TjGQ3OS0tqNaUXiMozHCk8rOuY/jH0e49/caqC38HswvOB8S8djWV3DMeu9pTHs+O3C6omojzNLLimlIUoNMq/iwEb6HZCU91bNKEVvqK29cehRUlJ7oeJcMridLg4nBvDuEZS7KkvdgLRHjMOy0aBPOvleLaUEJ6Er9YHedVppfhF41Fxax3sQLy+Ltd1WJFvahjxxiakOczLrZUOVQU0pPQnqUnu2RGM6tOa8Q7ViEq64JKXbYMt0XjERd4wVOHYpDLpWHA2ttDnMezUob6Eg61Ufwvg9lVmj45GcxiPaI8DiA9fzGhzGnGI8F2K8E8h2knkW4lspCQdjYBT1rNU6ejeT3/X6mLnO9lw+hM5XHC/N8WsfxxOD3xEC4Wd6c8wtEXxppwSGmwtR8Z5A2hKiVa2r00a31Aueqp4gWXJrTxbxzM7DYDk0Zm1S7RLhszGo7rXaOsuodBdUlKk7aIIB2NggGpC9xu4dRnnGXs+xhp1tRQttd5jBSSDoggr6GqZx0lFwWRZGWi2pMmtKg5458NwfnBxX/wC6jfv1Mocxi4RGJUV9uTFfQl1p9lYWhxChtKkqHQggggiqXGUeKLVJPgyS8LPm2xj7OY/YFSmotws+bbGPs5j9gVKa+i4j+6f1f3PIS4sUpSqDEUpSgFQzix/ivG+1IP8AeW6mdarJcci5ValQJinm2i426FsL5FpUhYWkg/pArODSkrmcJaMlJ8iFONodSUrSFpPeFDYrx8Qi/wCrM/8AwFbr5KYPti9++n4U+SmD7Yvfvp+Fee2R81ZM7nb6XRmnTCjoUFJYaSodQQgAivatl8lMH2xe/fT8KfJTB9sXv30/Cmx/mrJjaFLozW0qFeEXYXuGnBDMcosl7uzd1tcFT8dT0rnQFhQHVOuvfUww/h3HvWJWS4SbzeTJlwWH3SmZoFam0qOhrp1Jpsf5qyZO0KXRnrXgYEYnZjtE/wC4K3PyUwfbF799Pwp8lMH2xe/fT8KbI+asmRtCl0ZpRb4oH/pmfuxXulISAAAAOgA9VbP5KYPti9++n4U+SmD7Yvfvp+FNkfNWTHb6XRmbws+bbGPs5j9gVKawbHZ4+P2eFbInMIsRlLDXOrmVypGhs+s1nV6OrJTqSkuDbOC3d3FKUqogUpSgFKUoBSlKAUpSgKY8Mr/Je4j/AGWr9pNWLw5+b3F/suL/ANJNV14ZX+S9xH+y1ftJqxeHPze4v9lxf+kmgJFSlKAUpSgFKUoBSlKAUpSgFKUoBSlKAUpSgKY8Mr/Je4j/AGWr9pNWLw5+b3F/suL/ANJNUZ4YfGHApXAPiRjrOb449kAhORTaW7tHVLDyVgKb7IL5ucEEFOtjRq0ODHEjEstw/HrfY8pst6uDNqZLsW33Bl91vs22kucyEKJHKXEA7HQrSD3igLFpSlAKUpQClKUApSlAKUpQClKUApStHkmYQMZ7Jt8PSprwJZhREc7zgHedbASn/aUUp3ob2RWUYubtElJydkbylVy5nmSyTzMWi2wm/UJMtbrn6wlAAP1BR/TXn55Zd/q9l/req3VdZLP0NpYWs/dPn1/CZ8CxhHEuJn1rjdnZ8m2mXyD0W56B6R+rtEAK+tSXDVl/wV3BmTb4eQ8T5yXGUTmlWa2pPQOtBaFvufWOdttIP0ocFdBcZ8QncdsAm4jksa2Jt8lbbofhrWh9hxCgUrbUpKgFd46g7ClD11usEReeHGG2bGLJCszNqtUZEVhK1OlRSkfjKI1tSjtROupJNNUu8syeyVuhdFKrXzyy7/V7L/W9XszxAyCIsGZY4k1nfU2+WUugfUhxISf1rHwaq/CSz9SHhay90sSlavH8lgZNFW9BdUVNnkdYdQW3WVfzVoPUH6PUR1BI61tKqlFxdpLearTTsxSlKxIFKUoBSlKAUpSgNVlN+TjVhl3BTfbLbAS0zvXauqUEto36uZakjf11XsOM42XZEp0yZ8g88h9X5SvoT9CBshKfUPrJJ3vFVauyxpr/ADTt3SHPo0mO+tP/AO0IrV1bP+FOKXvb/wDiXkzs4GC0XPmKVz/fWbnn+bcVPG8uvONsYk0w1bGLVOVFbaKogkGS8kdHdqUQAvaeVBGt7Nafh5cb3xkz6yPXq/3u2Qn8EtV2kW61z3Ybbktx5/bv8WQUghPcCOYcoOwkCtU3tbvtY6YpXKWRZdfhmMHMsbl5CMeVl7Fmefud9JiyUqlCO82zACCkNg8wSsqSsFO9Gp1hFqn8XMozS8XXK7/bTZcikWmFa7RcFRWI7Mfl5VONp6Oqc2Vkr2NKAAFLEKrd2SLesGVWvJ3Lqi2SvGVWuau3TB2a0dlIQlKlI9IDegtJ2Njr399bWuWbpmV2wnAOLcuyOpiz5PEI2/xxbgbEVDwhtqcKylQRoKICilXKSDo60bD4TYfxAxjMn13iQpOMOwVJXEm5I9eXxKC0lDiFux21ISU9oFJ2Rvl0BqgjVu7WLYlLk2x9N2tyVKuEZO+xSrlElsdS0r1HfXlJ/FVo/SDZ9suMe8W2JPiOB2LKaQ+0sflIUAUn+oiq7rfcJlqVg0VJ/FakzGW/6NEp1KP1cqU1tL+VK75NLO/oaGOglaaJhSlKqOQKUpQClKUApSlAR3PrG/fcbdRETzz4ziJcZO9c7jagrk36ucBSN/Qo1DYUxufFbkMklCx3HoUnuKSPUQdgj1EEValQ3JMHeeluXGxvMRZjh5n4r6SGJJ/nEpG0L/2wFbHek9CLVapHQbs1w9DoYXEKk9GXBlW5jwVwzP7r5SvtlTMmlpLDriJDzIfaBJCHUtrSHUgk+isKHWt7Cw2zW7InL7FgoYubkFq2l1tSgkRm1KU22Eb5QAVq6gb663oCsDOc7b4W2F685hbpFktjK0trmlxp1kqUdJCSlfMST3ApB+rVbVi7zZLDbzeOXtTbiQtJMTWwRsdCajs9XkvNHVVSjxTRDrh4PnD+6XGZOk4+FyJUnx1fLLfShEjmCy82gOBLThUNlaAlR67PU7ysg4HYRlGRu364WMLuj3J27rEp5hMjk/ELqG1pS7rQA5weg1Uq8oXD82737qP3qeULh+bd791H71Oz1eg06HVeRpJHCnEpczIpL9jjvLyJtLV1Q4VKalhI0kqbJ5ebX5QAV0HXoK/MG4VYxw4XKXj9vciOSUIbdcflvyVlCN8iAp1aiEjmVpI0Bs1mZDmHmrZJ14u1lvEK2QWlPyZK4m0tNpG1KOiToDqa98Yvc7O7NBuuN2hydbJzYej3GRIaajLQe5WwpS/6kHuO9U7PU5rzXqNZRW+6M65zHIkb8Ha8YmOqDUaPvRddP4qf0esn1AE9wqxcXsicbx632xLheMZlLa3T3uL16Sz9albP661eLYZ5If8AH7i+i4XUpKUuJb5G46T3paSSSN+tRJKvqGkiUVLajHQi79f3wOPiq6rO0eCFKUqo0hSlKAUpSgFKUoBWqynKrPhFgmXu/XGPabTDRzvzJSwhtsbAGyfWSQAO8kgCs24XGJaYT0ydJZhRGU87siQ4G220/SpR6Afpquzj974kZLldnz7ErDIwOJJirsaXV+MvS3EacU84k+ikBRSkJ0D6KweZJ2QM6wWPJr1luTTskulmvOES/FVWC0x4oc7NKAHC+64oektSyCNbACEKGjU+r8A0NDur9oBSlKA/l1pDza23EJcbWClSFDYUD3giq5uqr3w9y+RfZl/stq4SQLHyPW56OGHLe80r0VtrSNFtSDylJ7uRISOpqyKx7hb4t2gSYM2O1LhyW1MvR30BbbqFDSkqSehBBIINAfluuMW72+NOgyWpkKS2l5iQwsLbdQobSpKh0IIIIIrJqtbW3feHmYxLFAsNitXCCBYytE9uR2DsB9pXVC0H0S2UHfN01yrKlb0DY7TqHm0ONrS42sBSVpOwoHuINAf3SlKAUpSgFKUoBSlKAqLIbB8vs7L8KzXCJkLCbZLhmJPfmls3Z1BDqylts/4EHkGyo72egUk8tssMNRWG2WW0MstpCENtpCUpSBoAAdwA9VQPgdaptmwRMa4ZojPpHjslflht3tAUl1RDW+ZX+DHo636u4VYFAKUpQClKUApSlAazJsatmZY9cbHeYbdwtVwYXGkxnR6Ljahoj6R9RHUHqOtQXDRO4b5VYuG1rxCccGh2MGFknjnbpadbVylh4L9JJKSkpIJ31AACTy2bVf8AE+1TblfsEciZojFG4t5Q8/DW7yG8I5D+CgcyeYnv1pXd3UBYFKUoBSlKAUrzfktRWy486hlsd63FBI/rNa45ZYwf5Zt/vSPjWahKXBA2tck+G54T/Ezwabvj0rGrJj9wxa6MKaVKucaQ663MQolSCW3kJCS2UFII2Slzr06dQedlj9s2/wB6R8aq7wmMFxvjzwbv2KKu9rFxW34zbHnJTemZbYJbO99AeqCf5q1VOrn3WTZnE3gk+GZnTmb4pw0sGHY2zZ7reAp9MVqWp5ltx4OSnQpySr8VvtVDfQaB6gaP1Ar5/fwbXBNrh+rIM6zFLdlvjpNrt0K5KDLrTI0p13kUdjnISlJ0OiV94VXdfnZY/bNv96R8aaufdYszbUrU+dlj9s2/3pHxrLh3aDcSREmR5RA2exdSv+w1DhJK7QszLpSlYEClKUAqqeNUrCY+UcMU5ZDmyri7kTaLCuISEtTeRXKpzShtOt94P6KtaoPxFuuUW69YY3j2PRb3Ck3ZLV2kSNc0CLynb6NqHpA6Hr7+6gJxSlKAVEcuy5+JL8k2nkNwKQt+S4OZuIg93T8pxX5Ke4AFSvyUrlUh9EWO684dNtpK1H6gNmqhxpbku1N3F/Rl3I+OvqGztSwCB19SU8qR9SRVsbRi6j5bl9fx6cjdwtFVZ/y4ILxqDKe8YuDZu8sjRk3DTyz130BGkj6kgD6q9vINs9nRPuE/CtBn/EmBw7NkRMg3G4v3iYYESPbWUuuLe7JbgBBUnQIbI33AkEkJ2oQtPhO2NEa4SZWNZRAiWqWmFeJEmC2EWtwlIHbEOnmGlpVtrnASoE62KrdWpLjJnb0oQ3cC0/INs9nRPuE/CnkG2ezon3CfhUdPFC1B/OWvF5nNiCUqnnkRp0GMmSOy9Lr6CgPS5ev1daiuO8UZ+QcS5wiJlS8fXhsG/Q7UlpoSFOvOv9ASR6akoQnlK+UEd46msdZPqyXOK3FmeQbZ7OifcJ+FPINs9nRPuE/CqCgeENfLt4Ns3NLlZb1YpyIwWbrboMSQ2nmWsdsyy5I9JCOUAhwpO1DQPUiw7/xsjWDJ7tjzGM5HkM+0RI8yY5aYrLiEtuhfKoczqST/ABavRA2fyQrR01k+rIVSL3k68g2z2dE+4T8K8JGKWaUQpy1xCtPVLiWUpWk/SlQGwfrBqt7ZxcXkXE62Kskty7YnNw1++sRIzKO0feTJbSkpKgFBfKpSeQqA2euiNiS2/jBYbra8HnQ0ypKcwWEW9ptCe0QOxW8tTo5vRCEoIVokhWho1Kq1Iu6k8yVKLJ1Z8km4k4lMyTIuVkJ0pT57R6GP53P+Mtsevm2od+yBoWUhaXEJUlQUlQ2FA7BFVmpIUCCAQehB9dbzhZMK7BJtqjvyRLXBR39G+VLjSev0NuIT+qrr62Lk+K811/eNzlYyhGFpxJlSlKpOWKr/AIn2qbcr9gjkTNEYo3FvKHn4a3eQ3hHIfwUDmTzE9+tK7u6rAqqeNUrCY+UcMU5ZDmyri7kTaLCuISEtTeRXKpzShtOt94P6KAtalKUBjXGIJ9vlRSdB9pTZP0bBH/eqlxVxS8ctocSpDrbCWXUKGilaByrB/QpJFXHVdZXYXcduMm6xGFPWqWvtZjbQJXGd0AXQn1tq16WuqVelohSim6K04OmuPFen70sdDB1VTm1LmVrxOxW6ZDlXDiZb4vjEa0X1UyavtEJ7JnxR9vm0ogq9JaRpOz17tbqvsw4V5RdOHnHO2RbX2s7JbmqRamvGGh4y2Y0ZG9lWkek2saWQen1ir+jSmZsdt+O82+w4OZDjSgpKh9II6GvStXenZnZdNSv4+lih8uxTNLTf+K7dkxgX6LmcRsxJiZ7LCIrqYQjKQ8lagr8gKBQFA82iU99ZvDrBMnxDP8XuEi1Ictr2GQLHPeTKb54EqN2iyFJ36aVFzlBQToj6OtXXSoI1ave5zUzw8zk+DJknDJ3Fim5Q7e7CgTUT46mbkS8opKAVgt+iQT2gT1rZP5Dk2PeEBxIONYkvKpLtos6SkT2YqWFgSuQrLhG0nat8uyOXuO66DrEYtEGNcpVwZhR2p8pCG5EpDSUuvJRvkStYG1BPMrQJ6cx130uY6q1rPh+fUozhlwgyXhXk/D54RWLxEj49Js12fYfS34o87JTKLiUr0Vt8wUgAde46FYPAvFhI4y5lIizY9yxHF5D8awKjLC2mnZxbky0JIJG21aR07gsiujO+sOz2S3Y7bmoFqgRbZBaGm4sNlLTSP0JSAB+qhKpJNW4IzK2/CuOow77OIPZzbmtTexr0W222D+rmaWf11HowfySa5bbQ4lTiFckuYOqIg9Y33FzXcj9BOh32babXGslsiwIbfZRYzaWm0b3pIGhs+s/X6624p04O/GX24+ljn42qmlTRl0pSqTkCoPxFuuUW69YY3j2PRb3Ck3ZLV2kSNc0CLynb6NqHpA6Hr7+6pxVf8T7VNuV+wRyJmiMUbi3lDz8NbvIbwjkP4KBzJ5ie/Wld3dQFgUpSgFKUoCMXThxYbpJck+LOwZTh2t6BIcjlZ3slQQQFHfrINa/5J7d7WvQ/98fhU3pV6r1Fu0ixVZx3KTIR8k9v9rXv34/CnyT2/wBrXv34/CpvSp19TqZa6p3mc1eCxCl8WeFCb/kF6ujtxNznReZiT2aeRp9SEdAO/QHWre+Se3+1r378fhVW+Ab8wLf23dP705XRNNfU6jXVO8yEfJPb/a179+Pwr0b4T2MqBlOXK4JH+bk3B0tn9KEqCVfoIIqZ0qNfV5SIdWo/eZ4QYMa2RGosOO1EjNDlbZYQEIQPoCR0Fe9KVS227sqFKUqAKqnjVKwmPlHDFOWQ5sq4u5E2iwriEhLU3kVyqc0obTrfeD+irWqD8RbrlFuvWGN49j0W9wpN2S1dpEjXNAi8p2+jah6QOh6+/uoCcUpSgFKUoBSlKAUpSgOdvAN+YFv7bun96cromuWuBd8f8GzOn+DeYJQxbbvOkXDE8jAKWLiHXCtcVzZ0h9KldB3K2AO9PN1LQClKUApSlAKUpQCq/wCJ9qm3K/YI5EzRGKNxbyh5+Gt3kN4RyH8FA5k8xPfrSu7uqwKqeexbeLfFddqu+H3hqPgkuNcrdkEhSmI0iapBJS0nYLoShQ2rqnZIOtJ5gLYpSlAKUpQClKUApSlAQvi7wlsHGrCJmM5CwpcZ3TjEpk8r8N9P4jzSvyVpP9YJB2CRVYcC+Ld+x7LHOD3FF9PnvBaLlpvRHK1kUJO+V1BP+eSAedHf0J66VXQdcg/wi/EbC8S4e2+DdIdycz0OpnYxNgNOMKhPJV6T4l8nJpHKnnaSSs8zWwkKS4kDq3zitPnB5B8pw/Lnivj3kzxhHjPi/Pydt2W+bs+f0ebWt9N7rY18cvBM463pHhhWDK8ou79ynZJLVbbjLkEbd7dPZtjp0ShK+x0kAJSlAAAAGvsbQClKUArXQsjtNyu9ytMS6QpV0tnZePQWJCFvxO0SVN9qgHmRzpBKeYDYGxus9a0tIUtaghCRsqUdAD6TXw9yzwisrb495hxFxK+zLJPu0qUhp+OQFGIs8qGlAghQCEt62OikJUNKSCAPrHl+Q2/jnNznhXYr/f8AGLvaERPKV7tkYthsOnnUw28ofjlsdSNdFggq0oVbcKKIMNiMlx11LLaWw48srWrQ1tSj1JPrJ76qnwaOPth8ITh8xfLQzPjzmEts3VqbFUns5QTpSQ+G0tPHSQrbfUJW3zJbKgkW5QClKUApSlAKUpQClKUBW7+dZJIu11ZgxrWmNDlrjJMguFauUDqddPXWuyCXecstT9svdmxm7218adiTmHHmlj60q2DX5bP5XyT7Ve/sTWyrUxOMnRrSpxSsvA8jivaWIpV5wi9yfRHKWaeAZjd+vDN3x0N4RcGVh5vyTIdWyHAoKC+R0KIII6BKkgfRXVgy7LwP8DZD998a/aVq7Qq9Fkau1sV1WSHndl/+hsn/ADvjTzuy/wD0Nk/53xpSm0KvRZDa2K6rJGnzGdmGW4je7GH7VbTc4L8LxyP2vaMdo2pHOnfTmTzbH1iueOGHgNYhw8kNzJ9ot2Y3BtXOly+uurZSdd3YoCUKH1LC66JtmVWu83y82eHK7a42dbSJzPZrT2KnWw42OYgBW0kH0SdevRrbVO0Ky5LIl+1cWuL8kY8C/ZLaobUSFAx6HFaHK2ww26hCB9ASOgFbGw5pfn8pt9suce3dhLQ6oLiFzmSUJB/K6aO6xqxrf84OOf0cr9hNbWFxc69XVzStZ8uibNzBe0cRXxEac3ud+S6Fo0pSto9WKUpQClKUApSlAVLbP5XyT7Ve/sTWyrW2z+V8k+1Xv7E1H73b+IL10kLtF+xqJbSR2LM2ySH3kjQ3zLTLQFdd9yR/3rjY1XxMz5/jEniql3beyZVz5xXueW5bxsRhVlU8i3wrE3djHjZA7ZnJDi31tlfatMOLWlAQkcg5RtezzdALENr4perJcQ/4dlf+dX5duElvz+Db3M9jQbxeoKl9hcLOmRbVNoV3pSpL6nACNbHPo/RWrFqLuyqk4UpaUnfP8fcq1NozhWVcLcSy7JrhGcls3zxxVlui0rlMNmOqMl15KGypxKVAFxKUKPpaI5lb1VvyzI7k3ZeH68mucSLKza62J2/+Mfh/icVtbzbPbHqHF+ijtPxtJPrNdA2/htjdqk46/EtiWHMfYejWwodc1HbdCQ4Nc2lc3InqrZ6dO81gXXgzhl7slxtE6yIkQLhcl3h9Cn3QvxxR2XkLCuZtX+4U62da2az1kea/d5csRDmvJcbvf5rIg/AvH04vxW4vW1E+4XJDMu2csi6SVSJBBhJVpTiuqtb0CdnQHU1dtV5B4UpwJiYrh55OsU64vNu3B+8NyriJHIjkSeslCgrWvS5jv1gnrX9+S+Kf5zYf/wAOyv8AzqwlaTvcoqONWWkpdOPgkvEsCsa3/ODjn9HK/YTUexmFm0e4qVkN5sE+B2ZAatlpfiu8+xo865Lg1rfTl+jr9Mht/wA4OOf0cr9hNbmA3YhfSX+rNv2crYuFnfj9mWjSlK657sUpSgFKUoBSlKAqFSLlab3fkqsN0kIeuDj7bsdgLQtCgnRB39VenlKf+bd791H71W1SsalKhVk5zhvficqr7Mw9WbqSvd+JUvlKf+bd791H71PKU/8ANu9+6j96rapVfZsN3HmVbIwvjn+CpfKU/wDNu9+6j96nlKf+bd791H71W1SnZsN3HmNkYXxz/BUvlKf+bd791H71PKU/82737qP3qtqlOzYbuPMbIwvjn+CpfKU/82737qP3q98eauNwzizSVWa4wo0ZuR2j0tkISOZIAHeatOlW06dGlLThHfv59VYuo+zqFCoqkL3XiKUpUnUP/9k=", + "image/jpeg": "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", "text/plain": [ "" ] @@ -88,53 +161,63 @@ "output_type": "display_data" } ], - "source": ["try:\n display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "try:\n", + " display(Image(runnable.get_graph(xray=True).draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[HumanMessage(content='What is 123 * 456?', id='fa2dbb36-c61b-4ce1-892d-c08f3e741035'),\n", - " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_ZpoO6ClLFKkppN9Y8GEelZH1', 'function': {'arguments': '{\"first_number\":123,\"second_number\":456}', 'name': 'multiply'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 57, 'total_tokens': 76}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-ee9faec5-3526-45d5-89b1-5292755a6893-0', tool_calls=[{'name': 'multiply', 'args': {'first_number': 123, 'second_number': 456}, 'id': 'call_ZpoO6ClLFKkppN9Y8GEelZH1'}], usage_metadata={'input_tokens': 57, 'output_tokens': 19, 'total_tokens': 76}),\n", - " ToolMessage(content='56088', name='multiply', id='b9992170-ca76-4256-8560-29b329c1b56e', tool_call_id='call_ZpoO6ClLFKkppN9Y8GEelZH1')]" + "[HumanMessage(content='What is 123 * 456?', id='81692a54-acd4-4ef7-9ccf-49b2efc0b9b1'),\n", + " AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_XneG8vpjfal3lKO4q4fmfUYc', 'function': {'arguments': '{\"first_number\": 123, \"second_number\": 456}', 'name': 'multiply'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 34, 'prompt_tokens': 57, 'total_tokens': 91}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2fe18a05-45bf-4cf6-ba29-c5956fc928bf-0', tool_calls=[{'name': 'multiply', 'args': {'first_number': 123, 'second_number': 456}, 'id': 'call_XneG8vpjfal3lKO4q4fmfUYc', 'type': 'tool_call'}], usage_metadata={'input_tokens': 57, 'output_tokens': 34, 'total_tokens': 91}),\n", + " ToolMessage(content='56088', name='multiply', id='d494f0c9-daa8-4da2-aa4b-e9d4ace87912', tool_call_id='call_XneG8vpjfal3lKO4q4fmfUYc')]" ] }, - "execution_count": 20, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], - "source": ["runnable.invoke(HumanMessage(\"What is 123 * 456?\"))"] + "source": [ + "runnable.invoke(HumanMessage(\"What is 123 * 456?\"))" + ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[HumanMessage(content='What is your name?', id='09f03ac4-ca68-4464-9ec3-2c393699b3bb'),\n", - " AIMessage(content='My name is Assistant. How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 13, 'prompt_tokens': 54, 'total_tokens': 67}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-a21b2f58-3fa6-428f-99e5-9c4e071a9319-0', usage_metadata={'input_tokens': 54, 'output_tokens': 13, 'total_tokens': 67})]" + "[HumanMessage(content='What is your name?', id='184ed583-58f1-4d4c-a428-d573b5a56286'),\n", + " AIMessage(content='My name is Assistant. How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 13, 'prompt_tokens': 54, 'total_tokens': 67}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-38dcd15d-8bdf-491a-b7ca-771bb64bb824-0', usage_metadata={'input_tokens': 54, 'output_tokens': 13, 'total_tokens': 67})]" ] }, - "execution_count": 21, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], - "source": ["runnable.invoke(HumanMessage(\"What is your name?\"))"] + "source": [ + "runnable.invoke(HumanMessage(\"What is your name?\"))" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [""] + "source": [] } ], "metadata": { From 0d9c0732d49af682c0ff65b96ee8400601cab843 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 26 Aug 2024 08:25:24 -0700 Subject: [PATCH 127/287] docs: Update langgraph-api/cloud version constraints --- docs/docs/cloud/deployment/setup.md | 38 +++++++++++-------- docs/docs/cloud/deployment/setup_pyproject.md | 18 +++++---- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/docs/docs/cloud/deployment/setup.md b/docs/docs/cloud/deployment/setup.md index 170e19394..d2d66f86a 100644 --- a/docs/docs/cloud/deployment/setup.md +++ b/docs/docs/cloud/deployment/setup.md @@ -1,15 +1,14 @@ # How to Set Up a LangGraph Application for Deployment -A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `requirements.txt` to specify project dependencies. +A LangGraph application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph application for deployment using `requirements.txt` to specify project dependencies. This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraph-example), which you can play around with to learn more about how to setup your LangGraph application for deployment. !!! tip "Setup with pyproject.toml" - If you prefer using poetry for dependency management, check out [this how-to guide](./setup_pyproject.md) on using `pyproject.toml` for LangGraph Cloud. +If you prefer using poetry for dependency management, check out [this how-to guide](./setup_pyproject.md) on using `pyproject.toml` for LangGraph Cloud. !!! tip "Setup with a Monorepo" - If you are interested in deploying a graph located inside a monorepo, take a look at [this](https://github.com/langchain-ai/langgraph-example-monorepo) repository for an example of how to do so. - +If you are interested in deploying a graph located inside a monorepo, take a look at [this](https://github.com/langchain-ai/langgraph-example-monorepo) repository for an example of how to do so. The final repo structure will look something like this: @@ -35,23 +34,27 @@ After each step, an example file directory is provided to demonstrate how code c Dependencies can optionally be specified in one of the following files: `pyproject.toml`, `setup.py`, or `requirements.txt`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config). The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range: + ``` -langgraph>=0.2.0,<0.3.0 +langgraph>=0.2.7,<0.3.0 +langgraph-checkpoint>=1.0.4 langchain-core>=0.2.27,<0.3.0 langsmith>=0.1.63 -orjson>=3.10.1 -httpx>=0.27.0 -tenacity>=8.3.0 -uvicorn>=0.29.0 +orjson>=3.9.7 +httpx>=0.25.0 +tenacity>=8.0.0 +uvicorn>=0.26.0 sse-starlette>=2.1.0 -uvloop>=0.19.0 -httptools>=0.6.1 -jsonschema-rs>=0.18.0 +uvloop>=0.18.0 +httptools>=0.5.0 +jsonschema-rs>=0.16.3 croniter>=1.0.1 -structlog>=24.4.0 +structlog>=23.1.0 +redis>=5.0.0,<6.0.0 ``` Example `requirements.txt` file: + ``` langgraph langchain_anthropic @@ -62,6 +65,7 @@ langchain_openai ``` Example file directory: + ```bash my-app/ ├── my_agent # all project code lies within here @@ -73,6 +77,7 @@ my-app/ Environment variables can optionally be specified in a file (e.g. `.env`). See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for a deployment. Example `.env` file: + ``` MY_ENV_VAR_1=foo MY_ENV_VAR_2=bar @@ -94,7 +99,6 @@ Implement your graphs! Graphs can be defined in a single file or multiple files. Example `agent.py` file, which shows how to import from other modules you define (code for the modules is not shown here, please see [this repo](https://github.com/langchain-ai/langgraph-example) to see their implementation): - ```python # my_agent/agent.py from typing import TypedDict, Literal @@ -125,9 +129,10 @@ graph = workflow.compile() ``` !!! warning "Assign `CompiledGraph` to Variable" - The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a Python module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)). +The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a Python module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)). Example file directory: + ```bash my-app/ ├── my_agent # all project code lies within here @@ -147,6 +152,7 @@ my-app/ Create a [LangGraph API configuration file](../reference/cli.md#configuration-file) called `langgraph.json`. See the [LangGraph CLI reference](../reference/cli.md#configuration-file) for detailed explanations of each key in the JSON object of the configuration file. Example `langgraph.json` file: + ```json { "dependencies": ["./my_agent"], @@ -160,7 +166,7 @@ Example `langgraph.json` file: Note that the variable name of the `CompiledGraph` appears at the end of the value of each subkey in the top-level `graphs` key (i.e. `:`). !!! warning "Configuration Location" - The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the Python files that contain compiled graphs and associated dependencies. +The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the Python files that contain compiled graphs and associated dependencies. Example file directory: diff --git a/docs/docs/cloud/deployment/setup_pyproject.md b/docs/docs/cloud/deployment/setup_pyproject.md index 767171f05..aef373a9b 100644 --- a/docs/docs/cloud/deployment/setup_pyproject.md +++ b/docs/docs/cloud/deployment/setup_pyproject.md @@ -35,19 +35,21 @@ Dependencies can optionally be specified in one of the following files: `pyproje The dependencies below will be included in the image, you can also use them in your code, as long as with a compatible version range: ``` -langgraph>=0.2.0,<0.3.0 +langgraph>=0.2.7,<0.3.0 +langgraph-checkpoint>=1.0.4 langchain-core>=0.2.27,<0.3.0 langsmith>=0.1.63 -orjson>=3.10.1 -httpx>=0.27.0 -tenacity>=8.3.0 -uvicorn>=0.29.0 +orjson>=3.9.7 +httpx>=0.25.0 +tenacity>=8.0.0 +uvicorn>=0.26.0 sse-starlette>=2.1.0 -uvloop>=0.19.0 -httptools>=0.6.1 -jsonschema-rs>=0.18.0 +uvloop>=0.18.0 +httptools>=0.5.0 +jsonschema-rs>=0.16.3 croniter>=1.0.1 structlog>=24.4.0 +redis>=5.0.8,<6.0.0 ``` Example `pyproject.toml` file: From 49c316578b835a6bbbd5484cc301ebdf6fabad57 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Mon, 26 Aug 2024 12:51:18 -0400 Subject: [PATCH 128/287] langgraph: allow END end key in add_edge with list inputs (#1478) --- libs/langgraph/langgraph/graph/state.py | 2 +- .../tests/__snapshots__/test_pregel.ambr | 70 +++++++++++++++++++ libs/langgraph/tests/test_pregel.py | 18 +++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 94de80284..73b7df783 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -374,7 +374,7 @@ class StateGraph(Graph): raise ValueError(f"Need to add_node `{start}` first") if end_key == START: raise ValueError("START cannot be an end node") - if end_key not in self.nodes: + if end_key != END and end_key not in self.nodes: raise ValueError(f"Need to add_node `{end_key}` first") self.waiting_edges.add((tuple(start_key), end_key)) diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index 4835ec611..91096bca6 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -2279,6 +2279,76 @@ ''' # --- +# name: test_conditional_state_graph_with_list_edge_inputs + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "A", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "A" + } + }, + { + "id": "B", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "B" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "A", + "target": "__end__" + }, + { + "source": "B", + "target": "__end__" + }, + { + "source": "__start__", + "target": "A" + }, + { + "source": "__start__", + "target": "B" + } + ] + } + ''' +# --- +# name: test_conditional_state_graph_with_list_edge_inputs.1 + ''' + graph TD; + A --> __end__; + B --> __end__; + __start__ --> A; + __start__ --> B; + + ''' +# --- # name: test_conditional_state_graph[postgres] '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' # --- diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 9f6a0c0e5..ba91c4eb0 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -3590,6 +3590,24 @@ def test_conditional_state_graph( ] +def test_conditional_state_graph_with_list_edge_inputs(snapshot: SnapshotAssertion): + class State(TypedDict): + foo: Annotated[list[str], operator.add] + + graph_builder = StateGraph(State) + graph_builder.add_node("A", lambda x: {"foo": ["A"]}) + graph_builder.add_node("B", lambda x: {"foo": ["B"]}) + graph_builder.add_edge(START, "A") + graph_builder.add_edge(START, "B") + graph_builder.add_edge(["A", "B"], END) + + app = graph_builder.compile() + assert app.invoke({"foo": []}) == {"foo": ["A", "B"]} + + assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot + + def test_state_graph_w_config_inherited_state_keys(snapshot: SnapshotAssertion) -> None: from langchain_core.agents import AgentAction, AgentFinish from langchain_core.language_models.fake import FakeStreamingListLLM From da806c466d80bd25be3680467c0d52cf62d1d12e Mon Sep 17 00:00:00 2001 From: Alexander Kovrigin Date: Mon, 26 Aug 2024 19:47:50 +0200 Subject: [PATCH 129/287] Allow passing `ToolNode` as `tools` in `create_react_agent` (#1451) --- .../langgraph/prebuilt/chat_agent_executor.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py index 546ad53bc..d5a1dc88e 100644 --- a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py +++ b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py @@ -130,7 +130,7 @@ def _get_model_preprocessing_runnable( @deprecated_parameter("messages_modifier", "0.1.9", "state_modifier", removal="0.3.0") def create_react_agent( model: LanguageModelLike, - tools: Union[ToolExecutor, Sequence[BaseTool]], + tools: Union[ToolExecutor, Sequence[BaseTool], ToolNode], *, state_schema: Optional[StateSchemaType] = None, messages_modifier: Optional[MessagesModifier] = None, @@ -144,7 +144,7 @@ def create_react_agent( Args: model: The `LangChain` chat model that supports tool calling. - tools: A list of tools or a ToolExecutor instance. + tools: A list of tools, a ToolExecutor, or a ToolNode instance. state_schema: An optional state schema that defines graph state. Must have `messages` and `is_last_step` keys. Defaults to `AgentState` that defines those two keys. @@ -419,8 +419,13 @@ def create_react_agent( if isinstance(tools, ToolExecutor): tool_classes = tools.tools + tool_node = ToolNode(tool_classes) + elif isinstance(tools, ToolNode): + tool_classes = tools.tools_by_name.values() + tool_node = tools else: tool_classes = tools + tool_node = ToolNode(tool_classes) model = model.bind_tools(tool_classes) # Define the function that determines whether to continue or not @@ -474,7 +479,7 @@ def create_react_agent( # Define the two nodes we will cycle between workflow.add_node("agent", RunnableLambda(call_model, acall_model)) - workflow.add_node("tools", ToolNode(tool_classes)) + workflow.add_node("tools", tool_node) # Set the entrypoint as `agent` # This means that this node is the first one called From 25a72e77efb00c8dbed907e5e1baaad77295388f Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 26 Aug 2024 11:57:57 -0700 Subject: [PATCH 130/287] Override with_config to store config in Pregel instance - This enables eg customizing callbacks/metadata in langgraph cloud deployments --- libs/langgraph/langgraph/pregel/__init__.py | 34 ++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 73db1bfb8..f127859ff 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -38,6 +38,7 @@ from langchain_core.runnables.config import ( ensure_config, get_async_callback_manager_for_config, get_callback_manager_for_config, + merge_configs, patch_config, ) from langchain_core.runnables.utils import ( @@ -219,11 +220,20 @@ class Pregel( config_type: Optional[Type[Any]] = None + config: Optional[RunnableConfig] = None + name: str = "LangGraph" class Config: arbitrary_types_allowed = True + def with_config( + self, config: RunnableConfig | None = None, **kwargs: Any + ) -> Runnable[Dict[All, Any] | Any, Dict[All, Any] | Any]: + return self.copy( + update={"config": cast(RunnableConfig, {**(config or {}), **kwargs})} + ) + @classmethod def is_lc_serializable(cls) -> bool: """Return whether the graph can be serialized by Langchain.""" @@ -297,6 +307,7 @@ class Pregel( def get_input_schema( self, config: Optional[RunnableConfig] = None ) -> Type[BaseModel]: + config = merge_configs(self.config, config) if isinstance(self.input_channels, str): return super().get_input_schema(config) else: @@ -316,6 +327,7 @@ class Pregel( def get_output_schema( self, config: Optional[RunnableConfig] = None ) -> Type[BaseModel]: + config = merge_configs(self.config, config) if isinstance(self.output_channels, str): return super().get_output_schema(config) else: @@ -342,6 +354,7 @@ class Pregel( if not self.checkpointer: raise ValueError("No checkpointer set") + config = merge_configs(self.config, config) if self.config else config saved = self.checkpointer.get_tuple(config) checkpoint = saved.checkpoint if saved else empty_checkpoint() config = saved.config if saved else config @@ -374,6 +387,7 @@ class Pregel( if not self.checkpointer: raise ValueError("No checkpointer set") + config = merge_configs(self.config, config) if self.config else config saved = await self.checkpointer.aget_tuple(config) checkpoint = saved.checkpoint if saved else empty_checkpoint() @@ -422,7 +436,12 @@ class Pregel( metadata, parent_config, pending_writes, - ) in self.checkpointer.list(config, before=before, limit=limit, filter=filter): + ) in self.checkpointer.list( + merge_configs(self.config, config) if self.config else config, + before=before, + limit=limit, + filter=filter, + ): with ChannelsManager( self.channels, checkpoint, config, skip_context=True ) as (channels, managed): @@ -467,7 +486,12 @@ class Pregel( metadata, parent_config, pending_writes, - ) in self.checkpointer.alist(config, before=before, limit=limit, filter=filter): + ) in self.checkpointer.alist( + merge_configs(self.config, config) if self.config else config, + before=before, + limit=limit, + filter=filter, + ): async with AsyncChannelsManager( self.channels, checkpoint, config, skip_context=True ) as (channels, managed): @@ -504,6 +528,7 @@ class Pregel( raise ValueError("No checkpointer set") # get last checkpoint + config = merge_configs(self.config, config) if self.config else config saved = self.checkpointer.get_tuple(config) checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() checkpoint_previous_versions = ( @@ -637,6 +662,7 @@ class Pregel( raise ValueError("No checkpointer set") # get last checkpoint + config = merge_configs(self.config, config) if self.config else config saved = await self.checkpointer.aget_tuple(config) checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() checkpoint_previous_versions = ( @@ -885,7 +911,7 @@ class Pregel( {'type': 'task_result', 'timestamp': '2024-06-23T...+00:00', 'step': 2, 'payload': {'id': '...', 'name': 'b', 'result': [('alist', ['there'])]}} ``` """ - config = ensure_config(config) + config = ensure_config(merge_configs(self.config, config)) callback_manager = get_callback_manager_for_config(config) run_manager = callback_manager.on_chain_start( dumpd(self), @@ -1125,7 +1151,7 @@ class Pregel( {'type': 'task_result', 'timestamp': '2024-06-23T...+00:00', 'step': 2, 'payload': {'id': '...', 'name': 'b', 'result': [('alist', ['there'])]}} ``` """ - config = ensure_config(config) + config = ensure_config(merge_configs(self.config, config)) callback_manager = get_async_callback_manager_for_config(config) run_manager = await callback_manager.on_chain_start( dumpd(self), From 3979bdb792a2f1605b954cca93ea9ffd78e84edd Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 26 Aug 2024 13:14:11 -0700 Subject: [PATCH 131/287] Type as self --- libs/langgraph/langgraph/pregel/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index f127859ff..e871815ea 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -227,9 +227,7 @@ class Pregel( class Config: arbitrary_types_allowed = True - def with_config( - self, config: RunnableConfig | None = None, **kwargs: Any - ) -> Runnable[Dict[All, Any] | Any, Dict[All, Any] | Any]: + def with_config(self, config: RunnableConfig | None = None, **kwargs: Any) -> Self: return self.copy( update={"config": cast(RunnableConfig, {**(config or {}), **kwargs})} ) From 1d0f3577a7e9ee7e5086352560298791e55db9ff Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 26 Aug 2024 13:17:19 -0700 Subject: [PATCH 132/287] Catch any type error when reviving saved values --- libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py index d52bfc8c1..e39a66e76 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py @@ -150,7 +150,7 @@ class JsonPlusSerializer(SerializerProtocol): return method(**value["kwargs"]) else: return method() - except (ImportError, AttributeError): + except (ImportError, AttributeError, TypeError): return None return LC_REVIVER(value) From f310716921abdc33336697d2b616bef96513664b Mon Sep 17 00:00:00 2001 From: vbarda Date: Mon, 26 Aug 2024 17:16:06 -0400 Subject: [PATCH 133/287] sdk-js: add checkpoint_id arg to runs create/stream/wait payload --- libs/sdk-js/src/client.mts | 3 +++ libs/sdk-js/src/types.mts | 5 +++++ libs/sdk-py/langgraph_sdk/client.py | 5 ++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/libs/sdk-js/src/client.mts b/libs/sdk-js/src/client.mts index 96654f0f4..03024299d 100644 --- a/libs/sdk-js/src/client.mts +++ b/libs/sdk-js/src/client.mts @@ -559,6 +559,7 @@ export class RunsClient extends BaseClient { assistant_id: assistantId, interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, + checkpoint_id: payload?.checkpointId, }; if (payload?.multitaskStrategy != null) { json["multitask_strategy"] = payload?.multitaskStrategy; @@ -638,6 +639,7 @@ export class RunsClient extends BaseClient { interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, webhook: payload?.webhook, + checkpoint_id: payload?.checkpointId, }; if (payload?.multitaskStrategy != null) { json["multitask_strategy"] = payload?.multitaskStrategy; @@ -681,6 +683,7 @@ export class RunsClient extends BaseClient { assistant_id: assistantId, interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, + checkpoint_id: payload?.checkpointId, }; if (payload?.multitaskStrategy != null) { json["multitask_strategy"] = payload?.multitaskStrategy; diff --git a/libs/sdk-js/src/types.mts b/libs/sdk-js/src/types.mts index e7d1bdc09..c1e1b0d26 100644 --- a/libs/sdk-js/src/types.mts +++ b/libs/sdk-js/src/types.mts @@ -30,6 +30,11 @@ interface RunsInvokePayload { */ config?: Config; + /** + * Checkpoint ID to pass when creating a run. + */ + checkpointId?: string; + /** * Interrupt execution before entering these nodes. */ diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index 8578680d7..84b6e1876 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -853,15 +853,14 @@ class ThreadsClient: thread_id: The ID of the thread to update. values: The values to update to the state. as_node: Update the state as if this node had just executed. - - checkpoint_id: The ID of the checkpoint to get the state of. + checkpoint_id: The ID of the checkpoint to update the state of. Returns: None Example Usage: - await client.threads.get_state( + await client.threads.update_state( thread_id="my_thread_id", values={"messages":[{"role": "user", "content": "hello!"}]}, as_node="my_node", From ada751d19b86c3e21a3680095aac0d38dc16858e Mon Sep 17 00:00:00 2001 From: vbarda Date: Mon, 26 Aug 2024 18:25:43 -0400 Subject: [PATCH 134/287] better comment --- libs/sdk-js/src/types.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/sdk-js/src/types.mts b/libs/sdk-js/src/types.mts index c1e1b0d26..872cc5723 100644 --- a/libs/sdk-js/src/types.mts +++ b/libs/sdk-js/src/types.mts @@ -31,7 +31,7 @@ interface RunsInvokePayload { config?: Config; /** - * Checkpoint ID to pass when creating a run. + * Checkpoint ID for when creating a new run. */ checkpointId?: string; From 1315c0d743176348cb0b2751b3a2675dcb9f902d Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 26 Aug 2024 15:27:53 -0700 Subject: [PATCH 135/287] sdk-py0.1.29 --- libs/sdk-py/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml index 2755c3fc6..2e528e4bc 100644 --- a/libs/sdk-py/pyproject.toml +++ b/libs/sdk-py/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-sdk" -version = "0.1.28" +version = "0.1.29" description = "SDK for interacting with LangGraph API" authors = [] license = "MIT" From 85e698e20b535a2a783874847a9ca80c73f79123 Mon Sep 17 00:00:00 2001 From: vbarda Date: Mon, 26 Aug 2024 19:17:59 -0400 Subject: [PATCH 136/287] filter on checkpoint NS --- .../langgraph/checkpoint/postgres/base.py | 4 + .../langgraph/checkpoint/sqlite/utils.py | 4 + .../langgraph/checkpoint/memory/__init__.py | 4 + libs/langgraph/tests/test_pregel.py | 210 ++++++------------ libs/langgraph/tests/test_pregel_async.py | 209 ++++++----------- 5 files changed, 145 insertions(+), 286 deletions(-) diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py index ff2ec6681..91b49a162 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py @@ -255,6 +255,10 @@ class BasePostgresSaver(BaseCheckpointSaver): if config: wheres.append("thread_id = %s ") param_values.append(config["configurable"]["thread_id"]) + if checkpoint_ns := config["configurable"].get("checkpoint_ns"): + wheres.append("checkpoint_ns = %s") + param_values.append(checkpoint_ns) + if checkpoint_id := get_checkpoint_id(config): wheres.append("checkpoint_id = %s ") param_values.append(checkpoint_id) diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py index 26b8594d6..0e1e06fcc 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py @@ -70,6 +70,10 @@ def search_where( if config is not None: wheres.append("thread_id = ?") param_values.append(config["configurable"]["thread_id"]) + if checkpoint_ns := config["configurable"].get("checkpoint_ns"): + wheres.append("checkpoint_ns = ?") + param_values.append(checkpoint_ns) + if checkpoint_id := get_checkpoint_id(config): wheres.append("checkpoint_id = ?") param_values.append(checkpoint_id) diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 0a84326d7..dee64a327 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -177,9 +177,13 @@ class MemorySaver( Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples. """ thread_ids = (config["configurable"]["thread_id"],) if config else self.storage + config_checkpoint_ns = config["configurable"].get("checkpoint_ns") if config else None config_checkpoint_id = get_checkpoint_id(config) if config else None for thread_id in thread_ids: for checkpoint_ns in self.storage[thread_id].keys(): + if config_checkpoint_ns and checkpoint_ns != config_checkpoint_ns: + continue + for checkpoint_id, ( checkpoint, metadata_b, diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index a7642851d..685eb7c87 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -10442,81 +10442,9 @@ def test_nested_graph_state( } }, ) - # test loading inner snapshot - child_snapshot = app.get_state( - {"configurable": {"thread_id": "1", "checkpoint_ns": "inner"}} - ) - assert child_snapshot == StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - # test looking up parent state by checkpoint ID - assert app.get_state( - { - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], - } - }, - ) == StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots={"inner": child_snapshot}, - ) # test full history at the end - assert list(app.get_state_history(config)) == [ + actual_history = list(app.get_state_history(config)) + expected_history = [ StateSnapshot( values={"my_key": "hi my value here and there and back again"}, tasks=(), @@ -10675,6 +10603,10 @@ def test_nested_graph_state( subgraph_state_snapshots=None, ), ] + assert actual_history == expected_history + # test looking up parent state by checkpoint ID + for actual_snapshot, expected_snapshot in zip(actual_history, expected_history): + assert app.get_state(actual_snapshot.config) == expected_snapshot @pytest.mark.parametrize( @@ -10847,75 +10779,11 @@ def test_doubly_nested_graph_state( } }, ) + + # test getting snapshot by ID + config = list(app.get_state_history(config))[2].config # test getting grandchild snapshot - grandchild_snapshot = app.get_state( - {"configurable": {"thread_id": "1", "checkpoint_ns": "child|child_1"}} - ) - assert grandchild_snapshot == StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"grandchild_2": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - # test getting child snapshot - child_snapshot = app.get_state( - {"configurable": {"thread_id": "1", "checkpoint_ns": "child"}}, - ) - assert child_snapshot == StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"child_1": {"my_key": "hi my value here and there"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots={"child_1": grandchild_snapshot}, - ) - # test getting parent snapshot for a checkpoint ID - assert app.get_state( - { - "configurable": { - "thread_id": "1", - "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], - } - }, - ) == StateSnapshot( + assert app.get_state(config) == StateSnapshot( values={"my_key": "hi my value"}, tasks=(PregelTask(AnyStr(), "child"),), next=("child",), @@ -10939,7 +10807,63 @@ def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={"child": child_snapshot}, + subgraph_state_snapshots={ + "child": StateSnapshot( + values={"my_key": "hi my value here and there"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "child_1": StateSnapshot( + values={"my_key": "hi my value here and there"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "grandchild_2": {"my_key": "hi my value here and there"} + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + }, ) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 426bfd730..50aafae86 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -8946,81 +8946,9 @@ async def test_nested_graph_state( } }, ) - # test loading inner snapshot - child_snapshot = await app.aget_state( - {"configurable": {"thread_id": "1", "checkpoint_ns": "inner"}} - ) - assert child_snapshot == StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - # test looking up parent state by checkpoint ID - assert await app.aget_state( - { - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], - } - }, - ) == StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots={"inner": child_snapshot}, - ) # test full history at the end - assert [s async for s in app.aget_state_history(config)] == [ + actual_history = [s async for s in app.aget_state_history(config)] + expected_history = [ StateSnapshot( values={"my_key": "hi my value here and there and back again"}, tasks=(), @@ -9179,6 +9107,10 @@ async def test_nested_graph_state( subgraph_state_snapshots=None, ), ] + assert actual_history == expected_history + # test looking up parent state by checkpoint ID + for actual_snapshot, expected_snapshot in zip(actual_history, expected_history): + assert await app.aget_state(actual_snapshot.config) == expected_snapshot @pytest.mark.parametrize( @@ -9356,75 +9288,10 @@ async def test_doubly_nested_graph_state( } }, ) + # test getting snapshot by ID + config = [s async for s in app.aget_state_history(config)][2].config # test getting grandchild snapshot - grandchild_snapshot = await app.aget_state( - {"configurable": {"thread_id": "1", "checkpoint_ns": "child|child_1"}} - ) - assert grandchild_snapshot == StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"grandchild_2": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - # test getting child snapshot - child_snapshot = await app.aget_state( - {"configurable": {"thread_id": "1", "checkpoint_ns": "child"}}, - ) - assert child_snapshot == StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"child_1": {"my_key": "hi my value here and there"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots={"child_1": grandchild_snapshot}, - ) - # test getting parent snapshot for a checkpoint ID - assert await app.aget_state( - { - "configurable": { - "thread_id": "1", - "checkpoint_id": child_snapshot.config["configurable"]["checkpoint_id"], - } - }, - ) == StateSnapshot( + assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value"}, tasks=(PregelTask(AnyStr(), "child"),), next=("child",), @@ -9448,7 +9315,63 @@ async def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={"child": child_snapshot}, + subgraph_state_snapshots={ + "child": StateSnapshot( + values={"my_key": "hi my value here and there"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots={ + "child_1": StateSnapshot( + values={"my_key": "hi my value here and there"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "grandchild_2": {"my_key": "hi my value here and there"} + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "child|child_1", + "checkpoint_id": AnyStr(), + } + }, + subgraph_state_snapshots=None, + ) + }, + ) + }, ) From 507930e5b71f371d463da6d1b68d2e1934640147 Mon Sep 17 00:00:00 2001 From: vbarda Date: Mon, 26 Aug 2024 19:29:05 -0400 Subject: [PATCH 137/287] lint --- libs/checkpoint/langgraph/checkpoint/memory/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index dee64a327..6918de87a 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -177,7 +177,9 @@ class MemorySaver( Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples. """ thread_ids = (config["configurable"]["thread_id"],) if config else self.storage - config_checkpoint_ns = config["configurable"].get("checkpoint_ns") if config else None + config_checkpoint_ns = ( + config["configurable"].get("checkpoint_ns") if config else None + ) config_checkpoint_id = get_checkpoint_id(config) if config else None for thread_id in thread_ids: for checkpoint_ns in self.storage[thread_id].keys(): From 86c047eb31d34ce951fcf038f36e1da4a8ae6df6 Mon Sep 17 00:00:00 2001 From: isaac hershenson Date: Mon, 26 Aug 2024 18:05:17 -0700 Subject: [PATCH 138/287] changes --- .../customer-support/customer-support.ipynb | 80 ++++++++++--------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/examples/customer-support/customer-support.ipynb b/examples/customer-support/customer-support.ipynb index 07927338c..28c107273 100644 --- a/examples/customer-support/customer-support.ipynb +++ b/examples/customer-support/customer-support.ipynb @@ -99,46 +99,50 @@ " # Backup - we will use this to \"reset\" our DB in each section\n", " shutil.copy(local_file, backup_file)\n", "# Convert the flights to present time for our tutorial\n", - "conn = sqlite3.connect(local_file)\n", - "cursor = conn.cursor()\n", + "def update_dates(file):\n", + " shutil.copy(backup_file, file)\n", + " conn = sqlite3.connect(file)\n", + " cursor = conn.cursor()\n", "\n", - "tables = pd.read_sql(\n", - " \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n", - ").name.tolist()\n", - "tdf = {}\n", - "for t in tables:\n", - " tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n", + " tables = pd.read_sql(\n", + " \"SELECT name FROM sqlite_master WHERE type='table';\", conn\n", + " ).name.tolist()\n", + " tdf = {}\n", + " for t in tables:\n", + " tdf[t] = pd.read_sql(f\"SELECT * from {t}\", conn)\n", "\n", - "example_time = pd.to_datetime(\n", - " tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n", - ").max()\n", - "current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n", - "time_diff = current_time - example_time\n", + " example_time = pd.to_datetime(\n", + " tdf[\"flights\"][\"actual_departure\"].replace(\"\\\\N\", pd.NaT)\n", + " ).max()\n", + " current_time = pd.to_datetime(\"now\").tz_localize(example_time.tz)\n", + " time_diff = current_time - example_time\n", "\n", - "tdf[\"bookings\"][\"book_date\"] = (\n", - " pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n", - " + time_diff\n", - ")\n", - "\n", - "datetime_columns = [\n", - " \"scheduled_departure\",\n", - " \"scheduled_arrival\",\n", - " \"actual_departure\",\n", - " \"actual_arrival\",\n", - "]\n", - "for column in datetime_columns:\n", - " tdf[\"flights\"][column] = (\n", - " pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n", + " tdf[\"bookings\"][\"book_date\"] = (\n", + " pd.to_datetime(tdf[\"bookings\"][\"book_date\"].replace(\"\\\\N\", pd.NaT), utc=True)\n", + " + time_diff\n", " )\n", "\n", - "for table_name, df in tdf.items():\n", - " df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n", - "del df\n", - "del tdf\n", - "conn.commit()\n", - "conn.close()\n", + " datetime_columns = [\n", + " \"scheduled_departure\",\n", + " \"scheduled_arrival\",\n", + " \"actual_departure\",\n", + " \"actual_arrival\",\n", + " ]\n", + " for column in datetime_columns:\n", + " tdf[\"flights\"][column] = (\n", + " pd.to_datetime(tdf[\"flights\"][column].replace(\"\\\\N\", pd.NaT)) + time_diff\n", + " )\n", "\n", - "db = local_file # We'll be using this local file as our DB in this tutorial" + " for table_name, df in tdf.items():\n", + " df.to_sql(table_name, conn, if_exists=\"replace\", index=False)\n", + " del df\n", + " del tdf\n", + " conn.commit()\n", + " conn.close()\n", + "\n", + " return file\n", + "\n", + "db = update_dates(local_file)" ] }, { @@ -1750,7 +1754,7 @@ "]\n", "\n", "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", + "db = update_dates(db)\n", "thread_id = str(uuid.uuid4())\n", "\n", "config = {\n", @@ -2304,7 +2308,7 @@ "import uuid\n", "\n", "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", + "db = update_dates(db)\n", "thread_id = str(uuid.uuid4())\n", "\n", "config = {\n", @@ -2908,7 +2912,7 @@ "import uuid\n", "\n", "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", + "db = update_dates(db)\n", "thread_id = str(uuid.uuid4())\n", "\n", "config = {\n", @@ -4330,7 +4334,7 @@ "import uuid\n", "\n", "# Update with the backup file so we can restart from the original place in each section\n", - "shutil.copy(backup_file, db)\n", + "db = update_dates(db)\n", "thread_id = str(uuid.uuid4())\n", "\n", "config = {\n", From 54b53f384d29e3377823f54b096396d7c7448821 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Tue, 27 Aug 2024 09:11:57 -0400 Subject: [PATCH 139/287] sdk-js: release 0.0.8 (#1490) --- 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 659b087ca..8c87ebdb7 100644 --- a/libs/sdk-js/package.json +++ b/libs/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-sdk", - "version": "0.0.7", + "version": "0.0.8", "description": "Client library for interacting with the LangGraph API", "type": "module", "packageManager": "yarn@1.22.19", From eb387cc946b22a0738171e93c75cf4acf7b3a893 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Tue, 27 Aug 2024 09:26:22 -0400 Subject: [PATCH 140/287] docs: update cloud time travel docs to pass checkpoint_id correctly (#1483) --- .../how-tos/human_in_the_loop_time_travel.md | 159 ++++++++++-------- 1 file changed, 90 insertions(+), 69 deletions(-) diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md b/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md index aac3ee566..046bb95a8 100644 --- a/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md +++ b/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md @@ -34,7 +34,8 @@ First, we need to setup our client so that we can communicate with our hosted gr ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' + --header 'Content-Type: application/json' \ + --data {} ``` ## Replay a state @@ -46,7 +47,7 @@ Before replaying a state - we need to create states to replay from! In order to === "Python" ```python - input = { 'messages':[{ "role":"user", "content":"Please search the weather in SF" }] } + input = {"messages": [{"role": "user", "content": "Please search the weather in SF"}]} async for chunk in client.runs.stream( thread["thread_id"], @@ -61,7 +62,7 @@ Before replaying a state - we need to create states to replay from! In order to === "Javascript" ```js - const input = {"messages": [{ "role": "human", "content": "Please search the weather in SF"}] } + const input = { "messages": [{ "role": "human", "content": "Please search the weather in SF" }] } const streamResponse = client.runs.stream( thread["thread_id"], @@ -155,17 +156,23 @@ Output: -To rerun from a state, we need to pass in the `checkpoint_id` into the config of the run like follows: +To rerun from a state, we need first issue an empty update to the thread state. Then we need to pass in the resulting `checkpoint_id` as follows: === "Python" ```python + state_to_replay = states[2] + updated_config = await client.threads.update_state( + thread["thread_id"], + {"messages": []}, + checkpoint_id=state_to_replay["checkpoint_id"] + ) async for chunk in client.runs.stream( thread["thread_id"], assistant_id, # graph_id input=None, stream_mode="updates", - config={"configurable": {"checkpoint_id": state_to_replay['checkpoint_id']}} + checkpoint_id=updated_config["checkpoint_id"] ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -174,13 +181,15 @@ To rerun from a state, we need to pass in the `checkpoint_id` into the config of === "Javascript" ```js + const stateToReplay = states[2]; + const config = await client.threads.updateState(thread["thread_id"], { values: {"messages": [] }, checkpointId: stateToReplay["checkpoint_id"] }); const streamResponse = client.runs.stream( thread["thread_id"], assistantId, { input: null, streamMode: "updates", - config: {"configurable": {"checkpoint_id": stateToReplay['checkpoint_id']}}, + checkpointId: config["checkpoint_id"] } ); for await (const chunk of streamResponse) { @@ -193,38 +202,46 @@ To rerun from a state, we need to pass in the `checkpoint_id` into the config of === "CURL" ```bash - curl --request GET --url /threads//history | jq -r '.[2].checkpoint_id' | { - read checkpoint_id - curl --request POST \ - --url /threads//runs/stream \ - --header 'Content-Type: application/json' \ - --data "{ - \"assistant_id\": \"agent\", - \"config\": {\"configurable\": {\"checkpoint_id\": \"$checkpoint_id\"}}, - \"stream_mode\": [ - \"updates\" - ] - }" | \ - sed 's/\r$//' | \ - awk ' - /^event:/ { - if (data_content != "" && event_type != "metadata") { - print data_content "\n" - } - sub(/^event: /, "", $0) - event_type = $0 - data_content = "" - } - /^data:/ { - sub(/^data: /, "", $0) - data_content = $0 - } - END { - if (data_content != "" && event_type != "metadata") { - print data_content "\n" - } + curl --request GET --url /threads//history | jq -c ' + .[2] as $state_to_replay | + { + values: { messages: .[2].values.messages[-1] }, + checkpoint_id: $state_to_replay.checkpoint_id + }' | \ + curl --request POST \ + --url /threads//state \ + --header 'Content-Type: application/json' \ + --data @- | jq .checkpoint_id | \ + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"checkpoint_id\": \"$1\", + \"stream_mode\": [ + \"updates\" + ] + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" } - ' + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' ``` Output: @@ -251,7 +268,7 @@ Let's show how to do this to edit the state at a particular point in time. Let's # Let's now update the args for that tool call last_message['tool_calls'][0]['args'] = {'query': 'current weather in SF'} - new_state = await client.threads.update_state(thread['thread_id'],{"messages":[last_message]},checkpoint_id=state_to_replay['checkpoint_id']) + config = await client.threads.update_state(thread['thread_id'],{"messages":[last_message]},checkpoint_id=state_to_replay['checkpoint_id']) ``` === "Javascript" @@ -262,9 +279,9 @@ Let's show how to do this to edit the state at a particular point in time. Let's let lastMessage = stateToReplay['values']['messages'][-1]; // Let's now update the args for that tool call - lastMessage['tool_calls'][0]['args'] = {'query': 'current weather in SF'}; + lastMessage['tool_calls'][0]['args'] = { 'query': 'current weather in SF' }; - const newState = await client.threads.updateState(thread['thread_id'],{values:{"messages":[lastMessage]},checkpointId:stateToReplay['checkpoint_id']}); + const config = await client.threads.updateState(thread['thread_id'], { values: { "messages": [lastMessage] }, checkpointId: stateToReplay['checkpoint_id'] }); ``` === "CURL" @@ -294,7 +311,7 @@ Now we can rerun our graph with this new config, starting from the `new_state`, assistant["assistant_id"], # graph_id input=None, stream_mode="updates", - config={"configurable": {"checkpoint_id": new_state['configurable']['checkpoint_id']}} + checkpoint_id=config['checkpoint_id'] ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -309,7 +326,7 @@ Now we can rerun our graph with this new config, starting from the `new_state`, { input: null, streamMode: "updates", - config: {"configurable": {"checkpoint_id": newState['configurable']['checkpoint_id']}}, + checkpointId: config['checkpoint_id'], } ); for await (const chunk of streamResponse) { @@ -323,33 +340,37 @@ Now we can rerun our graph with this new config, starting from the `new_state`, ```bash curl -s --request GET --url /threads//state | \ - jq -r '.config.configurable.checkpoint_id' | \ - sh -c ' - CHECKPOINT_ID="$1" - curl --request POST \ - --url /threads//runs/stream \ - --header "Content-Type: application/json" \ - --data "{\"assistant_id\": \"agent\", \"config\": {\"configurable\": {\"checkpoint_id\": \"$CHECKPOINT_ID\"}}, \"stream_mode\": [\"updates\"]}" | \ - sed "s/\r$//" | \ - awk " - /^event:/ { - if (data_content != \"\" && event_type != \"metadata\") { - print data_content \"\n\" - } - sub(/^event: /, \"\", \$0) - event_type = \$0 - data_content = \"\" - } - /^data:/ { - sub(/^data: /, \"\", \$0) - data_content = \$0 - } - END { - if (data_content != \"\" && event_type != \"metadata\") { - print data_content \"\n\" - } - }" - ' _ + jq -c '.checkpoint_id' | \ + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"checkpoint_id\": \"$1\", + \"stream_mode\": [ + \"updates\" + ] + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' ``` Output: From f2ec99b9a86a45bbf79d17b5f621968670d0768e Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 27 Aug 2024 08:10:26 -0700 Subject: [PATCH 141/287] lib: For all value channels the return value of checkpoint() and get() are the same --- .../langgraph/langgraph/channels/any_value.py | 6 -- libs/langgraph/langgraph/channels/base.py | 2 +- libs/langgraph/langgraph/channels/binop.py | 6 -- .../langgraph/channels/ephemeral_value.py | 6 -- .../langgraph/channels/last_value.py | 6 -- libs/langgraph/langgraph/channels/topic.py | 32 +++---- libs/langgraph/tests/test_channels.py | 87 ------------------- 7 files changed, 11 insertions(+), 134 deletions(-) diff --git a/libs/langgraph/langgraph/channels/any_value.py b/libs/langgraph/langgraph/channels/any_value.py index 345daa7c7..e9bd85572 100644 --- a/libs/langgraph/langgraph/channels/any_value.py +++ b/libs/langgraph/langgraph/channels/any_value.py @@ -28,12 +28,6 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]): """The type of the update received by the channel.""" return self.typ - def checkpoint(self) -> Value: - try: - return self.value - except AttributeError: - raise EmptyChannelError() - @contextmanager def from_checkpoint( self, checkpoint: Optional[Value], config: RunnableConfig diff --git a/libs/langgraph/langgraph/channels/base.py b/libs/langgraph/langgraph/channels/base.py index 885698743..955b6ab76 100644 --- a/libs/langgraph/langgraph/channels/base.py +++ b/libs/langgraph/langgraph/channels/base.py @@ -35,11 +35,11 @@ class BaseChannel(Generic[Value, Update, C], ABC): # serialize/deserialize methods - @abstractmethod def checkpoint(self) -> Optional[C]: """Return a serializable representation of the channel's current state. Raises EmptyChannelError if the channel is empty (never updated yet), or doesn't support checkpoints.""" + return self.get() @contextmanager @abstractmethod diff --git a/libs/langgraph/langgraph/channels/binop.py b/libs/langgraph/langgraph/channels/binop.py index 72e76c0a7..02e33bd2e 100644 --- a/libs/langgraph/langgraph/channels/binop.py +++ b/libs/langgraph/langgraph/channels/binop.py @@ -73,12 +73,6 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]): """The type of the update received by the channel.""" return self.typ - def checkpoint(self) -> Value: - try: - return self.value - except AttributeError: - raise EmptyChannelError() - @contextmanager def from_checkpoint( self, checkpoint: Optional[Value], config: RunnableConfig diff --git a/libs/langgraph/langgraph/channels/ephemeral_value.py b/libs/langgraph/langgraph/channels/ephemeral_value.py index 4e7f2ed63..59e34a5f0 100644 --- a/libs/langgraph/langgraph/channels/ephemeral_value.py +++ b/libs/langgraph/langgraph/channels/ephemeral_value.py @@ -28,12 +28,6 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]): """The type of the update received by the channel.""" return self.typ - def checkpoint(self) -> Value: - try: - return self.value - except AttributeError: - raise EmptyChannelError() - @contextmanager def from_checkpoint( self, checkpoint: Optional[Value], config: RunnableConfig diff --git a/libs/langgraph/langgraph/channels/last_value.py b/libs/langgraph/langgraph/channels/last_value.py index e74580d6a..e5e59d111 100644 --- a/libs/langgraph/langgraph/channels/last_value.py +++ b/libs/langgraph/langgraph/channels/last_value.py @@ -27,12 +27,6 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]): """The type of the update received by the channel.""" return self.typ - def checkpoint(self) -> Value: - try: - return self.value - except AttributeError: - raise EmptyChannelError() - @contextmanager def from_checkpoint( self, checkpoint: Optional[Value], config: RunnableConfig diff --git a/libs/langgraph/langgraph/channels/topic.py b/libs/langgraph/langgraph/channels/topic.py index 5e7af6a26..7b4b0b27d 100644 --- a/libs/langgraph/langgraph/channels/topic.py +++ b/libs/langgraph/langgraph/channels/topic.py @@ -30,23 +30,15 @@ class Topic( accumulate: Whether to accumulate values across steps. If False, the channel will be emptied after each step. """ - def __init__( - self, typ: Type[Value], unique: bool = False, accumulate: bool = False - ) -> None: + def __init__(self, typ: Type[Value], accumulate: bool = False) -> None: # attrs self.typ = typ - self.unique = unique self.accumulate = accumulate # state - self.seen = set[Value]() self.values = list[Value]() def __eq__(self, value: object) -> bool: - return ( - isinstance(value, Topic) - and value.unique == self.unique - and value.accumulate == self.accumulate - ) + return isinstance(value, Topic) and value.accumulate == self.accumulate @property def ValueType(self) -> Any: @@ -59,18 +51,20 @@ class Topic( return Union[self.typ, list[self.typ]] # type: ignore[name-defined] def checkpoint(self) -> tuple[set[Value], list[Value]]: - return (self.seen, self.values) + return self.values @contextmanager def from_checkpoint( self, - checkpoint: Optional[tuple[set[Value], list[Value]]], + checkpoint: Optional[list[Value]], config: RunnableConfig, ) -> Generator[Self, None, None]: - empty = self.__class__(self.typ, self.unique, self.accumulate) + empty = self.__class__(self.typ, self.accumulate) if checkpoint is not None: - empty.seen = checkpoint[0].copy() - empty.values = checkpoint[1].copy() + if isinstance(checkpoint, tuple): + empty.values = checkpoint[1].copy() + else: + empty.values = checkpoint.copy() try: yield empty finally: @@ -81,13 +75,7 @@ class Topic( if not self.accumulate: self.values = list[Value]() if flat_values := flatten(values): - if self.unique: - for value in flat_values: - if value not in self.seen: - self.seen.add(value) - self.values.append(value) - else: - self.values.extend(flat_values) + self.values.extend(flat_values) return self.values != current def get(self) -> Sequence[Value]: diff --git a/libs/langgraph/tests/test_channels.py b/libs/langgraph/tests/test_channels.py index 3e0c924ae..ff393577e 100644 --- a/libs/langgraph/tests/test_channels.py +++ b/libs/langgraph/tests/test_channels.py @@ -91,50 +91,6 @@ async def test_topic_async() -> None: assert channel.get() == ["e"] -def test_topic_unique() -> None: - with Topic(str, unique=True).from_checkpoint(None, {}) as channel: - assert channel.ValueType is Sequence[str] - assert channel.UpdateType is Union[str, list[str]] - - assert channel.update(["a", "b"]) - assert channel.get() == ["a", "b"] - assert channel.update(["b", ["c", "d"], "d"]) - assert channel.get() == ["c", "d"], "de-dupes from current and previous steps" - assert channel.update([]) - with pytest.raises(EmptyChannelError): - channel.get() - assert not channel.update([]), "channel already empty" - assert channel.update(["e"]) - assert channel.get() == ["e"] - checkpoint = channel.checkpoint() - with Topic(str, unique=True).from_checkpoint(checkpoint, {}) as channel: - assert channel.get() == ["e"] - assert channel.update(["d", "f"]) - assert channel.get() == ["f"], "de-dupes from checkpoint" - - -async def test_topic_unique_async() -> None: - async with Topic(str, unique=True).afrom_checkpoint(None, {}) as channel: - assert channel.ValueType is Sequence[str] - assert channel.UpdateType is Union[str, list[str]] - - assert channel.update(["a", "b"]) - assert channel.get() == ["a", "b"] - assert channel.update(["b", ["c", "d"], "d"]) - assert channel.get() == ["c", "d"], "de-dupes from current and previous steps" - assert channel.update([]) - with pytest.raises(EmptyChannelError): - channel.get() - assert not channel.update([]), "channel already empty" - assert channel.update(["e"]) - assert channel.get() == ["e"] - checkpoint = channel.checkpoint() - async with Topic(str, unique=True).afrom_checkpoint(checkpoint, {}) as channel: - assert channel.get() == ["e"] - assert channel.update(["d", "f"]) - assert channel.get() == ["f"], "de-dupes from checkpoint" - - def test_topic_accumulate() -> None: with Topic(str, accumulate=True).from_checkpoint(None, {}) as channel: assert channel.ValueType is Sequence[str] @@ -171,49 +127,6 @@ async def test_topic_accumulate_async() -> None: assert channel.get() == ["a", "b", "b", "c", "d", "d", "e"] -def test_topic_unique_accumulate() -> None: - with Topic(str, unique=True, accumulate=True).from_checkpoint(None, {}) as channel: - assert channel.ValueType is Sequence[str] - assert channel.UpdateType is Union[str, list[str]] - - assert channel.update(["a", "b"]) - assert channel.get() == ["a", "b"] - assert channel.update(["b", ["c", "d"], "d"]) - assert channel.get() == ["a", "b", "c", "d"] - assert not channel.update(["c"]), "no new values" - assert not channel.update([]) - assert channel.get() == ["a", "b", "c", "d"] - checkpoint = channel.checkpoint() - with Topic(str, unique=True, accumulate=True).from_checkpoint( - checkpoint, {} - ) as channel: - assert channel.get() == ["a", "b", "c", "d"] - assert channel.update(["d", "e"]) - assert channel.get() == ["a", "b", "c", "d", "e"] - - -async def test_topic_unique_accumulate_async() -> None: - async with Topic(str, unique=True, accumulate=True).afrom_checkpoint( - None, {} - ) as channel: - assert channel.ValueType is Sequence[str] - assert channel.UpdateType is Union[str, list[str]] - - channel.update(["a", "b"]) - assert channel.get() == ["a", "b"] - channel.update(["b", ["c", "d"], "d"]) - assert channel.get() == ["a", "b", "c", "d"] - channel.update([]) - assert channel.get() == ["a", "b", "c", "d"] - checkpoint = channel.checkpoint() - async with Topic(str, unique=True, accumulate=True).afrom_checkpoint( - checkpoint, {} - ) as channel: - assert channel.get() == ["a", "b", "c", "d"] - channel.update(["d", "e"]) - assert channel.get() == ["a", "b", "c", "d", "e"] - - def test_binop() -> None: with BinaryOperatorAggregate(int, operator.add).from_checkpoint( None, {} From 9ff54e029e24ace9f4c94e777b9eb5c6dc34e91d Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Tue, 27 Aug 2024 08:18:51 -0700 Subject: [PATCH 142/287] [docs]: conceptual guide for astream_events (#1482) --------- Co-authored-by: Vadym Barda --- docs/docs/concepts/low_level.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index fb0b734fd..e4a6778cd 100644 --- a/docs/docs/concepts/low_level.md +++ b/docs/docs/concepts/low_level.md @@ -433,7 +433,17 @@ LangGraph is built with first class support for streaming. There are several dif - [`"updates`](../how-tos/stream-updates.ipynb): This streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are streamed separately. - `"debug"`: This streams as much information as possible throughout the execution of the graph. -In addition, you can use the [`astream_events`](../how-tos/streaming-events-from-within-tools.ipynb) method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb). +In addition, you can use the [`astream_events`](../how-tos/streaming-events-from-within-tools.ipynb) method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb) among other things. + +Under the hood, the compiled graph and each of its nodes are turned into [runnables](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface). This means that as the graph is executed, certain events are emitted along the way and can be seen if you run the graph using `.astream_events`: + +* each node (runnable) emits `on_chain_start` when it starts execution, `on_chain_stream` during the node execution and `on_chain_end` when the node finishes. Node events will have the node name in the event's `name` field +* the graph will emit `on_chain_start` in the beginning of the graph execution, `on_chain_stream` during each node execution and `on_chain_end` when the graph finishes. Graph events will have the `LangGraph` in the event's `name` field +* LangGraph writes to state channels emit `on_chain_start` and `on_chain_end` events + +Additionally, any events that are created inside your nodes (LLM events, tool events, manually emitted events, etc.) will also be visible in the output of `.astream_events`. + +You can find a detailed table of all callback events and triggers [here](https://python.langchain.com/v0.2/docs/concepts/#callback-events). !!! warning "ASYNC IN PYTHON<=3.10" You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case. Please see examples [here](../how-tos/streaming-content.ipynb) and [here](../how-tos/streaming-events-from-within-tools.ipynb). \ No newline at end of file From d12f5c6d8bbe1a2fc4a707cdb609be9a181f4965 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Tue, 27 Aug 2024 11:30:13 -0400 Subject: [PATCH 143/287] checkpoint-postgres: allow passing pool (#1452) * checkpoint-postgres: allow passing pool * make psycopg_pool a non-dev dependency * code review * lockfile * move methods * relax requirements, remove binary * add binary to dev dependencies * update readme --- libs/checkpoint-postgres/README.md | 4 + .../langgraph/checkpoint/postgres/__init__.py | 91 ++- .../langgraph/checkpoint/postgres/aio.py | 101 ++- libs/checkpoint-postgres/poetry.lock | 4 +- libs/checkpoint-postgres/pyproject.toml | 5 +- libs/langgraph/poetry.lock | 23 +- libs/langgraph/pyproject.toml | 1 + .../tests/__snapshots__/test_pregel.ambr | 725 ++++++++++++++++++ libs/langgraph/tests/conftest.py | 69 ++ libs/langgraph/tests/test_pregel.py | 132 +--- libs/langgraph/tests/test_pregel_async.py | 87 +-- 11 files changed, 981 insertions(+), 261 deletions(-) diff --git a/libs/checkpoint-postgres/README.md b/libs/checkpoint-postgres/README.md index 24652a2b2..cf6beabac 100644 --- a/libs/checkpoint-postgres/README.md +++ b/libs/checkpoint-postgres/README.md @@ -2,6 +2,10 @@ Implementation of LangGraph CheckpointSaver that uses Postgres. +## Dependencies + +By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`). + ## Usage > [!IMPORTANT] diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py index cb4a79c35..3ce8e880c 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py @@ -1,12 +1,13 @@ import threading from contextlib import contextmanager -from typing import Any, Iterator, List, Optional +from typing import Any, Iterator, List, Optional, Union from langchain_core.runnables import RunnableConfig from psycopg import Connection, Cursor, Pipeline from psycopg.errors import UndefinedTable from psycopg.rows import dict_row from psycopg.types.json import Jsonb +from psycopg_pool import ConnectionPool from langgraph.checkpoint.base import ( ChannelVersions, @@ -21,16 +22,32 @@ from langgraph.checkpoint.postgres.base import ( from langgraph.checkpoint.serde.base import SerializerProtocol +@contextmanager +def _get_connection(conn: Union[Connection, ConnectionPool]) -> Iterator[Connection]: + if isinstance(conn, Connection): + yield conn + elif isinstance(conn, ConnectionPool): + with conn.connection() as conn: + yield conn + else: + raise TypeError(f"Invalid connection type: {type(conn)}") + + class PostgresSaver(BasePostgresSaver): lock: threading.Lock def __init__( self, - conn: Connection, + conn: Union[Connection, ConnectionPool], pipe: Optional[Pipeline] = None, serde: Optional[SerializerProtocol] = None, ) -> None: super().__init__(serde=serde) + if isinstance(conn, ConnectionPool) and pipe is not None: + raise ValueError( + "Pipeline should be used only with a single Connection, not ConnectionPool." + ) + self.conn = conn self.pipe = pipe self.lock = threading.Lock() @@ -65,22 +82,21 @@ class PostgresSaver(BasePostgresSaver): already exist and runs database migrations. It MUST be called directly by the user the first time checkpointer is used. """ - with self.lock: - with self.conn.cursor(binary=True, row_factory=dict_row) as cur: - try: - version = cur.execute( - "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1" - ).fetchone()["v"] - except UndefinedTable: - version = -1 - for v, migration in zip( - range(version + 1, len(self.MIGRATIONS)), - self.MIGRATIONS[version + 1 :], - ): - cur.execute(migration) - cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})") - if self.pipe: - self.pipe.sync() + with self._cursor() as cur: + try: + version = cur.execute( + "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1" + ).fetchone()["v"] + except UndefinedTable: + version = -1 + for v, migration in zip( + range(version + 1, len(self.MIGRATIONS)), + self.MIGRATIONS[version + 1 :], + ): + cur.execute(migration) + cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})") + if self.pipe: + self.pipe.sync() def list( self, @@ -333,23 +349,24 @@ class PostgresSaver(BasePostgresSaver): @contextmanager def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor]: - if self.pipe: - # a connection in pipeline mode can be used concurrently - # in multiple threads/coroutines, but only one cursor can be - # used at a time - try: - with self.conn.cursor(binary=True, row_factory=dict_row) as cur: + with _get_connection(self.conn) as conn: + if self.pipe: + # a connection in pipeline mode can be used concurrently + # in multiple threads/coroutines, but only one cursor can be + # used at a time + try: + with conn.cursor(binary=True, row_factory=dict_row) as cur: + yield cur + finally: + if pipeline: + self.pipe.sync() + elif pipeline: + # a connection not in pipeline mode can only be used by one + # thread/coroutine at a time, so we acquire a lock + with self.lock, conn.pipeline(), conn.cursor( + binary=True, row_factory=dict_row + ) as cur: + yield cur + else: + with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur: yield cur - finally: - if pipeline: - self.pipe.sync() - elif pipeline: - # a connection not in pipeline mode can only be used by one - # thread/coroutine at a time, so we acquire a lock - with self.lock, self.conn.pipeline(), self.conn.cursor( - binary=True, row_factory=dict_row - ) as cur: - yield cur - else: - with self.lock, self.conn.cursor(binary=True, row_factory=dict_row) as cur: - yield cur diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py index 569159d91..0db61a443 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py @@ -1,12 +1,13 @@ import asyncio from contextlib import asynccontextmanager -from typing import Any, AsyncIterator, Optional +from typing import Any, AsyncIterator, Optional, Union from langchain_core.runnables import RunnableConfig from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline from psycopg.errors import UndefinedTable from psycopg.rows import dict_row from psycopg.types.json import Jsonb +from psycopg_pool import AsyncConnectionPool from langgraph.checkpoint.base import ( ChannelVersions, @@ -19,16 +20,34 @@ from langgraph.checkpoint.postgres.base import BasePostgresSaver from langgraph.checkpoint.serde.base import SerializerProtocol +@asynccontextmanager +async def _get_connection( + conn: Union[AsyncConnection, AsyncConnectionPool], +) -> AsyncIterator[AsyncConnection]: + if isinstance(conn, AsyncConnection): + yield conn + elif isinstance(conn, AsyncConnectionPool): + async with conn.connection() as conn: + yield conn + else: + raise TypeError(f"Invalid connection type: {type(conn)}") + + class AsyncPostgresSaver(BasePostgresSaver): lock: asyncio.Lock def __init__( self, - conn: AsyncConnection, + conn: Union[AsyncConnection, AsyncConnectionPool], pipe: Optional[AsyncPipeline] = None, serde: Optional[SerializerProtocol] = None, ) -> None: super().__init__(serde=serde) + if isinstance(conn, AsyncConnectionPool) and pipe is not None: + raise ValueError( + "Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool." + ) + self.conn = conn self.pipe = pipe self.lock = asyncio.Lock() @@ -63,25 +82,22 @@ class AsyncPostgresSaver(BasePostgresSaver): already exist and runs database migrations. It MUST be called directly by the user the first time checkpointer is used. """ - async with self.lock: - async with self.conn.cursor(binary=True, row_factory=dict_row) as cur: - try: - results = await cur.execute( - "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1" - ) - version = (await results.fetchone())["v"] - except UndefinedTable: - version = -1 - for v, migration in zip( - range(version + 1, len(self.MIGRATIONS)), - self.MIGRATIONS[version + 1 :], - ): - await cur.execute(migration) - await cur.execute( - f"INSERT INTO checkpoint_migrations (v) VALUES ({v})" - ) - if self.pipe: - await self.pipe.sync() + async with self._cursor() as cur: + try: + results = await cur.execute( + "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1" + ) + version = (await results.fetchone())["v"] + except UndefinedTable: + version = -1 + for v, migration in zip( + range(version + 1, len(self.MIGRATIONS)), + self.MIGRATIONS[version + 1 :], + ): + await cur.execute(migration) + await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})") + if self.pipe: + await self.pipe.sync() async def alist( self, @@ -290,25 +306,26 @@ class AsyncPostgresSaver(BasePostgresSaver): @asynccontextmanager async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[AsyncCursor]: - if self.pipe: - # a connection in pipeline mode can be used concurrently - # in multiple threads/coroutines, but only one cursor can be - # used at a time - try: - async with self.conn.cursor(binary=True, row_factory=dict_row) as cur: + async with _get_connection(self.conn) as conn: + if self.pipe: + # a connection in pipeline mode can be used concurrently + # in multiple threads/coroutines, but only one cursor can be + # used at a time + try: + async with conn.cursor(binary=True, row_factory=dict_row) as cur: + yield cur + finally: + if pipeline: + await self.pipe.sync() + elif pipeline: + # a connection not in pipeline mode can only be used by one + # thread/coroutine at a time, so we acquire a lock + async with self.lock, conn.pipeline(), conn.cursor( + binary=True, row_factory=dict_row + ) as cur: + yield cur + else: + async with self.lock, conn.cursor( + binary=True, row_factory=dict_row + ) as cur: yield cur - finally: - if pipeline: - await self.pipe.sync() - elif pipeline: - # a connection not in pipeline mode can only be used by one - # thread/coroutine at a time, so we acquire a lock - async with self.lock, self.conn.pipeline(), self.conn.cursor( - binary=True, row_factory=dict_row - ) as cur: - yield cur - else: - async with self.lock, self.conn.cursor( - binary=True, row_factory=dict_row - ) as cur: - yield cur diff --git a/libs/checkpoint-postgres/poetry.lock b/libs/checkpoint-postgres/poetry.lock index 813bdf9b0..0c171e445 100644 --- a/libs/checkpoint-postgres/poetry.lock +++ b/libs/checkpoint-postgres/poetry.lock @@ -266,7 +266,7 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0" [[package]] name = "langgraph-checkpoint" -version = "1.0.1" +version = "1.0.6" description = "Library with base interfaces for LangGraph checkpoint savers." optional = false python-versions = "^3.9.0,<4.0" @@ -969,4 +969,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "422b6d716b86db072ea3a612287ad20ff5700c18f22d9e9d59cc4e198514519d" +content-hash = "cfa417cbaf126fa847242ef81e9af3e4d13e93f25631bb4c007cdc69926ff3b8" diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml index d7f6899b0..ddf3a8107 100644 --- a/libs/checkpoint-postgres/pyproject.toml +++ b/libs/checkpoint-postgres/pyproject.toml @@ -12,7 +12,8 @@ packages = [{ include = "langgraph" }] python = "^3.9.0,<4.0" langgraph-checkpoint = "^1.0.1" orjson = ">=3.10.1" -psycopg = {extras = ["binary"], version = ">=3.1.19"} +psycopg = "^3.0.0" +psycopg-pool = "^3.0.0" [tool.poetry.group.dev.dependencies] ruff = "^0.1.4" @@ -23,7 +24,7 @@ pytest-asyncio = "^0.21.1" pytest-mock = "^3.11.1" pytest-watch = "^4.2.0" mypy = "^1.10.0" -psycopg-pool = "^3.2.2" +psycopg = {extras = ["binary"], version = ">=3.0.0"} langgraph-checkpoint = {path = "../checkpoint", develop = true} [tool.pytest.ini_options] diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index aa0054283..caa98200a 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -1832,7 +1832,7 @@ types-requests = ">=2.31.0.2,<3.0.0.0" [[package]] name = "langgraph-checkpoint" -version = "1.0.2" +version = "1.0.6" description = "Library with base interfaces for LangGraph checkpoint savers." optional = false python-versions = "^3.9.0,<4.0" @@ -1848,7 +1848,7 @@ url = "../checkpoint" [[package]] name = "langgraph-checkpoint-postgres" -version = "1.0.0" +version = "1.0.3" description = "Library with a Postgres implementation of LangGraph checkpoint saver." optional = false python-versions = "^3.9.0,<4.0" @@ -1858,7 +1858,8 @@ develop = true [package.dependencies] langgraph-checkpoint = "^1.0.1" orjson = ">=3.10.1" -psycopg = {version = ">=3.1.19", extras = ["binary"]} +psycopg = "^3.0.0" +psycopg-pool = "^3.0.0" [package.source] type = "directory" @@ -2638,6 +2639,20 @@ files = [ {file = "psycopg_binary-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:921f0c7f39590763d64a619de84d1b142587acc70fd11cbb5ba8fa39786f3073"}, ] +[[package]] +name = "psycopg-pool" +version = "3.2.2" +description = "Connection Pool for Psycopg" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg_pool-3.2.2-py3-none-any.whl", hash = "sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153"}, + {file = "psycopg_pool-3.2.2.tar.gz", hash = "sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c"}, +] + +[package.dependencies] +typing-extensions = ">=4.4" + [[package]] name = "ptyprocess" version = "0.7.0" @@ -4294,4 +4309,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9.0,<4.0" -content-hash = "4ef9e25016072ce08554c8ff8d091104fb59da7cdab9a128312bd787e6f35146" +content-hash = "b4d234e851639ecb33b6507b5e396b8b87aae6395c327441d04a7195eae72582" diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 35164c2f4..4e14ee41e 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -35,6 +35,7 @@ pytest-repeat = "^0.9.3" langgraph-checkpoint = {path = "../checkpoint", develop = true} langgraph-checkpoint-sqlite = {path = "../checkpoint-sqlite", develop = true} langgraph-checkpoint-postgres = {path = "../checkpoint-postgres", develop = true} +psycopg = {extras = ["binary"], version = ">=3.0.0"} [tool.poetry.group.dev] optional = true diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index 91096bca6..d273ea9a7 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -135,6 +135,40 @@ ''' # --- +# name: test_branch_then[postgres_pool] + ''' + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + + ''' +# --- +# name: test_branch_then[postgres_pool].1 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + prepare(prepare) + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + finish(finish) + __end__([__end__]):::last + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- # name: test_branch_then[sqlite] ''' graph TD; @@ -1772,6 +1806,351 @@ ''' # --- +# name: test_conditional_graph[postgres_pool] + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableAssign" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres_pool].1 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres_pool].2 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent) + tools(tools
version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_conditional_graph[postgres_pool].3 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": 1, + "type": "schema", + "data": "ParallelInput" + }, + { + "id": 2, + "type": "schema", + "data": "ParallelOutput" + }, + { + "id": 3, + "type": "runnable", + "data": { + "id": [ + "langchain", + "prompts", + "prompt", + "PromptTemplate" + ], + "name": "PromptTemplate" + } + }, + { + "id": 4, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "language_models", + "fake", + "FakeStreamingListLLM" + ], + "name": "FakeStreamingListLLM" + } + }, + { + "id": 5, + "type": "runnable", + "data": { + "id": [ + "langchain_core", + "runnables", + "base", + "RunnableLambda" + ], + "name": "agent_parser" + } + }, + { + "id": 6, + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnablePassthrough" + ], + "name": "Passthrough" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + }, + "metadata": { + "version": 2, + "variant": "b" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": 3, + "target": 4 + }, + { + "source": 4, + "target": 5 + }, + { + "source": 1, + "target": 3 + }, + { + "source": 5, + "target": 2 + }, + { + "source": 1, + "target": 6 + }, + { + "source": 6, + "target": 2 + }, + { + "source": "__start__", + "target": 1 + }, + { + "source": "tools", + "target": 1 + }, + { + "source": 2, + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": 2, + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_graph[postgres_pool].4 + ''' + graph TD; + PromptTemplate --> FakeStreamingListLLM; + FakeStreamingListLLM --> agent_parser; + Parallel_agent_outcome_Input --> PromptTemplate; + agent_parser --> Parallel_agent_outcome_Output; + Parallel_agent_outcome_Input --> Passthrough; + Passthrough --> Parallel_agent_outcome_Output; + __start__ --> Parallel_agent_outcome_Input; + tools --> Parallel_agent_outcome_Input; + Parallel_agent_outcome_Output -.  continue  .-> tools; + Parallel_agent_outcome_Output -.  exit  .-> __end__; + + ''' +# --- +# name: test_conditional_graph[postgres_pool].5 + dict({ + 'edges': list([ + dict({ + 'source': '__start__', + 'target': 'agent', + }), + dict({ + 'source': 'tools', + 'target': 'agent', + }), + dict({ + 'conditional': True, + 'data': 'continue', + 'source': 'agent', + 'target': 'tools', + }), + dict({ + 'conditional': True, + 'data': 'exit', + 'source': 'agent', + 'target': '__end__', + }), + ]), + 'nodes': list([ + dict({ + 'data': '__start__', + 'id': '__start__', + 'type': 'schema', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langchain', + 'schema', + 'runnable', + 'RunnableAssign', + ]), + 'name': 'agent', + }), + 'id': 'agent', + 'metadata': dict({ + '__interrupt': 'after', + }), + 'type': 'runnable', + }), + dict({ + 'data': dict({ + 'id': list([ + 'langgraph', + 'utils', + 'RunnableCallable', + ]), + 'name': 'tools', + }), + 'id': 'tools', + 'metadata': dict({ + 'variant': 'b', + 'version': 2, + }), + 'type': 'runnable', + }), + dict({ + 'data': '__end__', + 'id': '__end__', + 'type': 'schema', + }), + ]), + }) +# --- +# name: test_conditional_graph[postgres_pool].6 + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + agent(agent
__interrupt = after) + tools(tools
version = 2 + variant = b) + __end__([__end__]):::last + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- # name: test_conditional_graph[sqlite] ''' { @@ -2511,6 +2890,87 @@ ''' # --- +# name: test_conditional_state_graph[postgres_pool] + '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' +# --- +# name: test_conditional_state_graph[postgres_pool].1 + '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' +# --- +# name: test_conditional_state_graph[postgres_pool].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "langchain", + "schema", + "runnable", + "RunnableSequence" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "exit", + "conditional": true + } + ] + } + ''' +# --- +# name: test_conditional_state_graph[postgres_pool].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  exit  .-> __end__; + + ''' +# --- # name: test_conditional_state_graph[sqlite] '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' # --- @@ -2662,6 +3122,19 @@ ''' # --- +# name: test_in_one_fan_out_state_graph_waiting_edge[postgres_pool] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + + ''' +# --- # name: test_in_one_fan_out_state_graph_waiting_edge[sqlite] ''' graph TD; @@ -2968,6 +3441,76 @@ 'type': 'object', }) # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pool] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pool].1 + dict({ + 'definitions': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/definitions/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[postgres_pool].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1[sqlite] ''' graph TD; @@ -3318,6 +3861,76 @@ 'type': 'object', }) # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pool] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pool].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + ]), + 'title': 'Input', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_pool].2 + dict({ + 'properties': dict({ + 'answer': dict({ + 'title': 'Answer', + 'type': 'string', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + }), + 'required': list([ + 'answer', + 'docs', + ]), + 'title': 'Output', + 'type': 'object', + }) +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite] ''' graph TD; @@ -3440,6 +4053,19 @@ ''' # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres_pool] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[sqlite] ''' graph TD; @@ -3777,6 +4403,87 @@ ''' # --- +# name: test_message_graph[postgres_pool] + '{"title": "LangGraphInput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' +# --- +# name: test_message_graph[postgres_pool].1 + '{"title": "LangGraphOutput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' +# --- +# name: test_message_graph[postgres_pool].2 + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "agent", + "type": "runnable", + "data": { + "id": [ + "tests", + "test_pregel", + "FakeFuntionChatModel" + ], + "name": "agent" + } + }, + { + "id": "tools", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "prebuilt", + "tool_node", + "ToolNode" + ], + "name": "tools" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "__start__", + "target": "agent" + }, + { + "source": "tools", + "target": "agent" + }, + { + "source": "agent", + "target": "tools", + "data": "continue", + "conditional": true + }, + { + "source": "agent", + "target": "__end__", + "data": "end", + "conditional": true + } + ] + } + ''' +# --- +# name: test_message_graph[postgres_pool].3 + ''' + graph TD; + __start__ --> agent; + tools --> agent; + agent -.  continue  .-> tools; + agent -.  end  .-> __end__; + + ''' +# --- # name: test_message_graph[sqlite] '{"title": "LangGraphInput", "type": "array", "items": {"anyOf": [{"$ref": "#/definitions/AIMessage"}, {"$ref": "#/definitions/HumanMessage"}, {"$ref": "#/definitions/ChatMessage"}, {"$ref": "#/definitions/SystemMessage"}, {"$ref": "#/definitions/FunctionMessage"}, {"$ref": "#/definitions/ToolMessage"}]}, "definitions": {"ToolCall": {"title": "ToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "object"}, "id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "enum": ["tool_call"], "type": "string"}}, "required": ["name", "args", "id"]}, "InvalidToolCall": {"title": "InvalidToolCall", "type": "object", "properties": {"name": {"title": "Name", "type": "string"}, "args": {"title": "Args", "type": "string"}, "id": {"title": "Id", "type": "string"}, "error": {"title": "Error", "type": "string"}, "type": {"title": "Type", "enum": ["invalid_tool_call"], "type": "string"}}, "required": ["name", "args", "id", "error"]}, "UsageMetadata": {"title": "UsageMetadata", "type": "object", "properties": {"input_tokens": {"title": "Input Tokens", "type": "integer"}, "output_tokens": {"title": "Output Tokens", "type": "integer"}, "total_tokens": {"title": "Total Tokens", "type": "integer"}}, "required": ["input_tokens", "output_tokens", "total_tokens"]}, "AIMessage": {"title": "AIMessage", "description": "Message from an AI.\\n\\nAIMessage is returned from a chat model as a response to a prompt.\\n\\nThis message represents the output of the model and consists of both\\nthe raw output as returned by the model together standardized fields\\n(e.g., tool calls, usage metadata) added by the LangChain framework.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "ai", "enum": ["ai"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}, "tool_calls": {"title": "Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/ToolCall"}}, "invalid_tool_calls": {"title": "Invalid Tool Calls", "default": [], "type": "array", "items": {"$ref": "#/definitions/InvalidToolCall"}}, "usage_metadata": {"$ref": "#/definitions/UsageMetadata"}}, "required": ["content"]}, "HumanMessage": {"title": "HumanMessage", "description": "Message from a human.\\n\\nHumanMessages are messages that are passed in from a human to the model.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Instantiate a chat model and invoke it with the messages\\n model = ...\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "human", "enum": ["human"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "example": {"title": "Example", "default": false, "type": "boolean"}}, "required": ["content"]}, "ChatMessage": {"title": "ChatMessage", "description": "Message that can be assigned an arbitrary speaker (i.e. role).", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "chat", "enum": ["chat"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "role": {"title": "Role", "type": "string"}}, "required": ["content", "role"]}, "SystemMessage": {"title": "SystemMessage", "description": "Message for priming AI behavior.\\n\\nThe system message is usually passed in as the first of a sequence\\nof input messages.\\n\\nExample:\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import HumanMessage, SystemMessage\\n\\n messages = [\\n SystemMessage(\\n content=\\"You are a helpful assistant! Your name is Bob.\\"\\n ),\\n HumanMessage(\\n content=\\"What is your name?\\"\\n )\\n ]\\n\\n # Define a chat model and invoke it with the messages\\n print(model.invoke(messages))", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "system", "enum": ["system"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content"]}, "FunctionMessage": {"title": "FunctionMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nFunctionMessage are an older version of the ToolMessage schema, and\\ndo not contain the tool_call_id field.\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "function", "enum": ["function"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "name"]}, "ToolMessage": {"title": "ToolMessage", "description": "Message for passing the result of executing a tool back to a model.\\n\\nToolMessages contain the result of a tool invocation. Typically, the result\\nis encoded inside the `content` field.\\n\\nExample: A ToolMessage representing a result of 42 from a tool call with id\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n ToolMessage(content=\'42\', tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\')\\n\\n\\nExample: A ToolMessage where only part of the tool output is sent to the model\\n and the full output is passed in to artifact.\\n\\n .. versionadded:: 0.2.17\\n\\n .. code-block:: python\\n\\n from langchain_core.messages import ToolMessage\\n\\n tool_output = {\\n \\"stdout\\": \\"From the graph we can see that the correlation between x and y is ...\\",\\n \\"stderr\\": None,\\n \\"artifacts\\": {\\"type\\": \\"image\\", \\"base64_data\\": \\"/9j/4gIcSU...\\"},\\n }\\n\\n ToolMessage(\\n content=tool_output[\\"stdout\\"],\\n artifact=tool_output,\\n tool_call_id=\'call_Jja7J89XsjrOLA5r!MEOW!SL\',\\n )\\n\\nThe tool_call_id field is used to associate the tool call request with the\\ntool call response. This is useful in situations where a chat model is able\\nto request multiple tool calls in parallel.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "default": "tool", "enum": ["tool"], "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}, "tool_call_id": {"title": "Tool Call Id", "type": "string"}, "artifact": {"title": "Artifact"}, "status": {"title": "Status", "default": "success", "enum": ["success", "error"], "type": "string"}}, "required": ["content", "tool_call_id"]}}}' # --- @@ -4299,6 +5006,24 @@ ''' # --- +# name: test_start_branch_then[postgres_pool] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + tool_two_slow(tool_two_slow) + tool_two_fast(tool_two_fast) + __end__([__end__]):::last + __start__ -.-> tool_two_slow; + tool_two_slow --> __end__; + __start__ -.-> tool_two_fast; + tool_two_fast --> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- # name: test_start_branch_then[sqlite] ''' %%{init: {'flowchart': {'curve': 'linear'}}}%% diff --git a/libs/langgraph/tests/conftest.py b/libs/langgraph/tests/conftest.py index 1219ff0f1..bb6ef9603 100644 --- a/libs/langgraph/tests/conftest.py +++ b/libs/langgraph/tests/conftest.py @@ -7,6 +7,7 @@ from uuid import UUID, uuid4 import pytest from psycopg import AsyncConnection, Connection +from psycopg_pool import AsyncConnectionPool, ConnectionPool from pytest_mock import MockerFixture from langgraph.checkpoint.postgres import PostgresSaver @@ -122,6 +123,26 @@ def checkpointer_postgres_pipe(): conn.execute(f"DROP DATABASE {database}") +@pytest.fixture(scope="function") +def checkpointer_postgres_pool(): + database = f"test_{uuid4().hex[:16]}" + # create unique db + with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn: + conn.execute(f"CREATE DATABASE {database}") + try: + # yield checkpointer + with ConnectionPool( + DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True} + ) as pool: + checkpointer = PostgresSaver(pool) + checkpointer.setup() + yield checkpointer + finally: + # drop unique db + with Connection.connect(DEFAULT_POSTGRES_URI, autocommit=True) as conn: + conn.execute(f"DROP DATABASE {database}") + + @pytest.fixture(scope="function") def checkpointer_postgres_aio(): if sys.version_info < (3, 10): @@ -185,3 +206,51 @@ async def _checkpointer_postgres_aio_pipe(): DEFAULT_POSTGRES_URI, autocommit=True ) as conn: await conn.execute(f"DROP DATABASE {database}") + + +@pytest.fixture(scope="function") +def checkpointer_postgres_aio_pool(): + if sys.version_info < (3, 10): + pytest.skip("Async Postgres tests require Python 3.10+") + with agen_to_gen(_checkpointer_postgres_aio_pool()) as checkpointer: + yield checkpointer + + +@asynccontextmanager +async def _checkpointer_postgres_aio_pool(): + database = f"test_{uuid4().hex[:16]}" + # create unique db + async with await AsyncConnection.connect( + DEFAULT_POSTGRES_URI, autocommit=True + ) as conn: + await conn.execute(f"CREATE DATABASE {database}") + try: + # yield checkpointer + async with AsyncConnectionPool( + DEFAULT_POSTGRES_URI + database, max_size=10, kwargs={"autocommit": True} + ) as pool: + checkpointer = AsyncPostgresSaver(pool) + await checkpointer.setup() + yield checkpointer + finally: + # drop unique db + async with await AsyncConnection.connect( + DEFAULT_POSTGRES_URI, autocommit=True + ) as conn: + await conn.execute(f"DROP DATABASE {database}") + + +ALL_CHECKPOINTERS_SYNC = [ + "memory", + "sqlite", + "postgres", + "postgres_pipe", + "postgres_pool", +] +ALL_CHECKPOINTERS_ASYNC = [ + "memory", + "sqlite_aio", + "postgres_aio", + "postgres_aio_pipe", + "postgres_aio_pool", +] diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 685eb7c87..2c28c4ec0 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -70,6 +70,7 @@ from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore from tests.any_str import AnyStr, AnyVersion, UnsortedSequence +from tests.conftest import ALL_CHECKPOINTERS_SYNC from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -593,10 +594,7 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert step == 2 -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_invoke_two_processes_in_out_interrupt( request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture ) -> None: @@ -804,10 +802,7 @@ def test_invoke_two_processes_in_out_interrupt( ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_fork_always_re_runs_nodes( request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture ) -> None: @@ -1248,10 +1243,7 @@ def test_invoke_two_processes_two_in_two_out_valid(mocker: MockerFixture) -> Non assert app.invoke(2) == [3, 3] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_invoke_checkpoint_two( mocker: MockerFixture, request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -1324,10 +1316,7 @@ def test_invoke_checkpoint_two( assert checkpoint["channel_values"].get("total") == 5 -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_pending_writes_resume( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -1606,10 +1595,7 @@ async def test_checkpointer_null_pending_writes() -> None: ] * 4 -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_invoke_checkpoint_three( mocker: MockerFixture, request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -1939,10 +1925,7 @@ def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: assert cleanup.call_count == 1, "Expected cleanup to be called once" -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_conditional_graph( snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -2802,10 +2785,7 @@ def test_conditional_entrypoint_to_multiple_state_graph( } -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_conditional_state_graph( snapshot: SnapshotAssertion, mocker: MockerFixture, @@ -4060,10 +4040,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_state_graph_packets( request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture ) -> None: @@ -4592,10 +4569,7 @@ def test_state_graph_packets( ) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_message_graph( snapshot: SnapshotAssertion, deterministic_uuids: MockerFixture, @@ -5323,10 +5297,7 @@ def test_message_graph( ) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_root_graph( deterministic_uuids: MockerFixture, request: pytest.FixtureRequest, @@ -6400,10 +6371,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None: ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_dynamic_interrupt( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -6487,10 +6455,7 @@ def test_dynamic_interrupt( ) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_start_branch_then( snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -6680,10 +6645,7 @@ def test_start_branch_then( ) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_branch_then( snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -7210,10 +7172,7 @@ def test_branch_then( ) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_in_one_fan_out_state_graph_waiting_edge( snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -7341,10 +7300,7 @@ def test_in_one_fan_out_state_graph_waiting_edge( ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_in_one_fan_out_state_graph_waiting_edge_via_branch( snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -7438,10 +7394,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_via_branch( ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1( snapshot: SnapshotAssertion, mocker: MockerFixture, @@ -7609,10 +7562,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic1( } -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2( snapshot: SnapshotAssertion, mocker: MockerFixture, @@ -7779,10 +7729,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2( } -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_in_one_fan_out_state_graph_waiting_edge_plus_regular( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -8254,10 +8201,7 @@ def test_nested_graph(snapshot: SnapshotAssertion) -> None: @pytest.mark.repeat(10) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -9949,10 +9893,7 @@ def test_nested_graph_interrupts( ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_nested_graph_interrupts_parallel( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -10076,10 +10017,7 @@ def test_nested_graph_interrupts_parallel( ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_doubly_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -10174,10 +10112,7 @@ def test_doubly_nested_graph_interrupts( ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_nested_graph_state( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -10609,10 +10544,7 @@ def test_nested_graph_state( assert app.get_state(actual_snapshot.config) == expected_snapshot -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_doubly_nested_graph_state( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -10867,11 +10799,7 @@ def test_doubly_nested_graph_state( ) -@pytest.mark.repeat(10) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_send_to_nested_graphs( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -11320,10 +11248,7 @@ def test_checkpoint_metadata() -> None: assert chkpnt_tuple.metadata["test_config_4"] == "bar" -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_remove_message_via_state_update( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -11507,10 +11432,7 @@ def test_xray_lance(snapshot: SnapshotAssertion): assert graph.get_graph(xray=1).to_json() == snapshot -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_channel_values(request: pytest.FixtureRequest, checkpointer_name: str) -> None: checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 50aafae86..06d9578f3 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -70,6 +70,7 @@ from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore from tests.any_str import AnyStr, AnyVersion, UnsortedSequence +from tests.conftest import ALL_CHECKPOINTERS_ASYNC from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, @@ -214,10 +215,7 @@ async def test_node_cancellation_on_other_node_exception() -> None: assert inner_task_cancelled -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_dynamic_interrupt( checkpointer_name: str, snapshot: SnapshotAssertion, request: pytest.FixtureRequest ) -> None: @@ -305,10 +303,7 @@ async def test_dynamic_interrupt( # TODO use aget_state_history -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_node_not_cancelled_on_other_node_interrupted( checkpointer_name: str, request: pytest.FixtureRequest ) -> None: @@ -390,10 +385,7 @@ async def test_step_timeout_on_stream_hang() -> None: assert inner_task_cancelled -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_cancel_graph_astream( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -463,10 +455,7 @@ async def test_cancel_graph_astream( assert state.metadata == {"source": "loop", "step": 0, "writes": None} -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe", None], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_cancel_graph_astream_events_v2( request: pytest.FixtureRequest, checkpointer_name: Optional[str] ) -> None: @@ -820,10 +809,7 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert step == 2 -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_invoke_two_processes_in_out_interrupt( request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture ) -> None: @@ -1038,10 +1024,7 @@ async def test_invoke_two_processes_in_out_interrupt( ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_fork_always_re_runs_nodes( request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture ) -> None: @@ -1546,10 +1529,7 @@ async def test_invoke_checkpoint(mocker: MockerFixture) -> None: assert checkpoint["channel_values"].get("total") == 5 -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_pending_writes_resume( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -1810,10 +1790,7 @@ async def test_cond_edge_after_send() -> None: assert await graph.ainvoke(["0"]) == ["0", "1", "2", "2", "3"] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_invoke_checkpoint_three( mocker: MockerFixture, request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -2155,10 +2132,7 @@ async def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: assert cleanup_async.call_count == 1, "Expected cleanup to be called once" -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_conditional_graph( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -5036,10 +5010,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_start_branch_then( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -5243,10 +5214,7 @@ async def test_start_branch_then( ) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_branch_then( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -6730,10 +6698,7 @@ async def test_nested_graph(snapshot: SnapshotAssertion) -> None: @pytest.mark.repeat(10) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -8443,10 +8408,7 @@ async def test_nested_graph_interrupts( ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_nested_graph_interrupts_parallel( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -8572,10 +8534,7 @@ async def test_nested_graph_interrupts_parallel( ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_doubly_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -8673,10 +8632,7 @@ async def test_doubly_nested_graph_interrupts( ] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_nested_graph_state( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -9113,10 +9069,7 @@ async def test_nested_graph_state( assert await app.aget_state(actual_snapshot.config) == expected_snapshot -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_doubly_nested_graph_state( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: @@ -9375,11 +9328,7 @@ async def test_doubly_nested_graph_state( ) -@pytest.mark.repeat(10) -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_send_to_nested_graphs( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: From c88d289a73622718abe64f0fd05536d373cb75ef Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Tue, 27 Aug 2024 11:34:26 -0400 Subject: [PATCH 144/287] checkpoint-postgres: release 1.0.4 (#1493) --- libs/checkpoint-postgres/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml index ddf3a8107..82926f1b4 100644 --- a/libs/checkpoint-postgres/pyproject.toml +++ b/libs/checkpoint-postgres/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint-postgres" -version = "1.0.3" +version = "1.0.4" description = "Library with a Postgres implementation of LangGraph checkpoint saver." authors = [] license = "MIT" From 8c2fefcb285a688e5bd4235933c625775c53e85a Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 27 Aug 2024 08:42:55 -0700 Subject: [PATCH 145/287] sdk-py: Add values to Thread schema --- libs/sdk-py/langgraph_sdk/schema.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/sdk-py/langgraph_sdk/schema.py b/libs/sdk-py/langgraph_sdk/schema.py index 29fa903a9..7007a9ad1 100644 --- a/libs/sdk-py/langgraph_sdk/schema.py +++ b/libs/sdk-py/langgraph_sdk/schema.py @@ -85,6 +85,8 @@ class Thread(TypedDict): """The thread metadata.""" status: ThreadStatus """The status of the thread, one of 'idle', 'busy', 'interrupted'.""" + values: dict + """The current state of the thread.""" class ThreadState(TypedDict): From 3dbf6f144e9e85719a354ff8bb3f9f4d9286f74c Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:26:28 -0700 Subject: [PATCH 146/287] [docs]: structured output from tool calling agent (#1480) * first draft * vadym comments * Update docs/mkdocs.yml Co-authored-by: Vadym Barda * vadym comments * spellign * more comments * spelling * final nits --------- Co-authored-by: Vadym Barda --- docs/_scripts/copy_notebooks.py | 1 + docs/docs/how-tos/index.md | 1 + docs/mkdocs.yml | 1 + examples/react-agent-structured-output.ipynb | 238 +++++++++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 examples/react-agent-structured-output.ipynb diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index caf2c6781..41970fa21 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -62,6 +62,7 @@ _MANUAL = { "human_in_the_loop/wait-user-input.ipynb", "human_in_the_loop/review-tool-calls.ipynb", "node-retries.ipynb", + "react-agent-structured-output.ipynb", ], "tutorials": [ "introduction.ipynb", diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 43b0c9265..ac32c1ad2 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -79,6 +79,7 @@ These guides show how to use different streaming modes. - [How to use a Pydantic model as your state](state-model.ipynb) - [How to use a context object in state](state-context-key.ipynb) - [How to add node retries](node-retries.ipynb) +- [How to force function calling agent to structure output](react-agent-structured-output.ipynb) ## Prebuilt ReAct Agent diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 1719b1cca..ec1f9faab 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -170,6 +170,7 @@ nav: - Visualize your graph: how-tos/visualization.ipynb - Add runtime configuration: how-tos/configuration.ipynb - Add node retries: how-tos/node-retries.ipynb + - How to force function calling agent to structure output: how-tos/react-agent-structured-output.ipynb - Prebuilt ReAct Agent: - Create a ReAct agent: how-tos/create-react-agent.ipynb - Add memory to a ReAct agent: how-tos/create-react-agent-memory.ipynb diff --git a/examples/react-agent-structured-output.ipynb b/examples/react-agent-structured-output.ipynb new file mode 100644 index 000000000..91e2b0dc6 --- /dev/null +++ b/examples/react-agent-structured-output.ipynb @@ -0,0 +1,238 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to force function calling agent to structure output\n", + "\n", + "You might want your agent to return its output in a structured format. For example, if the output of the agent is used by some other downstream software, you may want the output to be in the same structured format every time the agent is invoked to ensure consistency.\n", + "\n", + "This guide shows how you can do this. We will be using a basic [ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/) (a model node and a tool-calling node) together with a third node at the end that will format response for the user.\n", + "\n", + "\n", + "## Setup\n", + "\n", + "### Structured Output\n", + "\n", + "First we need to define how we want to structure our output. To do this, we will use the `with_structured_output` method from LangChain, which you can read more about [here](https://python.langchain.com/v0.2/docs/how_to/structured_output/)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from pydantic import BaseModel, Field\n", + "\n", + "class WeatherResponse(BaseModel):\n", + " temperature: float = Field(description=\"The temperature in fahrenheit\")\n", + " wind_directon: str = Field(description=\"The direction of the wind in abbreviated form\")\n", + " wind_speed: float = Field(description=\"The speed of the wind in km/h\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Graph State\n", + "\n", + "We can now define our graph state:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Annotated, Any\n", + "from typing_extensions import TypedDict\n", + "from langgraph.graph.message import add_messages\n", + "\n", + "class AgentState(TypedDict):\n", + " # list of chat messages from user, LLM, and tools\n", + " messages: Annotated[list, add_messages]\n", + " # Final structured response from the agent\n", + " final_response: WeatherResponse" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tools + Models\n", + "\n", + "We can now instantiate the tools and models we are going to use in our graph. We are going to use a single tool in this example for finding the weather, and we are going to have two models in our graph, one that does the function calling and one that does the responding." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Literal\n", + "from langchain_core.tools import tool\n", + "from langchain_anthropic import ChatAnthropic\n", + "\n", + "@tool\n", + "def get_weather(city: Literal[\"nyc\", \"sf\"]):\n", + " \"\"\"Use this to get weather information.\"\"\"\n", + " if city == \"nyc\":\n", + " return \"It is cloudy in NYC, with 5 mph winds in the North-East direction and a temperature of 70 degrees\"\n", + " elif city == \"sf\":\n", + " return \"It is 75 degrees and sunny in SF, with 3 mph winds in the South-East direction\"\n", + " else:\n", + " raise AssertionError(\"Unknown city\")\n", + " \n", + "tools = [get_weather]\n", + " \n", + "model = ChatAnthropic(model=\"claude-3-opus-20240229\")\n", + " \n", + "model_with_tools = model.bind_tools(tools)\n", + "model_with_structured_output = model.with_structured_output(WeatherResponse)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define Graph\n", + "\n", + "Now that we have defined our tools and models, we can define our graph." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "from langgraph.prebuilt import ToolNode\n", + "from langchain_core.messages import HumanMessage\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state: AgentState):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we respond to the user\n", + " if not last_message.tool_calls:\n", + " return \"respond\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state: AgentState):\n", + " response = model_with_tools.invoke(state['messages'])\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function that responds to the user\n", + "def respond(state: AgentState):\n", + " # We call the model with structured output in order to return the same format to the user every time\n", + " # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n", + " # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n", + " response = model_with_structured_output.invoke([HumanMessage(content=state['messages'][-2].content)])\n", + " # We return the final answer\n", + " return {\"final_response\": response}\n", + "\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"respond\", respond)\n", + "workflow.add_node(\"tools\", ToolNode(tools))\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " \"agent\",\n", + " should_continue,\n", + " {\n", + " \"continue\": \"tools\",\n", + " \"respond\": \"respond\",\n", + " },\n", + ")\n", + "\n", + "workflow.add_edge(\"tools\", \"agent\")\n", + "workflow.add_edge(\"respond\", END)\n", + "graph = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Usage\n", + "\n", + "We can now invoke our graph to verify that the output is being structured as desired:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})['final_response']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "WeatherResponse(temperature=75.0, wind_directon='SE', wind_speed=3.0)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, the agent returned a `WeatherResponse` object as we expected. If would now be easy to use this agent in a more complex software stack without having to worry about the output of the agent not matching the format expected from the next step in the stack." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 330c91f622b0a5ff907c9c6bc7ace24befd10400 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 27 Aug 2024 10:50:39 -0700 Subject: [PATCH 147/287] Update ruff --- libs/checkpoint-postgres/Makefile | 6 +- libs/checkpoint-postgres/poetry.lock | 43 ++++++------ libs/checkpoint-postgres/pyproject.toml | 2 +- libs/checkpoint-sqlite/Makefile | 6 +- libs/checkpoint-sqlite/poetry.lock | 43 ++++++------ libs/checkpoint-sqlite/pyproject.toml | 2 +- libs/checkpoint/Makefile | 6 +- .../langgraph/checkpoint/serde/types.py | 33 +++------ libs/checkpoint/poetry.lock | 43 ++++++------ libs/checkpoint/pyproject.toml | 2 +- libs/cli/Makefile | 6 +- libs/cli/poetry.lock | 39 +++++------ libs/cli/pyproject.toml | 6 +- libs/langgraph/Makefile | 6 +- libs/langgraph/langgraph/graph/graph.py | 6 +- libs/langgraph/langgraph/managed/base.py | 9 +-- libs/langgraph/langgraph/prebuilt/__init__.py | 1 + libs/langgraph/langgraph/pregel/__init__.py | 14 ++-- libs/langgraph/langgraph/pregel/algo.py | 6 +- libs/langgraph/langgraph/pregel/executor.py | 3 +- libs/langgraph/langgraph/pregel/get_state.py | 8 +-- libs/langgraph/poetry.lock | 41 ++++++------ libs/langgraph/pyproject.toml | 2 +- libs/langgraph/tests/memory_assert.py | 6 +- libs/langgraph/tests/messages.py | 1 + libs/langgraph/tests/test_pregel.py | 3 +- libs/langgraph/tests/test_pregel_async.py | 67 ++++++++++--------- libs/sdk-py/Makefile | 6 +- libs/sdk-py/langgraph_sdk/client.py | 18 ++--- libs/sdk-py/poetry.lock | 41 ++++++------ libs/sdk-py/pyproject.toml | 2 +- 31 files changed, 237 insertions(+), 240 deletions(-) diff --git a/libs/checkpoint-postgres/Makefile b/libs/checkpoint-postgres/Makefile index 043fef898..7057f1a97 100644 --- a/libs/checkpoint-postgres/Makefile +++ b/libs/checkpoint-postgres/Makefile @@ -38,11 +38,11 @@ lint_tests: PYTHON_FILES=tests lint_tests: MYPY_CACHE=.mypy_cache_test lint lint_diff lint_package lint_tests: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) + poetry run ruff check --select I --fix $(PYTHON_FILES) diff --git a/libs/checkpoint-postgres/poetry.lock b/libs/checkpoint-postgres/poetry.lock index 0c171e445..f9ca0f97e 100644 --- a/libs/checkpoint-postgres/poetry.lock +++ b/libs/checkpoint-postgres/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -394,6 +394,8 @@ files = [ {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"}, + {file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"}, {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, @@ -822,28 +824,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.1.15" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -969,4 +972,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "cfa417cbaf126fa847242ef81e9af3e4d13e93f25631bb4c007cdc69926ff3b8" +content-hash = "b139531e8c6f4e24cea4bdfc29d111d1ea000a6a8a604ba81be4bff0977a2466" diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml index 82926f1b4..381f2f063 100644 --- a/libs/checkpoint-postgres/pyproject.toml +++ b/libs/checkpoint-postgres/pyproject.toml @@ -16,7 +16,7 @@ psycopg = "^3.0.0" psycopg-pool = "^3.0.0" [tool.poetry.group.dev.dependencies] -ruff = "^0.1.4" +ruff = "^0.6.2" codespell = "^2.2.0" pytest = "^7.2.1" anyio = "^4.4.0" diff --git a/libs/checkpoint-sqlite/Makefile b/libs/checkpoint-sqlite/Makefile index 734e46308..94b1963d6 100644 --- a/libs/checkpoint-sqlite/Makefile +++ b/libs/checkpoint-sqlite/Makefile @@ -24,11 +24,11 @@ lint_tests: PYTHON_FILES=tests lint_tests: MYPY_CACHE=.mypy_cache_test lint lint_diff lint_package lint_tests: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) + poetry run ruff check --select I --fix $(PYTHON_FILES) diff --git a/libs/checkpoint-sqlite/poetry.lock b/libs/checkpoint-sqlite/poetry.lock index cd7aa8376..0ea747b2c 100644 --- a/libs/checkpoint-sqlite/poetry.lock +++ b/libs/checkpoint-sqlite/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aiosqlite" @@ -380,6 +380,8 @@ files = [ {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"}, + {file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"}, {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, @@ -707,28 +709,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.1.15" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -832,4 +835,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0" -content-hash = "e073e1a73cdae1fae8ea46499c39e55980cb61c7c8bd6be774c64f80a627eb31" +content-hash = "d50ec7c6b55075d19193e080cc95d3b0998b9efa1a0ed407bd6055b5b5e867e0" diff --git a/libs/checkpoint-sqlite/pyproject.toml b/libs/checkpoint-sqlite/pyproject.toml index 0a39f96cb..28dd851fd 100644 --- a/libs/checkpoint-sqlite/pyproject.toml +++ b/libs/checkpoint-sqlite/pyproject.toml @@ -14,7 +14,7 @@ langgraph-checkpoint = "^1.0.1" aiosqlite = "^0.20.0" [tool.poetry.group.dev.dependencies] -ruff = "^0.1.4" +ruff = "^0.6.2" codespell = "^2.2.0" pytest = "^7.2.1" pytest-asyncio = "^0.21.1" diff --git a/libs/checkpoint/Makefile b/libs/checkpoint/Makefile index 734e46308..94b1963d6 100644 --- a/libs/checkpoint/Makefile +++ b/libs/checkpoint/Makefile @@ -24,11 +24,11 @@ lint_tests: PYTHON_FILES=tests lint_tests: MYPY_CACHE=.mypy_cache_test lint lint_diff lint_package lint_tests: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) + poetry run ruff check --select I --fix $(PYTHON_FILES) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/types.py b/libs/checkpoint/langgraph/checkpoint/serde/types.py index 71588cfa0..26f118bbc 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/types.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/types.py @@ -22,34 +22,26 @@ C = TypeVar("C") class ChannelProtocol(Protocol[Value, Update, C]): # Mirrors langgraph.channels.base.BaseChannel @property - def ValueType(self) -> Any: - ... + def ValueType(self) -> Any: ... @property - def UpdateType(self) -> Any: - ... + def UpdateType(self) -> Any: ... - def checkpoint(self) -> Optional[C]: - ... + def checkpoint(self) -> Optional[C]: ... def from_checkpoint( self, checkpoint: Optional[C], config: RunnableConfig - ) -> Generator[Self, None, None]: - ... + ) -> Generator[Self, None, None]: ... async def afrom_checkpoint( self, checkpoint: Optional[C], config: RunnableConfig - ) -> AsyncGenerator[Self, None]: - ... + ) -> AsyncGenerator[Self, None]: ... - def update(self, values: Sequence[Update]) -> bool: - ... + def update(self, values: Sequence[Update]) -> bool: ... - def get(self) -> Value: - ... + def get(self) -> Value: ... - def consume(self) -> bool: - ... + def consume(self) -> bool: ... @runtime_checkable @@ -59,11 +51,8 @@ class SendProtocol(Protocol): arg: Any id: str - def __hash__(self) -> int: - ... + def __hash__(self) -> int: ... - def __repr__(self) -> str: - ... + def __repr__(self) -> str: ... - def __eq__(self, value: object) -> bool: - ... + def __eq__(self, value: object) -> bool: ... diff --git a/libs/checkpoint/poetry.lock b/libs/checkpoint/poetry.lock index 7bed6ad81..ce3055a79 100644 --- a/libs/checkpoint/poetry.lock +++ b/libs/checkpoint/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -380,6 +380,8 @@ files = [ {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"}, + {file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"}, {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, @@ -707,28 +709,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.1.15" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -847,4 +850,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "3bee25f1adc1349de4358a88037693cec47f6a2b88b809490f9198e6313239f6" +content-hash = "de79db3dc7701542739b3417e9d3f02c3b41167719603ee4d08b92e23b7443ee" diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml index 0cdafc185..c6541e5ec 100644 --- a/libs/checkpoint/pyproject.toml +++ b/libs/checkpoint/pyproject.toml @@ -13,7 +13,7 @@ python = "^3.9.0,<4.0" langchain-core = ">=0.2.22,<0.3" [tool.poetry.group.dev.dependencies] -ruff = "^0.1.4" +ruff = "^0.6.2" codespell = "^2.2.0" pytest = "^7.2.1" pytest-asyncio = "^0.21.1" diff --git a/libs/cli/Makefile b/libs/cli/Makefile index 680c8f53c..424946fdf 100644 --- a/libs/cli/Makefile +++ b/libs/cli/Makefile @@ -21,11 +21,11 @@ lint_tests: PYTHON_FILES=tests lint_tests: MYPY_CACHE=.mypy_cache_test lint lint_diff lint_package lint_tests: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) \ No newline at end of file + poetry run ruff check --select I --fix $(PYTHON_FILES) diff --git a/libs/cli/poetry.lock b/libs/cli/poetry.lock index 4f6c66396..101f65ea6 100644 --- a/libs/cli/poetry.lock +++ b/libs/cli/poetry.lock @@ -236,28 +236,29 @@ watchdog = ">=0.6.0" [[package]] name = "ruff" -version = "0.1.6" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, - {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, - {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, - {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, - {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, - {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, - {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, - {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -324,4 +325,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "5efa2f1ed4bd611a45e5d43d7c3fb907a8fa4447e2d1c30ce26b830411e189dd" +content-hash = "a1b0cc1de3e63b8d419342e311cd1e32b81606e89d3f8cf448be560b5727c202" diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml index 02c373e95..d2ad29301 100644 --- a/libs/cli/pyproject.toml +++ b/libs/cli/pyproject.toml @@ -16,7 +16,7 @@ python = "^3.9.0,<4.0" click = "^8.1.7" [tool.poetry.group.dev.dependencies] -ruff = "^0.1.4" +ruff = "^0.6.2" codespell = "^2.2.0" pytest = "^7.2.1" pytest-asyncio = "^0.21.1" @@ -40,7 +40,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.ruff] -select = [ +lint.select = [ # pycodestyle "E", # Pyflakes @@ -52,4 +52,4 @@ select = [ # isort "I", ] -ignore = [ "E501", "B008" ] +lint.ignore = [ "E501", "B008" ] diff --git a/libs/langgraph/Makefile b/libs/langgraph/Makefile index fd513f65f..feecd3fba 100644 --- a/libs/langgraph/Makefile +++ b/libs/langgraph/Makefile @@ -51,14 +51,14 @@ lint_tests: PYTHON_FILES=tests lint_tests: MYPY_CACHE=.mypy_cache_test lint lint_diff lint_package lint_tests: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) + poetry run ruff check --select I --fix $(PYTHON_FILES) spell_check: poetry run codespell --toml pyproject.toml diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index 3ecbe726f..2eb732274 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -141,8 +141,7 @@ class Graph: node: RunnableLike, *, metadata: Optional[dict[str, Any]] = None, - ) -> None: - ... + ) -> None: ... @overload def add_node( @@ -151,8 +150,7 @@ class Graph: action: RunnableLike, *, metadata: Optional[dict[str, Any]] = None, - ) -> None: - ... + ) -> None: ... def add_node( self, diff --git a/libs/langgraph/langgraph/managed/base.py b/libs/langgraph/langgraph/managed/base.py index 5ce15aec2..b86388930 100644 --- a/libs/langgraph/langgraph/managed/base.py +++ b/libs/langgraph/langgraph/managed/base.py @@ -58,18 +58,15 @@ class ManagedValue(ABC, Generic[V]): pass @abstractmethod - def __call__(self, step: int) -> V: - ... + def __call__(self, step: int) -> V: ... class WritableManagedValue(Generic[V, U], ManagedValue[V], ABC): @abstractmethod - def update(self, writes: Sequence[U]) -> None: - ... + def update(self, writes: Sequence[U]) -> None: ... @abstractmethod - async def aupdate(self, writes: Sequence[U]) -> None: - ... + async def aupdate(self, writes: Sequence[U]) -> None: ... class ConfiguredManagedValue(NamedTuple): diff --git a/libs/langgraph/langgraph/prebuilt/__init__.py b/libs/langgraph/langgraph/prebuilt/__init__.py index 4ad055730..0554b8036 100644 --- a/libs/langgraph/langgraph/prebuilt/__init__.py +++ b/libs/langgraph/langgraph/prebuilt/__init__.py @@ -1,4 +1,5 @@ """langgraph.prebuilt exposes a higher-level API for creating and executing agents and tools.""" + from langgraph.prebuilt.chat_agent_executor import create_react_agent from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation from langgraph.prebuilt.tool_node import InjectedState, ToolNode, tools_condition diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index e5648951a..63995068e 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -119,8 +119,7 @@ class Channel: *, key: Optional[str] = None, tags: Optional[list[str]] = None, - ) -> PregelNode: - ... + ) -> PregelNode: ... @overload @classmethod @@ -130,8 +129,7 @@ class Channel: *, key: None = None, tags: Optional[list[str]] = None, - ) -> PregelNode: - ... + ) -> PregelNode: ... @classmethod def subscribe_to( @@ -792,6 +790,9 @@ class Pregel( }, ), ) + # save task writes + if saved: + self.checkpointer.put_writes(checkpoint_config, task.writes, task.id) # apply to checkpoint and save assert not apply_writes( checkpoint, channels, [task], self.checkpointer.get_next_version @@ -924,6 +925,11 @@ class Pregel( }, ), ) + # save task writes + if saved: + await self.checkpointer.aput_writes( + checkpoint_config, task.writes, task.id + ) # apply to checkpoint and save assert not apply_writes( checkpoint, channels, [task], self.checkpointer.get_next_version diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 5b6064306..39ca717b6 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -240,8 +240,7 @@ def prepare_next_tasks( is_resuming: bool = False, checkpointer: Literal[None] = None, manager: Literal[None] = None, -) -> list[PregelTask]: - ... +) -> list[PregelTask]: ... @overload @@ -257,8 +256,7 @@ def prepare_next_tasks( is_resuming: bool, checkpointer: Optional[BaseCheckpointSaver], manager: Union[None, ParentRunManager, AsyncParentRunManager], -) -> list[PregelExecutableTask]: - ... +) -> list[PregelExecutableTask]: ... def prepare_next_tasks( diff --git a/libs/langgraph/langgraph/pregel/executor.py b/libs/langgraph/langgraph/pregel/executor.py index 3e4243c61..3a4175cac 100644 --- a/libs/langgraph/langgraph/pregel/executor.py +++ b/libs/langgraph/langgraph/pregel/executor.py @@ -32,8 +32,7 @@ class Submit(Protocol[P, T]): __name__: Optional[str] = None, __cancel_on_exit__: bool = False, **kwargs: P.kwargs, - ) -> concurrent.futures.Future[T]: - ... + ) -> concurrent.futures.Future[T]: ... class BackgroundExecutor(ContextManager): diff --git a/libs/langgraph/langgraph/pregel/get_state.py b/libs/langgraph/langgraph/pregel/get_state.py index b8efbe6a2..457ab6b4f 100644 --- a/libs/langgraph/langgraph/pregel/get_state.py +++ b/libs/langgraph/langgraph/pregel/get_state.py @@ -24,10 +24,10 @@ def assemble_state_snapshot_hierarchy( **(parent_state_snapshot.subgraph_state_snapshots or {}), subgraph_node: state_snapshot, } - checkpoint_ns_to_state_snapshots[ - parent_checkpoint_ns - ] = checkpoint_ns_to_state_snapshots[parent_checkpoint_ns]._replace( - subgraph_state_snapshots=parent_subgraph_snapshots + checkpoint_ns_to_state_snapshots[parent_checkpoint_ns] = ( + checkpoint_ns_to_state_snapshots[ + parent_checkpoint_ns + ]._replace(subgraph_state_snapshots=parent_subgraph_snapshots) ) state_snapshot = checkpoint_ns_to_state_snapshots.pop(root_checkpoint_ns, None) diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index caa98200a..36bbe5651 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -3505,28 +3505,29 @@ files = [ [[package]] name = "ruff" -version = "0.1.15" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -4309,4 +4310,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9.0,<4.0" -content-hash = "b4d234e851639ecb33b6507b5e396b8b87aae6395c327441d04a7195eae72582" +content-hash = "7e0d6fd967987fcf46b038c47b5ca81d86cb8fff89704eac2074f2ce55336a2b" diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 4e14ee41e..8d1dfd694 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -25,7 +25,7 @@ pytest-watcher = "^0.4.1" langchain = ">=0.1.0" grandalf = "^0.8" mypy = "^1.6.0" -ruff = "^0.1.4" +ruff = "^0.6.2" jupyter = "^1.0.0" langchainhub = "^0.1.14" langchain-openai = ">=0.1.2" diff --git a/libs/langgraph/tests/memory_assert.py b/libs/langgraph/tests/memory_assert.py index 624a711c7..6b44051f7 100644 --- a/libs/langgraph/tests/memory_assert.py +++ b/libs/langgraph/tests/memory_assert.py @@ -57,9 +57,9 @@ class MemorySaverAssertImmutable(MemorySaver): ) == saved ) - self.storage_for_copies[thread_id][checkpoint_ns][ - checkpoint["id"] - ] = self.serde.dumps_typed(copy_checkpoint(checkpoint)) + self.storage_for_copies[thread_id][checkpoint_ns][checkpoint["id"]] = ( + self.serde.dumps_typed(copy_checkpoint(checkpoint)) + ) # call super to write checkpoint return super().put(config, checkpoint, metadata, new_versions) diff --git a/libs/langgraph/tests/messages.py b/libs/langgraph/tests/messages.py index be67db1f1..988a7332a 100644 --- a/libs/langgraph/tests/messages.py +++ b/libs/langgraph/tests/messages.py @@ -6,6 +6,7 @@ Please note that the `id` field is assigned AFTER the model is created to workaround an issue with pydantic ignoring the __eq__ method on subclassed strings. """ + from typing import Any from langchain_core.documents import Document diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 2c28c4ec0..492d87570 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -190,8 +190,7 @@ def test_graph_validation() -> None: with pytest.raises(ValueError, match="Found edge starting at unknown node "): graph.compile() - def bad_reducer(a): - ... + def bad_reducer(a): ... class BadReducerState(TypedDict): hello: Annotated[str, bad_reducer] diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 06d9578f3..4b1a0e72e 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -3865,7 +3865,10 @@ class ToolInput(BaseModel, arbitrary_types_allowed=True): my_session: httpx.AsyncClient -async def test_state_graph_packets() -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_state_graph_packets( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, ) @@ -3877,6 +3880,8 @@ async def test_state_graph_packets() -> None: ) from langchain_core.tools import tool + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + class AgentState(TypedDict): messages: Annotated[list[BaseMessage], add_messages] session: Annotated[httpx.AsyncClient, Context(httpx.AsyncClient)] @@ -4099,7 +4104,7 @@ async def test_state_graph_packets() -> None: ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -4180,6 +4185,7 @@ async def test_state_graph_packets() -> None: await app_w_interrupt.aupdate_state(config, {"messages": last_message}) # message was replaced instead of appended + tup = await app_w_interrupt.checkpointer.aget_tuple(config) assert await app_w_interrupt.aget_state(config) == StateSnapshot( values={ "messages": [ @@ -4199,10 +4205,8 @@ async def test_state_graph_packets() -> None: }, tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], + config=tup.config, + created_at=tup.checkpoint["ts"], metadata={ "source": "update", "step": 2, @@ -4260,6 +4264,7 @@ async def test_state_graph_packets() -> None: }, ] + tup = await app_w_interrupt.checkpointer.aget_tuple(config) assert await app_w_interrupt.aget_state(config) == StateSnapshot( values={ "messages": [ @@ -4301,10 +4306,8 @@ async def test_state_graph_packets() -> None: }, tasks=(PregelTask(AnyStr(), "tools"), PregelTask(AnyStr(), "tools")), next=("tools", "tools"), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], + config=tup.config, + created_at=tup.checkpoint["ts"], metadata={ "source": "loop", "step": 4, @@ -4340,6 +4343,7 @@ async def test_state_graph_packets() -> None: ) # replaces message even if object identity is different, as long as id is the same + tup = await app_w_interrupt.checkpointer.aget_tuple(config) assert await app_w_interrupt.aget_state(config) == StateSnapshot( values={ "messages": [ @@ -4366,10 +4370,8 @@ async def test_state_graph_packets() -> None: }, tasks=(), next=(), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], + config=tup.config, + created_at=tup.checkpoint["ts"], metadata={ "source": "update", "step": 5, @@ -4381,7 +4383,10 @@ async def test_state_graph_packets() -> None: ) -async def test_message_graph() -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_message_graph( + request: pytest.FixtureRequest, checkpointer_name: str +) -> None: from langchain_core.agents import AgentAction from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, @@ -4389,6 +4394,8 @@ async def test_message_graph() -> None: from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage from langchain_core.tools import tool + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + class FakeFuntionChatModel(FakeMessagesListChatModel): def bind_functions(self, functions: list): return self @@ -4552,7 +4559,7 @@ async def test_message_graph() -> None: ] app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), + checkpointer=checkpointer, interrupt_after=["agent"], ) config = {"configurable": {"thread_id": "1"}} @@ -4574,6 +4581,7 @@ async def test_message_graph() -> None: }, ] + tup = await app_w_interrupt.checkpointer.aget_tuple(config) assert await app_w_interrupt.aget_state(config) == StateSnapshot( values=[ _AnyIdHumanMessage(content="what is weather in sf"), @@ -4587,10 +4595,8 @@ async def test_message_graph() -> None: ], tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], + config=tup.config, + created_at=tup.checkpoint["ts"], metadata={ "source": "loop", "step": 1, @@ -4615,6 +4621,7 @@ async def test_message_graph() -> None: await app_w_interrupt.aupdate_state(config, last_message) # message was replaced instead of appended + tup = await app_w_interrupt.checkpointer.aget_tuple(config) assert await app_w_interrupt.aget_state(config) == StateSnapshot( values=[ _AnyIdHumanMessage(content="what is weather in sf"), @@ -4631,10 +4638,8 @@ async def test_message_graph() -> None: ], tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], + config=tup.config, + created_at=tup.checkpoint["ts"], metadata={ "source": "update", "step": 2, @@ -4675,6 +4680,7 @@ async def test_message_graph() -> None: }, ] + tup = await app_w_interrupt.checkpointer.aget_tuple(config) assert await app_w_interrupt.aget_state(config) == StateSnapshot( values=[ _AnyIdHumanMessage(content="what is weather in sf"), @@ -4703,10 +4709,8 @@ async def test_message_graph() -> None: ], tasks=(PregelTask(AnyStr(), "tools"),), next=("tools",), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], + config=tup.config, + created_at=tup.checkpoint["ts"], metadata={ "source": "loop", "step": 4, @@ -4734,6 +4738,7 @@ async def test_message_graph() -> None: ) # replaces message even if object identity is different, as long as id is the same + tup = await app_w_interrupt.checkpointer.aget_tuple(config) assert await app_w_interrupt.aget_state(config) == StateSnapshot( values=[ _AnyIdHumanMessage(content="what is weather in sf"), @@ -4756,10 +4761,8 @@ async def test_message_graph() -> None: ], tasks=(), next=(), - config=app_w_interrupt.checkpointer.get_tuple(config).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], + config=tup.config, + created_at=tup.checkpoint["ts"], metadata={ "source": "update", "step": 5, diff --git a/libs/sdk-py/Makefile b/libs/sdk-py/Makefile index 98dbab0f7..86c45abe9 100644 --- a/libs/sdk-py/Makefile +++ b/libs/sdk-py/Makefile @@ -14,11 +14,11 @@ lint format: PYTHON_FILES=. lint_diff format_diff: PYTHON_FILES=$(shell git diff --name-only --relative --diff-filter=d main . | grep -E '\.py$$|\.ipynb$$') lint lint_diff: - poetry run ruff . + poetry run ruff check . [ "$(PYTHON_FILES)" = "" ] || poetry run ruff format $(PYTHON_FILES) --diff - [ "$(PYTHON_FILES)" = "" ] || poetry run ruff --select I $(PYTHON_FILES) + [ "$(PYTHON_FILES)" = "" ] || poetry run ruff check --select I $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) || poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) format format_diff: poetry run ruff format $(PYTHON_FILES) - poetry run ruff --select I --fix $(PYTHON_FILES) + poetry run ruff check --select I --fix $(PYTHON_FILES) diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index 84b6e1876..f252c829f 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -967,8 +967,7 @@ class RunsClient: on_disconnect: Optional[DisconnectMode] = None, webhook: Optional[str] = None, multitask_strategy: Optional[MultitaskStrategy] = None, - ) -> AsyncIterator[StreamPart]: - ... + ) -> AsyncIterator[StreamPart]: ... @overload def stream( @@ -986,8 +985,7 @@ class RunsClient: on_disconnect: Optional[DisconnectMode] = None, webhook: Optional[str] = None, on_completion: Optional[OnCompletionBehavior] = None, - ) -> AsyncIterator[StreamPart]: - ... + ) -> AsyncIterator[StreamPart]: ... def stream( self, @@ -1096,8 +1094,7 @@ class RunsClient: interrupt_after: Optional[list[str]] = None, webhook: Optional[str] = None, on_completion: Optional[OnCompletionBehavior] = None, - ) -> Run: - ... + ) -> Run: ... @overload async def create( @@ -1113,8 +1110,7 @@ class RunsClient: interrupt_after: Optional[list[str]] = None, webhook: Optional[str] = None, multitask_strategy: Optional[MultitaskStrategy] = None, - ) -> Run: - ... + ) -> Run: ... async def create( self, @@ -1258,8 +1254,7 @@ class RunsClient: webhook: Optional[str] = None, on_disconnect: Optional[DisconnectMode] = None, multitask_strategy: Optional[MultitaskStrategy] = None, - ) -> Union[list[dict], dict[str, Any]]: - ... + ) -> Union[list[dict], dict[str, Any]]: ... @overload async def wait( @@ -1275,8 +1270,7 @@ class RunsClient: webhook: Optional[str] = None, on_disconnect: Optional[DisconnectMode] = None, on_completion: Optional[OnCompletionBehavior] = None, - ) -> Union[list[dict], dict[str, Any]]: - ... + ) -> Union[list[dict], dict[str, Any]]: ... async def wait( self, diff --git a/libs/sdk-py/poetry.lock b/libs/sdk-py/poetry.lock index 5a17cca54..5bff56b98 100644 --- a/libs/sdk-py/poetry.lock +++ b/libs/sdk-py/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "anyio" @@ -388,28 +388,29 @@ watchdog = ">=0.6.0" [[package]] name = "ruff" -version = "0.1.15" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -489,4 +490,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "dcd64c96c58d777998a9a0fee6e8f78153870796e294989a9b19d0e7ac10b2b7" +content-hash = "832acea0ad21ce71ae74edef225a1ad6f8fb166f6bf1531d876fe80fac7495f0" diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml index 2e528e4bc..bcba0feec 100644 --- a/libs/sdk-py/pyproject.toml +++ b/libs/sdk-py/pyproject.toml @@ -15,7 +15,7 @@ httpx-sse = ">=0.4.0" orjson = ">=3.10.1" [tool.poetry.group.dev.dependencies] -ruff = "^0.1.4" +ruff = "^0.6.2" codespell = "^2.2.0" pytest = "^7.2.1" pytest-asyncio = "^0.21.1" From d7eff6ab85042c1373d8e59c9db60e3674d15578 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 27 Aug 2024 10:52:26 -0700 Subject: [PATCH 148/287] Remove abc --- libs/checkpoint/langgraph/checkpoint/base/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/base/__init__.py b/libs/checkpoint/langgraph/checkpoint/base/__init__.py index a4f76f87b..1afe69ed4 100644 --- a/libs/checkpoint/langgraph/checkpoint/base/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/base/__init__.py @@ -1,4 +1,3 @@ -from abc import ABC from datetime import datetime, timezone from typing import ( Any, @@ -192,7 +191,7 @@ CheckpointId = ConfigurableFieldSpec( ) -class BaseCheckpointSaver(ABC): +class BaseCheckpointSaver: """Base class for creating a graph checkpointer. Checkpointers allow LangGraph agents to persist their state From 7fcc0da4e3a5428c8c8edc5c02c3033b515b13e1 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 27 Aug 2024 11:00:25 -0700 Subject: [PATCH 149/287] Reformat --- libs/checkpoint/langgraph/checkpoint/serde/base.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/base.py b/libs/checkpoint/langgraph/checkpoint/serde/base.py index 5e4fba5be..58be7e08c 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/base.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/base.py @@ -12,17 +12,13 @@ class SerializerProtocol(Protocol): Valid implementations include the `pickle`, `json` and `orjson` modules. """ - def dumps(self, obj: Any) -> bytes: - ... + def dumps(self, obj: Any) -> bytes: ... - def dumps_typed(self, obj: Any) -> tuple[str, bytes]: - ... + def dumps_typed(self, obj: Any) -> tuple[str, bytes]: ... - def loads(self, data: bytes) -> Any: - ... + def loads(self, data: bytes) -> Any: ... - def loads_typed(self, data: tuple[str, bytes]) -> Any: - ... + def loads_typed(self, data: tuple[str, bytes]) -> Any: ... class SerializerCompat(SerializerProtocol): From 9b6100bc4fbb6b03e6862e8a9926b0a3bcd75681 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 27 Aug 2024 11:10:18 -0700 Subject: [PATCH 150/287] Avoid storing Sends twice in memory and postgres checkpointers - Sends are stored through put_writes, so we don't need to also store them inside checkpoint object - On reading checkpoint, reconstruct pending_sends from the stored writes --- .../langgraph/checkpoint/postgres/__init__.py | 18 +++--- .../langgraph/checkpoint/postgres/aio.py | 26 ++++----- .../langgraph/checkpoint/postgres/base.py | 43 ++++++++------ .../langgraph/checkpoint/memory/__init__.py | 58 +++++++++++++++++-- .../langgraph/checkpoint/serde/types.py | 1 + 5 files changed, 102 insertions(+), 44 deletions(-) diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py index 3ce8e880c..1c064415a 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py @@ -154,10 +154,11 @@ class PostgresSaver(BasePostgresSaver): "checkpoint_id": value["checkpoint_id"], } }, - { - **self._load_checkpoint(value["checkpoint"]), - "channel_values": self._load_blobs(value["channel_values"]), - }, + self._load_checkpoint( + value["checkpoint"], + value["channel_values"], + value["pending_sends"], + ), self._load_metadata(value["metadata"]), { "configurable": { @@ -232,10 +233,11 @@ class PostgresSaver(BasePostgresSaver): "checkpoint_id": value["checkpoint_id"], } }, - { - **self._load_checkpoint(value["checkpoint"]), - "channel_values": self._load_blobs(value["channel_values"]), - }, + self._load_checkpoint( + value["checkpoint"], + value["channel_values"], + value["pending_sends"], + ), self._load_metadata(value["metadata"]), { "configurable": { diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py index 0db61a443..63a2038a3 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py @@ -64,7 +64,7 @@ class AsyncPostgresSaver(BasePostgresSaver): pipeline (bool): whether to use AsyncPipeline Returns: - PostgresSaver: A new PostgresSaver instance. + AsyncPostgresSaver: A new AsyncPostgresSaver instance. """ async with await AsyncConnection.connect( conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row @@ -137,12 +137,12 @@ class AsyncPostgresSaver(BasePostgresSaver): "checkpoint_id": value["checkpoint_id"], } }, - { - **self._load_checkpoint(value["checkpoint"]), - "channel_values": await asyncio.to_thread( - self._load_blobs, value["channel_values"] - ), - }, + await asyncio.to_thread( + self._load_checkpoint, + value["checkpoint"], + value["channel_values"], + value["pending_sends"], + ), self._load_metadata(value["metadata"]), { "configurable": { @@ -196,12 +196,12 @@ class AsyncPostgresSaver(BasePostgresSaver): "checkpoint_id": value["checkpoint_id"], } }, - { - **self._load_checkpoint(value["checkpoint"]), - "channel_values": await asyncio.to_thread( - self._load_blobs, value["channel_values"] - ), - }, + await asyncio.to_thread( + self._load_checkpoint, + value["checkpoint"], + value["channel_values"], + value["pending_sends"], + ), self._load_metadata(value["metadata"]), { "configurable": { diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py index 91b49a162..c0f723f9e 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py @@ -1,4 +1,3 @@ -from base64 import b64decode, b64encode from hashlib import md5 from typing import Any, List, Optional, Tuple @@ -13,7 +12,7 @@ from langgraph.checkpoint.base import ( get_checkpoint_id, ) from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer -from langgraph.checkpoint.serde.types import ChannelProtocol +from langgraph.checkpoint.serde.types import TASKS, ChannelProtocol MetadataInput = Optional[dict[str, Any]] @@ -58,7 +57,7 @@ MIGRATIONS = [ "ALTER TABLE checkpoint_blobs ALTER COLUMN blob DROP not null;", ] -SELECT_SQL = """ +SELECT_SQL = f""" select thread_id, checkpoint, @@ -82,7 +81,15 @@ select where cw.thread_id = checkpoints.thread_id and cw.checkpoint_ns = checkpoints.checkpoint_ns and cw.checkpoint_id = checkpoints.checkpoint_id - ) as pending_writes + ) as pending_writes, + ( + select array_agg(array[cw.type::bytea, cw.blob]) + from checkpoint_writes cw + where cw.thread_id = checkpoints.thread_id + and cw.checkpoint_ns = checkpoints.checkpoint_ns + and cw.checkpoint_id = checkpoints.parent_checkpoint_id + and cw.channel = '{TASKS}' + ) as pending_sends from checkpoints """ UPSERT_CHECKPOINT_BLOBS_SQL = """ @@ -116,25 +123,23 @@ class BasePostgresSaver(BaseCheckpointSaver): jsonplus_serde = JsonPlusSerializer() - def _load_checkpoint(self, checkpoint: dict[str, Any]) -> Checkpoint: - if len(checkpoint["pending_sends"]) == 2 and all( - isinstance(a, str) for a in checkpoint["pending_sends"] - ): - type, bs = checkpoint["pending_sends"] - return { - **checkpoint, - "pending_sends": self.serde.loads_typed((type, b64decode(bs))), - } - - return checkpoint - - def _dump_checkpoint(self, checkpoint: Checkpoint) -> dict[str, Any]: - type, bs = self.serde.dumps_typed(checkpoint["pending_sends"]) + def _load_checkpoint( + self, + checkpoint: dict[str, Any], + channel_values: list[tuple[bytes, bytes, bytes]], + pending_sends: list[tuple[bytes, bytes]], + ) -> Checkpoint: return { **checkpoint, - "pending_sends": (type, b64encode(bs).decode()), + "pending_sends": [ + self.serde.loads_typed((c.decode(), b)) for c, b in pending_sends or [] + ], + "channel_values": self._load_blobs(channel_values), } + def _dump_checkpoint(self, checkpoint: Checkpoint) -> dict[str, Any]: + return {**checkpoint, "pending_sends": []} + def _load_blobs( self, blob_values: list[tuple[bytes, bytes, bytes]] ) -> dict[str, Any]: diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 6918de87a..03e1f7efa 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -17,6 +17,7 @@ from langgraph.checkpoint.base import ( SerializerProtocol, get_checkpoint_id, ) +from langgraph.checkpoint.serde.types import TASKS class MemorySaver( @@ -108,9 +109,22 @@ class MemorySaver( if saved := self.storage[thread_id][checkpoint_ns].get(checkpoint_id): checkpoint, metadata, parent_checkpoint_id = saved writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values() + if parent_checkpoint_id: + sends = [ + w[2] + for w in self.writes[ + (thread_id, checkpoint_ns, parent_checkpoint_id) + ].values() + if w[1] == TASKS + ] + else: + sends = [] return CheckpointTuple( config=config, - checkpoint=self.serde.loads_typed(checkpoint), + checkpoint={ + **self.serde.loads_typed(checkpoint), + "pending_sends": [self.serde.loads_typed(s) for s in sends], + }, metadata=self.serde.loads_typed(metadata), pending_writes=[ (id, c, self.serde.loads_typed(v)) for id, c, v in writes @@ -130,6 +144,23 @@ class MemorySaver( checkpoint_id = max(checkpoints.keys()) checkpoint, metadata, parent_checkpoint_id = checkpoints[checkpoint_id] writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values() + if parent_checkpoint_id: + print( + [ + *self.writes[ + (thread_id, checkpoint_ns, parent_checkpoint_id) + ].values() + ] + ) + sends = [ + w[2] + for w in self.writes[ + (thread_id, checkpoint_ns, parent_checkpoint_id) + ].values() + if w[1] == TASKS + ] + else: + sends = [] return CheckpointTuple( config={ "configurable": { @@ -138,7 +169,10 @@ class MemorySaver( "checkpoint_id": checkpoint_id, } }, - checkpoint=self.serde.loads_typed(checkpoint), + checkpoint={ + **self.serde.loads_typed(checkpoint), + "pending_sends": [self.serde.loads_typed(s) for s in sends], + }, metadata=self.serde.loads_typed(metadata), pending_writes=[ (id, c, self.serde.loads_typed(v)) for id, c, v in writes @@ -225,6 +259,17 @@ class MemorySaver( (thread_id, checkpoint_ns, checkpoint_id) ].values() + if parent_checkpoint_id: + sends = [ + w[2] + for w in self.writes[ + (thread_id, checkpoint_ns, parent_checkpoint_id) + ].values() + if w[1] == TASKS + ] + else: + sends = [] + yield CheckpointTuple( config={ "configurable": { @@ -233,7 +278,10 @@ class MemorySaver( "checkpoint_id": checkpoint_id, } }, - checkpoint=self.serde.loads_typed(checkpoint), + checkpoint={ + **self.serde.loads_typed(checkpoint), + "pending_sends": [self.serde.loads_typed(s) for s in sends], + }, metadata=metadata, parent_config={ "configurable": { @@ -270,12 +318,14 @@ class MemorySaver( Returns: RunnableConfig: The updated config containing the saved checkpoint's timestamp. """ + c = checkpoint.copy() + c.pop("pending_sends") thread_id = config["configurable"]["thread_id"] checkpoint_ns = config["configurable"]["checkpoint_ns"] self.storage[thread_id][checkpoint_ns].update( { checkpoint["id"]: ( - self.serde.dumps_typed(checkpoint), + self.serde.dumps_typed(c), self.serde.dumps_typed(metadata), config["configurable"].get("checkpoint_id"), # parent ) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/types.py b/libs/checkpoint/langgraph/checkpoint/serde/types.py index 26f118bbc..fb0055b51 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/types.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/types.py @@ -13,6 +13,7 @@ from langchain_core.runnables import RunnableConfig from typing_extensions import Self ERROR = "__error__" +TASKS = "__pregel_tasks" Value = TypeVar("Value") Update = TypeVar("Update") From 8d23929288d19cde58669d1828799ab840504626 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 27 Aug 2024 11:18:53 -0700 Subject: [PATCH 151/287] Remove print --- libs/checkpoint/langgraph/checkpoint/memory/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 03e1f7efa..3d69165c0 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -145,13 +145,6 @@ class MemorySaver( checkpoint, metadata, parent_checkpoint_id = checkpoints[checkpoint_id] writes = self.writes[(thread_id, checkpoint_ns, checkpoint_id)].values() if parent_checkpoint_id: - print( - [ - *self.writes[ - (thread_id, checkpoint_ns, parent_checkpoint_id) - ].values() - ] - ) sends = [ w[2] for w in self.writes[ From a8758661bc4533fd4719860b0ed0687cdf402f04 Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:47:20 -0700 Subject: [PATCH 152/287] acesss tool calls directly (#1495) --- examples/storm/storm.ipynb | 772 +++++++++++++++++++++++++++-- examples/tutorials/sql-agent.ipynb | 73 +-- libs/cli/examples/graphs/storm.py | 2 +- 3 files changed, 773 insertions(+), 74 deletions(-) diff --git a/examples/storm/storm.ipynb b/examples/storm/storm.ipynb index 67e56dbf7..42ba05faa 100644 --- a/examples/storm/storm.ipynb +++ b/examples/storm/storm.ipynb @@ -48,7 +48,10 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture --no-stderr\n%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n# We use one or the other search engine below\n%pip install -U duckduckgo tavily-python" + "%%capture --no-stderr\n", + "%pip install -U langchain_community langchain_openai langgraph wikipedia scikit-learn langchain_fireworks\n", + "# We use one or the other search engine below\n", + "%pip install -U duckduckgo tavily-python" ] }, { @@ -57,7 +60,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Uncomment if you want to draw the pretty graph diagrams.\n# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n# ! brew install graphviz\n# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz" + "# Uncomment if you want to draw the pretty graph diagrams.\n", + "# If you are on MacOS, you will need to run brew install graphviz before installing and update some environment flags\n", + "# ! brew install graphviz\n", + "# !CFLAGS=\"-I $(brew --prefix graphviz)/include\" LDFLAGS=\"-L $(brew --prefix graphviz)/lib\" pip install -U pygraphviz" ] }, { @@ -66,7 +72,21 @@ "metadata": {}, "outputs": [], "source": [ - "import getpass\nimport os\n\n\ndef _set_env(var: str):\n if os.environ.get(var):\n return\n os.environ[var] = getpass.getpass(var + \":\")\n\n\n# Set for tracing\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n_set_env(\"LANGCHAIN_API_KEY\")\n_set_env(\"OPENAI_API_KEY\")" + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if os.environ.get(var):\n", + " return\n", + " os.environ[var] = getpass.getpass(var + \":\")\n", + "\n", + "\n", + "# Set for tracing\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"STORM\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")\n", + "_set_env(\"OPENAI_API_KEY\")" ] }, { @@ -84,7 +104,12 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_openai import ChatOpenAI\n\nfast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n# Uncomment for a Fireworks model\n# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\nlong_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")" + "from langchain_openai import ChatOpenAI\n", + "\n", + "fast_llm = ChatOpenAI(model=\"gpt-3.5-turbo\")\n", + "# Uncomment for a Fireworks model\n", + "# fast_llm = ChatFireworks(model=\"accounts/fireworks/models/firefunction-v1\", max_tokens=32_000)\n", + "long_context_llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")" ] }, { @@ -112,7 +137,64 @@ } ], "source": [ - "from typing import List, Optional\n\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\ndirect_gen_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n ),\n (\"user\", \"{topic}\"),\n ]\n)\n\n\nclass Subsection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n description: str = Field(..., title=\"Content of the subsection\")\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n\n\nclass Section(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n description: str = Field(..., title=\"Content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n for subsection in self.subsections or []\n )\n return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n\n\nclass Outline(BaseModel):\n page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n sections: List[Section] = Field(\n default_factory=list,\n title=\"Titles and descriptions for each section of the Wikipedia page.\",\n )\n\n @property\n def as_str(self) -> str:\n sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n return f\"# {self.page_title}\\n\\n{sections}\".strip()\n\n\ngenerate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n Outline\n)" + "from typing import List, Optional\n", + "\n", + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", + "\n", + "direct_gen_outline_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are a Wikipedia writer. Write an outline for a Wikipedia page about a user-provided topic. Be comprehensive and specific.\",\n", + " ),\n", + " (\"user\", \"{topic}\"),\n", + " ]\n", + ")\n", + "\n", + "\n", + "class Subsection(BaseModel):\n", + " subsection_title: str = Field(..., title=\"Title of the subsection\")\n", + " description: str = Field(..., title=\"Content of the subsection\")\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " return f\"### {self.subsection_title}\\n\\n{self.description}\".strip()\n", + "\n", + "\n", + "class Section(BaseModel):\n", + " section_title: str = Field(..., title=\"Title of the section\")\n", + " description: str = Field(..., title=\"Content of the section\")\n", + " subsections: Optional[List[Subsection]] = Field(\n", + " default=None,\n", + " title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " subsections = \"\\n\\n\".join(\n", + " f\"### {subsection.subsection_title}\\n\\n{subsection.description}\"\n", + " for subsection in self.subsections or []\n", + " )\n", + " return f\"## {self.section_title}\\n\\n{self.description}\\n\\n{subsections}\".strip()\n", + "\n", + "\n", + "class Outline(BaseModel):\n", + " page_title: str = Field(..., title=\"Title of the Wikipedia page\")\n", + " sections: List[Section] = Field(\n", + " default_factory=list,\n", + " title=\"Titles and descriptions for each section of the Wikipedia page.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " sections = \"\\n\\n\".join(section.as_str for section in self.sections)\n", + " return f\"# {self.page_title}\\n\\n{sections}\".strip()\n", + "\n", + "\n", + "generate_outline_direct = direct_gen_outline_prompt | fast_llm.with_structured_output(\n", + " Outline\n", + ")" ] }, { @@ -145,7 +227,11 @@ } ], "source": [ - "example_topic = \"Impact of million-plus token context window language models on RAG\"\n\ninitial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n\nprint(initial_outline.as_str)" + "example_topic = \"Impact of million-plus token context window language models on RAG\"\n", + "\n", + "initial_outline = generate_outline_direct.invoke({\"topic\": example_topic})\n", + "\n", + "print(initial_outline.as_str)" ] }, { @@ -165,7 +251,25 @@ "metadata": {}, "outputs": [], "source": [ - "gen_related_topics_prompt = ChatPromptTemplate.from_template(\n \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n\nPlease list the as many subjects and urls as you can.\n\nTopic of interest: {topic}\n\"\"\"\n)\n\n\nclass RelatedSubjects(BaseModel):\n topics: List[str] = Field(\n description=\"Comprehensive list of related subjects as background research.\",\n )\n\n\nexpand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n RelatedSubjects\n)" + "gen_related_topics_prompt = ChatPromptTemplate.from_template(\n", + " \"\"\"I'm writing a Wikipedia page for a topic mentioned below. Please identify and recommend some Wikipedia pages on closely related subjects. I'm looking for examples that provide insights into interesting aspects commonly associated with this topic, or examples that help me understand the typical content and structure included in Wikipedia pages for similar topics.\n", + "\n", + "Please list the as many subjects and urls as you can.\n", + "\n", + "Topic of interest: {topic}\n", + "\"\"\"\n", + ")\n", + "\n", + "\n", + "class RelatedSubjects(BaseModel):\n", + " topics: List[str] = Field(\n", + " description=\"Comprehensive list of related subjects as background research.\",\n", + " )\n", + "\n", + "\n", + "expand_chain = gen_related_topics_prompt | fast_llm.with_structured_output(\n", + " RelatedSubjects\n", + ")" ] }, { @@ -185,7 +289,8 @@ } ], "source": [ - "related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\nrelated_subjects" + "related_subjects = await expand_chain.ainvoke({\"topic\": example_topic})\n", + "related_subjects" ] }, { @@ -204,7 +309,49 @@ "metadata": {}, "outputs": [], "source": [ - "class Editor(BaseModel):\n affiliation: str = Field(\n description=\"Primary affiliation of the editor.\",\n )\n name: str = Field(\n description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n )\n role: str = Field(\n description=\"Role of the editor in the context of the topic.\",\n )\n description: str = Field(\n description=\"Description of the editor's focus, concerns, and motives.\",\n )\n\n @property\n def persona(self) -> str:\n return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n\n\nclass Perspectives(BaseModel):\n editors: List[Editor] = Field(\n description=\"Comprehensive list of editors with their roles and affiliations.\",\n # Add a pydantic validation/restriction to be at most M editors\n )\n\n\ngen_perspectives_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n\n Wiki page outlines of related topics for inspiration:\n {examples}\"\"\",\n ),\n (\"user\", \"Topic of interest: {topic}\"),\n ]\n)\n\ngen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Perspectives)" + "class Editor(BaseModel):\n", + " affiliation: str = Field(\n", + " description=\"Primary affiliation of the editor.\",\n", + " )\n", + " name: str = Field(\n", + " description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n", + " )\n", + " role: str = Field(\n", + " description=\"Role of the editor in the context of the topic.\",\n", + " )\n", + " description: str = Field(\n", + " description=\"Description of the editor's focus, concerns, and motives.\",\n", + " )\n", + "\n", + " @property\n", + " def persona(self) -> str:\n", + " return f\"Name: {self.name}\\nRole: {self.role}\\nAffiliation: {self.affiliation}\\nDescription: {self.description}\\n\"\n", + "\n", + "\n", + "class Perspectives(BaseModel):\n", + " editors: List[Editor] = Field(\n", + " description=\"Comprehensive list of editors with their roles and affiliations.\",\n", + " # Add a pydantic validation/restriction to be at most M editors\n", + " )\n", + "\n", + "\n", + "gen_perspectives_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You need to select a diverse (and distinct) group of Wikipedia editors who will work together to create a comprehensive article on the topic. Each of them represents a different perspective, role, or affiliation related to this topic.\\\n", + " You can use other Wikipedia pages of related topics for inspiration. For each editor, add a description of what they will focus on.\n", + "\n", + " Wiki page outlines of related topics for inspiration:\n", + " {examples}\"\"\",\n", + " ),\n", + " (\"user\", \"Topic of interest: {topic}\"),\n", + " ]\n", + ")\n", + "\n", + "gen_perspectives_chain = gen_perspectives_prompt | ChatOpenAI(\n", + " model=\"gpt-3.5-turbo\"\n", + ").with_structured_output(Perspectives)" ] }, { @@ -213,7 +360,37 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_community.retrievers import WikipediaRetriever\nfrom langchain_core.runnables import RunnableLambda\nfrom langchain_core.runnables import chain as as_runnable\n\nwikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n\n\ndef format_doc(doc, max_length=1000):\n related = \"- \".join(doc.metadata[\"categories\"])\n return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n :max_length\n ]\n\n\ndef format_docs(docs):\n return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n\n\n@as_runnable\nasync def survey_subjects(topic: str):\n related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n retrieved_docs = await wikipedia_retriever.abatch(\n related_subjects.topics, return_exceptions=True\n )\n all_docs = []\n for docs in retrieved_docs:\n if isinstance(docs, BaseException):\n continue\n all_docs.extend(docs)\n formatted = format_docs(all_docs)\n return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})" + "from langchain_community.retrievers import WikipediaRetriever\n", + "from langchain_core.runnables import RunnableLambda\n", + "from langchain_core.runnables import chain as as_runnable\n", + "\n", + "wikipedia_retriever = WikipediaRetriever(load_all_available_meta=True, top_k_results=1)\n", + "\n", + "\n", + "def format_doc(doc, max_length=1000):\n", + " related = \"- \".join(doc.metadata[\"categories\"])\n", + " return f\"### {doc.metadata['title']}\\n\\nSummary: {doc.page_content}\\n\\nRelated\\n{related}\"[\n", + " :max_length\n", + " ]\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(format_doc(doc) for doc in docs)\n", + "\n", + "\n", + "@as_runnable\n", + "async def survey_subjects(topic: str):\n", + " related_subjects = await expand_chain.ainvoke({\"topic\": topic})\n", + " retrieved_docs = await wikipedia_retriever.abatch(\n", + " related_subjects.topics, return_exceptions=True\n", + " )\n", + " all_docs = []\n", + " for docs in retrieved_docs:\n", + " if isinstance(docs, BaseException):\n", + " continue\n", + " all_docs.extend(docs)\n", + " formatted = format_docs(all_docs)\n", + " return await gen_perspectives_chain.ainvoke({\"examples\": formatted, \"topic\": topic})" ] }, { @@ -280,7 +457,40 @@ "metadata": {}, "outputs": [], "source": [ - "from typing import Annotated\n\nfrom langchain_core.messages import AnyMessage\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef add_messages(left, right):\n if not isinstance(left, list):\n left = [left]\n if not isinstance(right, list):\n right = [right]\n return left + right\n\n\ndef update_references(references, new_references):\n if not references:\n references = {}\n references.update(new_references)\n return references\n\n\ndef update_editor(editor, new_editor):\n # Can only set at the outset\n if not editor:\n return new_editor\n return editor\n\n\nclass InterviewState(TypedDict):\n messages: Annotated[List[AnyMessage], add_messages]\n references: Annotated[Optional[dict], update_references]\n editor: Annotated[Optional[Editor], update_editor]" + "from typing import Annotated\n", + "\n", + "from langchain_core.messages import AnyMessage\n", + "from typing_extensions import TypedDict\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "\n", + "def add_messages(left, right):\n", + " if not isinstance(left, list):\n", + " left = [left]\n", + " if not isinstance(right, list):\n", + " right = [right]\n", + " return left + right\n", + "\n", + "\n", + "def update_references(references, new_references):\n", + " if not references:\n", + " references = {}\n", + " references.update(new_references)\n", + " return references\n", + "\n", + "\n", + "def update_editor(editor, new_editor):\n", + " # Can only set at the outset\n", + " if not editor:\n", + " return new_editor\n", + " return editor\n", + "\n", + "\n", + "class InterviewState(TypedDict):\n", + " messages: Annotated[List[AnyMessage], add_messages]\n", + " references: Annotated[Optional[dict], update_references]\n", + " editor: Annotated[Optional[Editor], update_editor]" ] }, { @@ -298,7 +508,56 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\nfrom langchain_core.prompts import MessagesPlaceholder\n\ngen_qn_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\nBesides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\nNow, you are chatting with an expert to get information. Ask good questions to get more useful information.\n\nWhen you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\nPlease only ask one question at a time and don't ask what you have asked before.\\\nYour questions should be related to the topic you want to write.\nBe comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n\nStay true to your specific perspective:\n\n{persona}\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\n\ndef tag_with_name(ai_message: AIMessage, name: str):\n ai_message.name = name\n return ai_message\n\n\ndef swap_roles(state: InterviewState, name: str):\n converted = []\n for message in state[\"messages\"]:\n if isinstance(message, AIMessage) and message.name != name:\n message = HumanMessage(**message.dict(exclude={\"type\"}))\n converted.append(message)\n return {\"messages\": converted}\n\n\n@as_runnable\nasync def generate_question(state: InterviewState):\n editor = state[\"editor\"]\n gn_chain = (\n RunnableLambda(swap_roles).bind(name=editor.name)\n | gen_qn_prompt.partial(persona=editor.persona)\n | fast_llm\n | RunnableLambda(tag_with_name).bind(name=editor.name)\n )\n result = await gn_chain.ainvoke(state)\n return {\"messages\": [result]}" + "from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n", + "from langchain_core.prompts import MessagesPlaceholder\n", + "\n", + "gen_qn_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You are an experienced Wikipedia writer and want to edit a specific page. \\\n", + "Besides your identity as a Wikipedia writer, you have a specific focus when researching the topic. \\\n", + "Now, you are chatting with an expert to get information. Ask good questions to get more useful information.\n", + "\n", + "When you have no more questions to ask, say \"Thank you so much for your help!\" to end the conversation.\\\n", + "Please only ask one question at a time and don't ask what you have asked before.\\\n", + "Your questions should be related to the topic you want to write.\n", + "Be comprehensive and curious, gaining as much unique insight from the expert as possible.\\\n", + "\n", + "Stay true to your specific perspective:\n", + "\n", + "{persona}\"\"\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", + " ]\n", + ")\n", + "\n", + "\n", + "def tag_with_name(ai_message: AIMessage, name: str):\n", + " ai_message.name = name\n", + " return ai_message\n", + "\n", + "\n", + "def swap_roles(state: InterviewState, name: str):\n", + " converted = []\n", + " for message in state[\"messages\"]:\n", + " if isinstance(message, AIMessage) and message.name != name:\n", + " message = HumanMessage(**message.dict(exclude={\"type\"}))\n", + " converted.append(message)\n", + " return {\"messages\": converted}\n", + "\n", + "\n", + "@as_runnable\n", + "async def generate_question(state: InterviewState):\n", + " editor = state[\"editor\"]\n", + " gn_chain = (\n", + " RunnableLambda(swap_roles).bind(name=editor.name)\n", + " | gen_qn_prompt.partial(persona=editor.persona)\n", + " | fast_llm\n", + " | RunnableLambda(tag_with_name).bind(name=editor.name)\n", + " )\n", + " result = await gn_chain.ainvoke(state)\n", + " return {\"messages\": [result]}" ] }, { @@ -318,7 +577,17 @@ } ], "source": [ - "messages = [\n HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n]\nquestion = await generate_question.ainvoke(\n {\n \"editor\": perspectives.editors[0],\n \"messages\": messages,\n }\n)\n\nquestion[\"messages\"][0].content" + "messages = [\n", + " HumanMessage(f\"So you said you were writing an article on {example_topic}?\")\n", + "]\n", + "question = await generate_question.ainvoke(\n", + " {\n", + " \"editor\": perspectives.editors[0],\n", + " \"messages\": messages,\n", + " }\n", + ")\n", + "\n", + "question[\"messages\"][0].content" ] }, { @@ -336,7 +605,24 @@ "metadata": {}, "outputs": [], "source": [ - "class Queries(BaseModel):\n queries: List[str] = Field(\n description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n )\n\n\ngen_queries_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\ngen_queries_chain = gen_queries_prompt | ChatOpenAI(\n model=\"gpt-3.5-turbo\"\n).with_structured_output(Queries, include_raw=True)" + "class Queries(BaseModel):\n", + " queries: List[str] = Field(\n", + " description=\"Comprehensive list of search engine queries to answer the user's questions.\",\n", + " )\n", + "\n", + "\n", + "gen_queries_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are a helpful research assistant. Query the search engine to answer the user's questions.\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", + " ]\n", + ")\n", + "gen_queries_chain = gen_queries_prompt | ChatOpenAI(\n", + " model=\"gpt-3.5-turbo\"\n", + ").with_structured_output(Queries, include_raw=True)" ] }, { @@ -357,7 +643,10 @@ } ], "source": [ - "queries = await gen_queries_chain.ainvoke(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nqueries[\"parsed\"].queries" + "queries = await gen_queries_chain.ainvoke(\n", + " {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n", + ")\n", + "queries[\"parsed\"].queries" ] }, { @@ -366,7 +655,38 @@ "metadata": {}, "outputs": [], "source": [ - "class AnswerWithCitations(BaseModel):\n answer: str = Field(\n description=\"Comprehensive answer to the user's question with citations.\",\n )\n cited_urls: List[str] = Field(\n description=\"List of urls cited in the answer.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n )\n\n\ngen_answer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n\nMake your response as informative as possible and make sure every sentence is supported by the gathered information.\nEach response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n ),\n MessagesPlaceholder(variable_name=\"messages\", optional=True),\n ]\n)\n\ngen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n AnswerWithCitations, include_raw=True\n).with_config(run_name=\"GenerateAnswer\")" + "class AnswerWithCitations(BaseModel):\n", + " answer: str = Field(\n", + " description=\"Comprehensive answer to the user's question with citations.\",\n", + " )\n", + " cited_urls: List[str] = Field(\n", + " description=\"List of urls cited in the answer.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " return f\"{self.answer}\\n\\nCitations:\\n\\n\" + \"\\n\".join(\n", + " f\"[{i+1}]: {url}\" for i, url in enumerate(self.cited_urls)\n", + " )\n", + "\n", + "\n", + "gen_answer_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You are an expert who can use information effectively. You are chatting with a Wikipedia writer who wants\\\n", + " to write a Wikipedia page on the topic you know. You have gathered the related information and will now use the information to form a response.\n", + "\n", + "Make your response as informative as possible and make sure every sentence is supported by the gathered information.\n", + "Each response must be backed up by a citation from a reliable source, formatted as a footnote, reproducing the URLS after your response.\"\"\",\n", + " ),\n", + " MessagesPlaceholder(variable_name=\"messages\", optional=True),\n", + " ]\n", + ")\n", + "\n", + "gen_answer_chain = gen_answer_prompt | fast_llm.with_structured_output(\n", + " AnswerWithCitations, include_raw=True\n", + ").with_config(run_name=\"GenerateAnswer\")" ] }, { @@ -375,7 +695,29 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\nfrom langchain_core.tools import tool\n\n'''\n# Tavily is typically a better search engine, but your free queries are limited\nsearch_engine = TavilySearchResults(max_results=4)\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = tavily_search.invoke(query)\n return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n'''\n\n# DDG\nsearch_engine = DuckDuckGoSearchAPIWrapper()\n\n\n@tool\nasync def search_engine(query: str):\n \"\"\"Search engine to the internet.\"\"\"\n results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]" + "from langchain_community.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper\n", + "from langchain_core.tools import tool\n", + "\n", + "'''\n", + "# Tavily is typically a better search engine, but your free queries are limited\n", + "search_engine = TavilySearchResults(max_results=4)\n", + "\n", + "@tool\n", + "async def search_engine(query: str):\n", + " \"\"\"Search engine to the internet.\"\"\"\n", + " results = tavily_search.invoke(query)\n", + " return [{\"content\": r[\"content\"], \"url\": r[\"url\"]} for r in results]\n", + "'''\n", + "\n", + "# DDG\n", + "search_engine = DuckDuckGoSearchAPIWrapper()\n", + "\n", + "\n", + "@tool\n", + "async def search_engine(query: str):\n", + " \"\"\"Search engine to the internet.\"\"\"\n", + " results = DuckDuckGoSearchAPIWrapper()._ddgs_text(query)\n", + " return [{\"content\": r[\"body\"], \"url\": r[\"href\"]} for r in results]" ] }, { @@ -384,7 +726,43 @@ "metadata": {}, "outputs": [], "source": [ - "import json\n\nfrom langchain_core.runnables import RunnableConfig\n\n\nasync def gen_answer(\n state: InterviewState,\n config: Optional[RunnableConfig] = None,\n name: str = \"Subject_Matter_Expert\",\n max_str_len: int = 15000,\n):\n swapped_state = swap_roles(state, name) # Convert all other AI messages\n queries = await gen_queries_chain.ainvoke(swapped_state)\n query_results = await search_engine.abatch(\n queries[\"parsed\"].queries, config, return_exceptions=True\n )\n successful_results = [\n res for res in query_results if not isinstance(res, Exception)\n ]\n all_query_results = {\n res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n }\n # We could be more precise about handling max token length if we wanted to here\n dumped = json.dumps(all_query_results)[:max_str_len]\n ai_message: AIMessage = queries[\"raw\"]\n tool_call = queries[\"raw\"].additional_kwargs[\"tool_calls\"][0]\n tool_id = tool_call[\"id\"]\n tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n swapped_state[\"messages\"].extend([ai_message, tool_message])\n # Only update the shared state with the final answer to avoid\n # polluting the dialogue history with intermediate messages\n generated = await gen_answer_chain.ainvoke(swapped_state)\n cited_urls = set(generated[\"parsed\"].cited_urls)\n # Save the retrieved information to a the shared state for future reference\n cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n return {\"messages\": [formatted_message], \"references\": cited_references}" + "import json\n", + "\n", + "from langchain_core.runnables import RunnableConfig\n", + "\n", + "\n", + "async def gen_answer(\n", + " state: InterviewState,\n", + " config: Optional[RunnableConfig] = None,\n", + " name: str = \"Subject_Matter_Expert\",\n", + " max_str_len: int = 15000,\n", + "):\n", + " swapped_state = swap_roles(state, name) # Convert all other AI messages\n", + " queries = await gen_queries_chain.ainvoke(swapped_state)\n", + " query_results = await search_engine.abatch(\n", + " queries[\"parsed\"].queries, config, return_exceptions=True\n", + " )\n", + " successful_results = [\n", + " res for res in query_results if not isinstance(res, Exception)\n", + " ]\n", + " all_query_results = {\n", + " res[\"url\"]: res[\"content\"] for results in successful_results for res in results\n", + " }\n", + " # We could be more precise about handling max token length if we wanted to here\n", + " dumped = json.dumps(all_query_results)[:max_str_len]\n", + " ai_message: AIMessage = queries[\"raw\"]\n", + " tool_call = queries[\"raw\"].tool_calls[0]\n", + " tool_id = tool_call[\"id\"]\n", + " tool_message = ToolMessage(tool_call_id=tool_id, content=dumped)\n", + " swapped_state[\"messages\"].extend([ai_message, tool_message])\n", + " # Only update the shared state with the final answer to avoid\n", + " # polluting the dialogue history with intermediate messages\n", + " generated = await gen_answer_chain.ainvoke(swapped_state)\n", + " cited_urls = set(generated[\"parsed\"].cited_urls)\n", + " # Save the retrieved information to a the shared state for future reference\n", + " cited_references = {k: v for k, v in all_query_results.items() if k in cited_urls}\n", + " formatted_message = AIMessage(name=name, content=generated[\"parsed\"].as_str)\n", + " return {\"messages\": [formatted_message], \"references\": cited_references}" ] }, { @@ -404,7 +782,10 @@ } ], "source": [ - "example_answer = await gen_answer(\n {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n)\nexample_answer[\"messages\"][-1].content" + "example_answer = await gen_answer(\n", + " {\"messages\": [HumanMessage(content=question[\"messages\"][0].content)]}\n", + ")\n", + "example_answer[\"messages\"][-1].content" ] }, { @@ -423,7 +804,31 @@ "metadata": {}, "outputs": [], "source": [ - "max_num_turns = 5\n\n\ndef route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n messages = state[\"messages\"]\n num_responses = len(\n [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n )\n if num_responses >= max_num_turns:\n return END\n last_question = messages[-2]\n if last_question.content.endswith(\"Thank you so much for your help!\"):\n return END\n return \"ask_question\"\n\n\nbuilder = StateGraph(InterviewState)\n\nbuilder.add_node(\"ask_question\", generate_question)\nbuilder.add_node(\"answer_question\", gen_answer)\nbuilder.add_conditional_edges(\"answer_question\", route_messages)\nbuilder.add_edge(\"ask_question\", \"answer_question\")\n\nbuilder.add_edge(START, \"ask_question\")\ninterview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")" + "max_num_turns = 5\n", + "\n", + "\n", + "def route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n", + " messages = state[\"messages\"]\n", + " num_responses = len(\n", + " [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n", + " )\n", + " if num_responses >= max_num_turns:\n", + " return END\n", + " last_question = messages[-2]\n", + " if last_question.content.endswith(\"Thank you so much for your help!\"):\n", + " return END\n", + " return \"ask_question\"\n", + "\n", + "\n", + "builder = StateGraph(InterviewState)\n", + "\n", + "builder.add_node(\"ask_question\", generate_question)\n", + "builder.add_node(\"answer_question\", gen_answer)\n", + "builder.add_conditional_edges(\"answer_question\", route_messages)\n", + "builder.add_edge(\"ask_question\", \"answer_question\")\n", + "\n", + "builder.add_edge(START, \"ask_question\")\n", + "interview_graph = builder.compile().with_config(run_name=\"Conduct Interviews\")" ] }, { @@ -444,7 +849,11 @@ } ], "source": [ - "from IPython.display import Image\n\n# Feel free to comment out if you have\n# not installed pygraphviz\nImage(interview_graph.get_graph().draw_png())" + "from IPython.display import Image\n", + "\n", + "# Feel free to comment out if you have\n", + "# not installed pygraphviz\n", + "Image(interview_graph.get_graph().draw_png())" ] }, { @@ -474,7 +883,23 @@ } ], "source": [ - "final_step = None\n\ninitial_state = {\n \"editor\": perspectives.editors[0],\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {example_topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n}\nasync for step in interview_graph.astream(initial_state):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name][\"messages\"])[:300])\n if END in step:\n final_step = step" + "final_step = None\n", + "\n", + "initial_state = {\n", + " \"editor\": perspectives.editors[0],\n", + " \"messages\": [\n", + " AIMessage(\n", + " content=f\"So you said you were writing an article on {example_topic}?\",\n", + " name=\"Subject_Matter_Expert\",\n", + " )\n", + " ],\n", + "}\n", + "async for step in interview_graph.astream(initial_state):\n", + " name = next(iter(step))\n", + " print(name)\n", + " print(\"-- \", str(step[name][\"messages\"])[:300])\n", + " if END in step:\n", + " final_step = step" ] }, { @@ -501,7 +926,29 @@ "metadata": {}, "outputs": [], "source": [ - "refine_outline_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\nYou need to make sure that the outline is comprehensive and specific. \\\nTopic you are writing about: {topic} \n\nOld outline:\n\n{old_outline}\"\"\",\n ),\n (\n \"user\",\n \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n ),\n ]\n)\n\n# Using turbo preview since the context can get quite long\nrefine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n Outline\n)" + "refine_outline_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"\"\"You are a Wikipedia writer. You have gathered information from experts and search engines. Now, you are refining the outline of the Wikipedia page. \\\n", + "You need to make sure that the outline is comprehensive and specific. \\\n", + "Topic you are writing about: {topic} \n", + "\n", + "Old outline:\n", + "\n", + "{old_outline}\"\"\",\n", + " ),\n", + " (\n", + " \"user\",\n", + " \"Refine the outline based on your conversations with subject-matter experts:\\n\\nConversations:\\n\\n{conversations}\\n\\nWrite the refined Wikipedia outline:\",\n", + " ),\n", + " ]\n", + ")\n", + "\n", + "# Using turbo preview since the context can get quite long\n", + "refine_outline_chain = refine_outline_prompt | long_context_llm.with_structured_output(\n", + " Outline\n", + ")" ] }, { @@ -510,7 +957,15 @@ "metadata": {}, "outputs": [], "source": [ - "refined_outline = refine_outline_chain.invoke(\n {\n \"topic\": example_topic,\n \"old_outline\": initial_outline.as_str,\n \"conversations\": \"\\n\\n\".join(\n f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n ),\n }\n)" + "refined_outline = refine_outline_chain.invoke(\n", + " {\n", + " \"topic\": example_topic,\n", + " \"old_outline\": initial_outline.as_str,\n", + " \"conversations\": \"\\n\\n\".join(\n", + " f\"### {m.name}\\n\\n{m.content}\" for m in final_state[\"messages\"]\n", + " ),\n", + " }\n", + ")" ] }, { @@ -595,7 +1050,23 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_core.documents import Document\nfrom langchain_openai import OpenAIEmbeddings\n\nembeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\nreference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in final_state[\"references\"].items()\n]\n# This really doesn't need to be a vectorstore for this size of data.\n# It could just be a numpy matrix. Or you could store documents\n# across requests if you want.\nvectorstore = SKLearnVectorStore.from_documents(\n reference_docs,\n embedding=embeddings,\n)\nretriever = vectorstore.as_retriever(k=10)" + "from langchain_community.vectorstores import SKLearnVectorStore\n", + "from langchain_core.documents import Document\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")\n", + "reference_docs = [\n", + " Document(page_content=v, metadata={\"source\": k})\n", + " for k, v in final_state[\"references\"].items()\n", + "]\n", + "# This really doesn't need to be a vectorstore for this size of data.\n", + "# It could just be a numpy matrix. Or you could store documents\n", + "# across requests if you want.\n", + "vectorstore = SKLearnVectorStore.from_documents(\n", + " reference_docs,\n", + " embedding=embeddings,\n", + ")\n", + "retriever = vectorstore.as_retriever(k=10)" ] }, { @@ -636,7 +1107,67 @@ "metadata": {}, "outputs": [], "source": [ - "class SubSection(BaseModel):\n subsection_title: str = Field(..., title=\"Title of the subsection\")\n content: str = Field(\n ...,\n title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n )\n\n @property\n def as_str(self) -> str:\n return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n\n\nclass WikiSection(BaseModel):\n section_title: str = Field(..., title=\"Title of the section\")\n content: str = Field(..., title=\"Full content of the section\")\n subsections: Optional[List[Subsection]] = Field(\n default=None,\n title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n )\n citations: List[str] = Field(default_factory=list)\n\n @property\n def as_str(self) -> str:\n subsections = \"\\n\\n\".join(\n subsection.as_str for subsection in self.subsections or []\n )\n citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n return (\n f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n + f\"\\n\\n{citations}\".strip()\n )\n\n\nsection_writer_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n \"{outline}\\n\\nCite your sources, using the following references:\\n\\n\\n{docs}\\n\",\n ),\n (\"user\", \"Write the full WikiSection for the {section} section.\"),\n ]\n)\n\n\nasync def retrieve(inputs: dict):\n docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n formatted = \"\\n\".join(\n [\n f'\\n{doc.page_content}\\n'\n for doc in docs\n ]\n )\n return {\"docs\": formatted, **inputs}\n\n\nsection_writer = (\n retrieve\n | section_writer_prompt\n | long_context_llm.with_structured_output(WikiSection)\n)" + "class SubSection(BaseModel):\n", + " subsection_title: str = Field(..., title=\"Title of the subsection\")\n", + " content: str = Field(\n", + " ...,\n", + " title=\"Full content of the subsection. Include [#] citations to the cited sources where relevant.\",\n", + " )\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " return f\"### {self.subsection_title}\\n\\n{self.content}\".strip()\n", + "\n", + "\n", + "class WikiSection(BaseModel):\n", + " section_title: str = Field(..., title=\"Title of the section\")\n", + " content: str = Field(..., title=\"Full content of the section\")\n", + " subsections: Optional[List[Subsection]] = Field(\n", + " default=None,\n", + " title=\"Titles and descriptions for each subsection of the Wikipedia page.\",\n", + " )\n", + " citations: List[str] = Field(default_factory=list)\n", + "\n", + " @property\n", + " def as_str(self) -> str:\n", + " subsections = \"\\n\\n\".join(\n", + " subsection.as_str for subsection in self.subsections or []\n", + " )\n", + " citations = \"\\n\".join([f\" [{i}] {cit}\" for i, cit in enumerate(self.citations)])\n", + " return (\n", + " f\"## {self.section_title}\\n\\n{self.content}\\n\\n{subsections}\".strip()\n", + " + f\"\\n\\n{citations}\".strip()\n", + " )\n", + "\n", + "\n", + "section_writer_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are an expert Wikipedia writer. Complete your assigned WikiSection from the following outline:\\n\\n\"\n", + " \"{outline}\\n\\nCite your sources, using the following references:\\n\\n\\n{docs}\\n\",\n", + " ),\n", + " (\"user\", \"Write the full WikiSection for the {section} section.\"),\n", + " ]\n", + ")\n", + "\n", + "\n", + "async def retrieve(inputs: dict):\n", + " docs = await retriever.ainvoke(inputs[\"topic\"] + \": \" + inputs[\"section\"])\n", + " formatted = \"\\n\".join(\n", + " [\n", + " f'\\n{doc.page_content}\\n'\n", + " for doc in docs\n", + " ]\n", + " )\n", + " return {\"docs\": formatted, **inputs}\n", + "\n", + "\n", + "section_writer = (\n", + " retrieve\n", + " | section_writer_prompt\n", + " | long_context_llm.with_structured_output(WikiSection)\n", + ")" ] }, { @@ -663,7 +1194,14 @@ } ], "source": [ - "section = await section_writer.ainvoke(\n {\n \"outline\": refined_outline.as_str,\n \"section\": refined_outline.sections[1].section_title,\n \"topic\": example_topic,\n }\n)\nprint(section.as_str)" + "section = await section_writer.ainvoke(\n", + " {\n", + " \"outline\": refined_outline.as_str,\n", + " \"section\": refined_outline.sections[1].section_title,\n", + " \"topic\": example_topic,\n", + " }\n", + ")\n", + "print(section.as_str)" ] }, { @@ -681,7 +1219,24 @@ "metadata": {}, "outputs": [], "source": [ - "from langchain_core.output_parsers import StrOutputParser\n\nwriter_prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n ),\n (\n \"user\",\n 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n \" avoiding duplicates in the footer. Include URLs in the footer.\",\n ),\n ]\n)\n\nwriter = writer_prompt | long_context_llm | StrOutputParser()" + "from langchain_core.output_parsers import StrOutputParser\n", + "\n", + "writer_prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\n", + " \"system\",\n", + " \"You are an expert Wikipedia author. Write the complete wiki article on {topic} using the following section drafts:\\n\\n\"\n", + " \"{draft}\\n\\nStrictly follow Wikipedia format guidelines.\",\n", + " ),\n", + " (\n", + " \"user\",\n", + " 'Write the complete Wiki article using markdown format. Organize citations using footnotes like \"[1]\",'\n", + " \" avoiding duplicates in the footer. Include URLs in the footer.\",\n", + " ),\n", + " ]\n", + ")\n", + "\n", + "writer = writer_prompt | long_context_llm | StrOutputParser()" ] }, { @@ -774,7 +1329,8 @@ } ], "source": [ - "for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n print(tok, end=\"\")" + "for tok in writer.stream({\"topic\": example_topic, \"draft\": section.as_str}):\n", + " print(tok, end=\"\")" ] }, { @@ -801,7 +1357,14 @@ "metadata": {}, "outputs": [], "source": [ - "class ResearchState(TypedDict):\n topic: str\n outline: Outline\n editors: List[Editor]\n interview_results: List[InterviewState]\n # The final sections output\n sections: List[WikiSection]\n article: str" + "class ResearchState(TypedDict):\n", + " topic: str\n", + " outline: Outline\n", + " editors: List[Editor]\n", + " interview_results: List[InterviewState]\n", + " # The final sections output\n", + " sections: List[WikiSection]\n", + " article: str" ] }, { @@ -810,7 +1373,109 @@ "metadata": {}, "outputs": [], "source": [ - "import asyncio\n\n\nasync def initialize_research(state: ResearchState):\n topic = state[\"topic\"]\n coros = (\n generate_outline_direct.ainvoke({\"topic\": topic}),\n survey_subjects.ainvoke(topic),\n )\n results = await asyncio.gather(*coros)\n return {\n **state,\n \"outline\": results[0],\n \"editors\": results[1].editors,\n }\n\n\nasync def conduct_interviews(state: ResearchState):\n topic = state[\"topic\"]\n initial_states = [\n {\n \"editor\": editor,\n \"messages\": [\n AIMessage(\n content=f\"So you said you were writing an article on {topic}?\",\n name=\"Subject_Matter_Expert\",\n )\n ],\n }\n for editor in state[\"editors\"]\n ]\n # We call in to the sub-graph here to parallelize the interviews\n interview_results = await interview_graph.abatch(initial_states)\n\n return {\n **state,\n \"interview_results\": interview_results,\n }\n\n\ndef format_conversation(interview_state):\n messages = interview_state[\"messages\"]\n convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n\n\nasync def refine_outline(state: ResearchState):\n convos = \"\\n\\n\".join(\n [\n format_conversation(interview_state)\n for interview_state in state[\"interview_results\"]\n ]\n )\n\n updated_outline = await refine_outline_chain.ainvoke(\n {\n \"topic\": state[\"topic\"],\n \"old_outline\": state[\"outline\"].as_str,\n \"conversations\": convos,\n }\n )\n return {**state, \"outline\": updated_outline}\n\n\nasync def index_references(state: ResearchState):\n all_docs = []\n for interview_state in state[\"interview_results\"]:\n reference_docs = [\n Document(page_content=v, metadata={\"source\": k})\n for k, v in interview_state[\"references\"].items()\n ]\n all_docs.extend(reference_docs)\n await vectorstore.aadd_documents(all_docs)\n return state\n\n\nasync def write_sections(state: ResearchState):\n outline = state[\"outline\"]\n sections = await section_writer.abatch(\n [\n {\n \"outline\": refined_outline.as_str,\n \"section\": section.section_title,\n \"topic\": state[\"topic\"],\n }\n for section in outline.sections\n ]\n )\n return {\n **state,\n \"sections\": sections,\n }\n\n\nasync def write_article(state: ResearchState):\n topic = state[\"topic\"]\n sections = state[\"sections\"]\n draft = \"\\n\\n\".join([section.as_str for section in sections])\n article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n return {\n **state,\n \"article\": article,\n }" + "import asyncio\n", + "\n", + "\n", + "async def initialize_research(state: ResearchState):\n", + " topic = state[\"topic\"]\n", + " coros = (\n", + " generate_outline_direct.ainvoke({\"topic\": topic}),\n", + " survey_subjects.ainvoke(topic),\n", + " )\n", + " results = await asyncio.gather(*coros)\n", + " return {\n", + " **state,\n", + " \"outline\": results[0],\n", + " \"editors\": results[1].editors,\n", + " }\n", + "\n", + "\n", + "async def conduct_interviews(state: ResearchState):\n", + " topic = state[\"topic\"]\n", + " initial_states = [\n", + " {\n", + " \"editor\": editor,\n", + " \"messages\": [\n", + " AIMessage(\n", + " content=f\"So you said you were writing an article on {topic}?\",\n", + " name=\"Subject_Matter_Expert\",\n", + " )\n", + " ],\n", + " }\n", + " for editor in state[\"editors\"]\n", + " ]\n", + " # We call in to the sub-graph here to parallelize the interviews\n", + " interview_results = await interview_graph.abatch(initial_states)\n", + "\n", + " return {\n", + " **state,\n", + " \"interview_results\": interview_results,\n", + " }\n", + "\n", + "\n", + "def format_conversation(interview_state):\n", + " messages = interview_state[\"messages\"]\n", + " convo = \"\\n\".join(f\"{m.name}: {m.content}\" for m in messages)\n", + " return f'Conversation with {interview_state[\"editor\"].name}\\n\\n' + convo\n", + "\n", + "\n", + "async def refine_outline(state: ResearchState):\n", + " convos = \"\\n\\n\".join(\n", + " [\n", + " format_conversation(interview_state)\n", + " for interview_state in state[\"interview_results\"]\n", + " ]\n", + " )\n", + "\n", + " updated_outline = await refine_outline_chain.ainvoke(\n", + " {\n", + " \"topic\": state[\"topic\"],\n", + " \"old_outline\": state[\"outline\"].as_str,\n", + " \"conversations\": convos,\n", + " }\n", + " )\n", + " return {**state, \"outline\": updated_outline}\n", + "\n", + "\n", + "async def index_references(state: ResearchState):\n", + " all_docs = []\n", + " for interview_state in state[\"interview_results\"]:\n", + " reference_docs = [\n", + " Document(page_content=v, metadata={\"source\": k})\n", + " for k, v in interview_state[\"references\"].items()\n", + " ]\n", + " all_docs.extend(reference_docs)\n", + " await vectorstore.aadd_documents(all_docs)\n", + " return state\n", + "\n", + "\n", + "async def write_sections(state: ResearchState):\n", + " outline = state[\"outline\"]\n", + " sections = await section_writer.abatch(\n", + " [\n", + " {\n", + " \"outline\": refined_outline.as_str,\n", + " \"section\": section.section_title,\n", + " \"topic\": state[\"topic\"],\n", + " }\n", + " for section in outline.sections\n", + " ]\n", + " )\n", + " return {\n", + " **state,\n", + " \"sections\": sections,\n", + " }\n", + "\n", + "\n", + "async def write_article(state: ResearchState):\n", + " topic = state[\"topic\"]\n", + " sections = state[\"sections\"]\n", + " draft = \"\\n\\n\".join([section.as_str for section in sections])\n", + " article = await writer.ainvoke({\"topic\": topic, \"draft\": draft})\n", + " return {\n", + " **state,\n", + " \"article\": article,\n", + " }" ] }, { @@ -826,7 +1491,27 @@ "metadata": {}, "outputs": [], "source": [ - "from langgraph.checkpoint.memory import MemorySaver\n\nbuilder_of_storm = StateGraph(ResearchState)\n\nnodes = [\n (\"init_research\", initialize_research),\n (\"conduct_interviews\", conduct_interviews),\n (\"refine_outline\", refine_outline),\n (\"index_references\", index_references),\n (\"write_sections\", write_sections),\n (\"write_article\", write_article),\n]\nfor i in range(len(nodes)):\n name, node = nodes[i]\n builder_of_storm.add_node(name, node)\n if i > 0:\n builder_of_storm.add_edge(nodes[i - 1][0], name)\n\nbuilder_of_storm.add_edge(START, nodes[0][0])\nbuilder_of_storm.add_edge(nodes[-1][0], END)\nstorm = builder_of_storm.compile(checkpointer=MemorySaver())" + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "builder_of_storm = StateGraph(ResearchState)\n", + "\n", + "nodes = [\n", + " (\"init_research\", initialize_research),\n", + " (\"conduct_interviews\", conduct_interviews),\n", + " (\"refine_outline\", refine_outline),\n", + " (\"index_references\", index_references),\n", + " (\"write_sections\", write_sections),\n", + " (\"write_article\", write_article),\n", + "]\n", + "for i in range(len(nodes)):\n", + " name, node = nodes[i]\n", + " builder_of_storm.add_node(name, node)\n", + " if i > 0:\n", + " builder_of_storm.add_edge(nodes[i - 1][0], name)\n", + "\n", + "builder_of_storm.add_edge(START, nodes[0][0])\n", + "builder_of_storm.add_edge(nodes[-1][0], END)\n", + "storm = builder_of_storm.compile(checkpointer=MemorySaver())" ] }, { @@ -877,7 +1562,16 @@ } ], "source": [ - "config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\nasync for step in storm.astream(\n {\n \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n },\n config,\n):\n name = next(iter(step))\n print(name)\n print(\"-- \", str(step[name])[:300])" + "config = {\"configurable\": {\"thread_id\": \"my-thread\"}}\n", + "async for step in storm.astream(\n", + " {\n", + " \"topic\": \"Groq, NVIDIA, Llamma.cpp and the future of LLM Inference\",\n", + " },\n", + " config,\n", + "):\n", + " name = next(iter(step))\n", + " print(name)\n", + " print(\"-- \", str(step[name])[:300])" ] }, { @@ -886,7 +1580,8 @@ "metadata": {}, "outputs": [], "source": [ - "checkpoint = storm.get_state(config)\narticle = checkpoint.values[\"article\"]" + "checkpoint = storm.get_state(config)\n", + "article = checkpoint.values[\"article\"]" ] }, { @@ -967,7 +1662,10 @@ } ], "source": [ - "from IPython.display import Markdown\n\n# We will down-header the sections to create less confusion in this notebook\nMarkdown(article.replace(\"\\n#\", \"\\n##\"))" + "from IPython.display import Markdown\n", + "\n", + "# We will down-header the sections to create less confusion in this notebook\n", + "Markdown(article.replace(\"\\n#\", \"\\n##\"))" ] }, { @@ -975,9 +1673,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "" - ] + "source": [] } ], "metadata": { diff --git a/examples/tutorials/sql-agent.ipynb b/examples/tutorials/sql-agent.ipynb index fb1354ef4..3a7f077fb 100644 --- a/examples/tutorials/sql-agent.ipynb +++ b/examples/tutorials/sql-agent.ipynb @@ -97,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 1, "id": "64b0bf1b14c2e902", "metadata": { "ExecuteTime": { @@ -170,7 +170,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 2, "id": "1f1e1f4f86ed54", "metadata": { "ExecuteTime": { @@ -197,7 +197,7 @@ "\"[(1, 'AC/DC'), (2, 'Accept'), (3, 'Aerosmith'), (4, 'Alanis Morissette'), (5, 'Alice In Chains'), (6, 'Antônio Carlos Jobim'), (7, 'Apocalyptica'), (8, 'Audioslave'), (9, 'BackBeat'), (10, 'Billy Cobham')]\"" ] }, - "execution_count": 20, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -228,7 +228,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 3, "id": "deae8460e4cf72b1", "metadata": { "ExecuteTime": { @@ -295,7 +295,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 4, "id": "452d049a3d2a4406", "metadata": { "ExecuteTime": { @@ -360,7 +360,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 5, "id": "f7eb708ecb4c7cfc", "metadata": { "ExecuteTime": { @@ -416,7 +416,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 6, "id": "293017e8f05ac2b3", "metadata": { "ExecuteTime": { @@ -432,10 +432,10 @@ { "data": { "text/plain": [ - "AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_5zdRt3uWwY23FSYmKZT7crGF', 'function': {'arguments': '{\"query\":\"SELECT * FROM Artist LIMIT 10;\"}', 'name': 'db_query_tool'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 222, 'total_tokens': 242}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_319be4768e', 'finish_reason': 'stop', 'logprobs': None}, id='run-a062c91e-084e-4a91-bba8-fdbb957e2a5c-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': 'SELECT * FROM Artist LIMIT 10;'}, 'id': 'call_5zdRt3uWwY23FSYmKZT7crGF'}], usage_metadata={'input_tokens': 222, 'output_tokens': 20, 'total_tokens': 242})" + "AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_la8JTjHox6P1VjTqc15GSgdk', 'function': {'arguments': '{\"query\":\"SELECT * FROM Artist LIMIT 10;\"}', 'name': 'db_query_tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 20, 'prompt_tokens': 221, 'total_tokens': 241}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'stop', 'logprobs': None}, id='run-dd7873ef-d2f7-4769-a5c0-e6776ec2c515-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': 'SELECT * FROM Artist LIMIT 10;'}, 'id': 'call_la8JTjHox6P1VjTqc15GSgdk', 'type': 'tool_call'}], usage_metadata={'input_tokens': 221, 'output_tokens': 20, 'total_tokens': 241})" ] }, - "execution_count": 24, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -485,7 +485,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 7, "id": "90d04ceea7b6b010", "metadata": { "ExecuteTime": { @@ -675,7 +675,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 14, "id": "4f200d1813897000", "metadata": { "ExecuteTime": { @@ -690,7 +690,7 @@ "outputs": [ { "data": { - "image/jpeg": "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", + "image/jpeg": "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", "text/plain": [ "" ] @@ -727,7 +727,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 15, "id": "85958809-03c5-4e52-97cc-e7c0ae986f60", "metadata": {}, "outputs": [ @@ -737,29 +737,42 @@ "'The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.'" ] }, - "execution_count": 53, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import json\n", - "\n", "messages = app.invoke(\n", " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n", ")\n", - "json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n", - " \"arguments\"\n", - "]\n", - "json.loads(json_str)[\"final_answer\"]" + "json_str = messages[\"messages\"][-1].tool_calls[0][\"args\"][\"final_answer\"]\n", + "json_str" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "3bf7709f-500c-4f28-bb85-dda317286c63", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'first_tool_call': {'messages': [AIMessage(content='', tool_calls=[{'name': 'sql_db_list_tables', 'args': {}, 'id': 'tool_abcd123', 'type': 'tool_call'}])]}}\n", + "{'list_tables_tool': {'messages': [ToolMessage(content='Album, Artist, Customer, Employee, Genre, Invoice, InvoiceLine, MediaType, Playlist, PlaylistTrack, Track', name='sql_db_list_tables', tool_call_id='tool_abcd123')]}}\n", + "{'model_get_schema': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_z1tyC7cEAawi5oIQn731Uknp', 'function': {'arguments': '{\"table_names\":\"Employee, Invoice\"}', 'name': 'sql_db_schema'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 177, 'total_tokens': 195}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-c91a5aad-fc05-4881-87f9-0662d703c3c8-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': 'Employee, Invoice'}, 'id': 'call_z1tyC7cEAawi5oIQn731Uknp', 'type': 'tool_call'}], usage_metadata={'input_tokens': 177, 'output_tokens': 18, 'total_tokens': 195})]}}\n", + "{'get_schema_tool': {'messages': [ToolMessage(content='\\nCREATE TABLE \"Employee\" (\\n\\t\"EmployeeId\" INTEGER NOT NULL, \\n\\t\"LastName\" NVARCHAR(20) NOT NULL, \\n\\t\"FirstName\" NVARCHAR(20) NOT NULL, \\n\\t\"Title\" NVARCHAR(30), \\n\\t\"ReportsTo\" INTEGER, \\n\\t\"BirthDate\" DATETIME, \\n\\t\"HireDate\" DATETIME, \\n\\t\"Address\" NVARCHAR(70), \\n\\t\"City\" NVARCHAR(40), \\n\\t\"State\" NVARCHAR(40), \\n\\t\"Country\" NVARCHAR(40), \\n\\t\"PostalCode\" NVARCHAR(10), \\n\\t\"Phone\" NVARCHAR(24), \\n\\t\"Fax\" NVARCHAR(24), \\n\\t\"Email\" NVARCHAR(60), \\n\\tPRIMARY KEY (\"EmployeeId\"), \\n\\tFOREIGN KEY(\"ReportsTo\") REFERENCES \"Employee\" (\"EmployeeId\")\\n)\\n\\n/*\\n3 rows from Employee table:\\nEmployeeId\\tLastName\\tFirstName\\tTitle\\tReportsTo\\tBirthDate\\tHireDate\\tAddress\\tCity\\tState\\tCountry\\tPostalCode\\tPhone\\tFax\\tEmail\\n1\\tAdams\\tAndrew\\tGeneral Manager\\tNone\\t1962-02-18 00:00:00\\t2002-08-14 00:00:00\\t11120 Jasper Ave NW\\tEdmonton\\tAB\\tCanada\\tT5K 2N1\\t+1 (780) 428-9482\\t+1 (780) 428-3457\\tandrew@chinookcorp.com\\n2\\tEdwards\\tNancy\\tSales Manager\\t1\\t1958-12-08 00:00:00\\t2002-05-01 00:00:00\\t825 8 Ave SW\\tCalgary\\tAB\\tCanada\\tT2P 2T3\\t+1 (403) 262-3443\\t+1 (403) 262-3322\\tnancy@chinookcorp.com\\n3\\tPeacock\\tJane\\tSales Support Agent\\t2\\t1973-08-29 00:00:00\\t2002-04-01 00:00:00\\t1111 6 Ave SW\\tCalgary\\tAB\\tCanada\\tT2P 5M5\\t+1 (403) 262-3443\\t+1 (403) 262-6712\\tjane@chinookcorp.com\\n*/\\n\\n\\nCREATE TABLE \"Invoice\" (\\n\\t\"InvoiceId\" INTEGER NOT NULL, \\n\\t\"CustomerId\" INTEGER NOT NULL, \\n\\t\"InvoiceDate\" DATETIME NOT NULL, \\n\\t\"BillingAddress\" NVARCHAR(70), \\n\\t\"BillingCity\" NVARCHAR(40), \\n\\t\"BillingState\" NVARCHAR(40), \\n\\t\"BillingCountry\" NVARCHAR(40), \\n\\t\"BillingPostalCode\" NVARCHAR(10), \\n\\t\"Total\" NUMERIC(10, 2) NOT NULL, \\n\\tPRIMARY KEY (\"InvoiceId\"), \\n\\tFOREIGN KEY(\"CustomerId\") REFERENCES \"Customer\" (\"CustomerId\")\\n)\\n\\n/*\\n3 rows from Invoice table:\\nInvoiceId\\tCustomerId\\tInvoiceDate\\tBillingAddress\\tBillingCity\\tBillingState\\tBillingCountry\\tBillingPostalCode\\tTotal\\n1\\t2\\t2009-01-01 00:00:00\\tTheodor-Heuss-Straße 34\\tStuttgart\\tNone\\tGermany\\t70174\\t1.98\\n2\\t4\\t2009-01-02 00:00:00\\tUllevålsveien 14\\tOslo\\tNone\\tNorway\\t0171\\t3.96\\n3\\t8\\t2009-01-03 00:00:00\\tGrétrystraat 63\\tBrussels\\tNone\\tBelgium\\t1000\\t5.94\\n*/', name='sql_db_schema', tool_call_id='call_z1tyC7cEAawi5oIQn731Uknp')]}}\n", + "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_ErWLktUfxKsHGNGr74m72yYD', 'function': {'arguments': '{\"table_names\":\"Customer\"}', 'name': 'sql_db_schema'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 16, 'prompt_tokens': 1179, 'total_tokens': 1195}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-19e02169-5e1e-40d9-90a2-384336ca5069-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': 'Customer'}, 'id': 'call_ErWLktUfxKsHGNGr74m72yYD', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1179, 'output_tokens': 16, 'total_tokens': 1195}), ToolMessage(content='Error: The wrong tool was called: sql_db_schema. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.', id='de5d25f5-b891-4e47-8282-d04dc9b93e9e', tool_call_id='call_ErWLktUfxKsHGNGr74m72yYD')]}}\n", + "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_TFaA52SbhgEqm3ElEAd4HCsn', 'function': {'arguments': '{\"table_names\":[\"Customer\"]}', 'name': 'sql_db_schema'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 1245, 'total_tokens': 1262}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2c5f800f-43dc-4224-847b-49b5079efd2a-0', tool_calls=[{'name': 'sql_db_schema', 'args': {'table_names': ['Customer']}, 'id': 'call_TFaA52SbhgEqm3ElEAd4HCsn', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1245, 'output_tokens': 17, 'total_tokens': 1262}), ToolMessage(content='Error: The wrong tool was called: sql_db_schema. Please fix your mistakes. Remember to only call SubmitFinalAnswer to submit the final answer. Generated queries should be outputted WITHOUT a tool call.', id='6c962a35-fc24-4f27-86f0-6ec05256d478', tool_call_id='call_TFaA52SbhgEqm3ElEAd4HCsn')]}}\n", + "{'query_gen': {'messages': [AIMessage(content=\"To determine which sales agent made the most in sales in 2009, we need to join the `Invoice`, `Customer`, and `Employee` tables. Here is the query to find the top sales agent:\\n\\n```sql\\nSELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\nFROM Invoice i\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\nWHERE strftime('%Y', i.InvoiceDate) = '2009'\\nGROUP BY e.EmployeeId\\nORDER BY TotalSales DESC\\nLIMIT 1;\\n```\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 125, 'prompt_tokens': 1312, 'total_tokens': 1437}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_3aa7262c27', 'finish_reason': 'stop', 'logprobs': None}, id='run-6cacd10d-d3aa-49ae-b9d7-8cc209fc4ccc-0', usage_metadata={'input_tokens': 1312, 'output_tokens': 125, 'total_tokens': 1437})]}}\n", + "{'correct_query': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_FwCE2c7WORU7lKHdSWqMv0ON', 'function': {'arguments': '{\"query\":\"SELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\\\nFROM Invoice i\\\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\\\nWHERE strftime(\\'%Y\\', i.InvoiceDate) = \\'2009\\'\\\\nGROUP BY e.EmployeeId\\\\nORDER BY TotalSales DESC\\\\nLIMIT 1;\"}', 'name': 'db_query_tool'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 90, 'prompt_tokens': 337, 'total_tokens': 427}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5', 'finish_reason': 'stop', 'logprobs': None}, id='run-71067e75-80f6-4356-8239-518e466b3526-0', tool_calls=[{'name': 'db_query_tool', 'args': {'query': \"SELECT e.FirstName, e.LastName, SUM(i.Total) as TotalSales\\nFROM Invoice i\\nJOIN Customer c ON i.CustomerId = c.CustomerId\\nJOIN Employee e ON c.SupportRepId = e.EmployeeId\\nWHERE strftime('%Y', i.InvoiceDate) = '2009'\\nGROUP BY e.EmployeeId\\nORDER BY TotalSales DESC\\nLIMIT 1;\"}, 'id': 'call_FwCE2c7WORU7lKHdSWqMv0ON', 'type': 'tool_call'}], usage_metadata={'input_tokens': 337, 'output_tokens': 90, 'total_tokens': 427})]}}\n", + "{'execute_query': {'messages': [ToolMessage(content=\"[('Steve', 'Johnson', 164.34)]\", name='db_query_tool', tool_call_id='call_FwCE2c7WORU7lKHdSWqMv0ON')]}}\n", + "{'query_gen': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_fHJ4lvdiFM9HY6gupE6vLZV4', 'function': {'arguments': '{\"final_answer\":\"The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.\"}', 'name': 'SubmitFinalAnswer'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 41, 'prompt_tokens': 1553, 'total_tokens': 1594}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_cb7cc8e106', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-2ec7bf3a-2a16-47bd-aa9c-b7d6dc531c1b-0', tool_calls=[{'name': 'SubmitFinalAnswer', 'args': {'final_answer': 'The sales agent who made the most in sales in 2009 is Steve Johnson with total sales of 164.34.'}, 'id': 'call_fHJ4lvdiFM9HY6gupE6vLZV4', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1553, 'output_tokens': 41, 'total_tokens': 1594})]}}\n" + ] + } + ], "source": [ "for event in app.stream(\n", " {\"messages\": [(\"user\", \"Which sales agent made the most in sales in 2009?\")]}\n", @@ -806,10 +819,8 @@ " \"\"\"Use this for answer evaluation\"\"\"\n", " msg = {\"messages\": (\"user\", example[\"input\"])}\n", " messages = app.invoke(msg)\n", - " json_str = messages[\"messages\"][-1].additional_kwargs[\"tool_calls\"][0][\"function\"][\n", - " \"arguments\"\n", - " ]\n", - " response = json.loads(json_str)[\"final_answer\"]\n", + " json_str = messages[\"messages\"][-1].tool_calls[0][\"args\"]\n", + " response = json_str[\"final_answer\"]\n", " return {\"response\": response}" ] }, @@ -1053,14 +1064,6 @@ "\n", "We will explore ways to resolve this using LangGraph in future cookbooks!" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0681b6e0-196e-440c-ab16-1a530411719e", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -1079,7 +1082,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/libs/cli/examples/graphs/storm.py b/libs/cli/examples/graphs/storm.py index e5ef2346e..a1317bc51 100644 --- a/libs/cli/examples/graphs/storm.py +++ b/libs/cli/examples/graphs/storm.py @@ -339,7 +339,7 @@ async def gen_answer( # We could be more precise about handling max token length if we wanted to here dumped = json.dumps(all_query_results)[:max_str_len] ai_message: AIMessage = queries["raw"] - tool_call = queries["raw"].additional_kwargs["tool_calls"][0] + tool_call = queries["raw"].tool_calls[0] tool_id = tool_call["id"] tool_message = ToolMessage(tool_call_id=tool_id, content=dumped) swapped_state["messages"].extend([ai_message, tool_message]) From 499fe10ef2e0df2ec34c37d19584f02d64db1ff5 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 27 Aug 2024 13:34:50 -0700 Subject: [PATCH 153/287] Undo addition of Send.id --- .../checkpoint/langgraph/checkpoint/serde/jsonplus.py | 2 +- libs/checkpoint/langgraph/checkpoint/serde/types.py | 1 - libs/langgraph/langgraph/constants.py | 11 ++++------- libs/langgraph/langgraph/pregel/algo.py | 6 +++--- libs/langgraph/langgraph/pregel/loop.py | 1 - 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py index e80b9a678..e39a66e76 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py @@ -109,7 +109,7 @@ class JsonPlusSerializer(SerializerProtocol): return self._encode_constructor_args(obj.__class__, args=[obj.value]) elif isinstance(obj, SendProtocol): return self._encode_constructor_args( - obj.__class__, kwargs={"node": obj.node, "arg": obj.arg, "id": obj.id} + obj.__class__, kwargs={"node": obj.node, "arg": obj.arg} ) elif isinstance(obj, (bytes, bytearray)): return self._encode_constructor_args( diff --git a/libs/checkpoint/langgraph/checkpoint/serde/types.py b/libs/checkpoint/langgraph/checkpoint/serde/types.py index fb0055b51..67f59ae89 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/types.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/types.py @@ -50,7 +50,6 @@ class SendProtocol(Protocol): # Mirrors langgraph.constants.Send node: str arg: Any - id: str def __hash__(self) -> int: ... diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index 3b322d391..085316333 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -1,6 +1,5 @@ from dataclasses import dataclass -from typing import Any, Literal, Optional -from uuid import uuid4 +from typing import Any, Literal INPUT = "__input__" CONFIG_KEY_SEND = "__pregel_send" @@ -79,9 +78,8 @@ class Send: node: str arg: Any - id: Optional[str] - def __init__(self, /, node: str, arg: Any, id: Optional[str] = None) -> None: + def __init__(self, /, node: str, arg: Any) -> None: """ Initialize a new instance of the Send class. @@ -92,13 +90,12 @@ class Send: """ self.node = node self.arg = arg - self.id = id or str(uuid4()) def __hash__(self) -> int: - return hash((self.node, self.arg, self.id)) + return hash((self.node, self.arg)) def __repr__(self) -> str: - return f"Send(node={self.node!r}, arg={self.arg!r}, id={self.id!r})" + return f"Send(node={self.node!r}, arg={self.arg!r})" def __eq__(self, value: object) -> bool: return ( diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 39ca717b6..2f1589573 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -291,9 +291,9 @@ def prepare_next_tasks( "langgraph_task_idx": len(tasks), } checkpoint_ns = ( - f"{parent_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{packet.node}:{packet.id}" + f"{parent_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{packet.node}" if parent_ns - else f"{packet.node}:{packet.id}" + else packet.node ) task_id = str( uuid5(UUID(checkpoint["id"]), json.dumps((checkpoint_ns, metadata))) @@ -344,7 +344,7 @@ def prepare_next_tasks( CONFIG_KEY_CHECKPOINTER: checkpointer, CONFIG_KEY_RESUMING: is_resuming, "checkpoint_id": checkpoint["id"], - "checkpoint_ns": checkpoint_ns, + "checkpoint_ns": f"{checkpoint_ns}:{task_id}", }, ), triggers, diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index b4e6d4118..509f6eabc 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -118,7 +118,6 @@ class PregelLoop: checkpoint_config: RunnableConfig checkpoint_metadata: CheckpointMetadata checkpoint_pending_writes: List[PendingWrite] - # (thread_id, checkpoint_ns -> channel_versions) checkpoint_previous_versions: dict[str, Union[str, float, int]] step: int From e338a4ec989dc0dc43e69d0a3b49ce9c96040f8a Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 27 Aug 2024 16:09:11 -0700 Subject: [PATCH 154/287] Fix filtering on ns --- .../langgraph/checkpoint/postgres/base.py | 3 ++- libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py | 3 ++- libs/checkpoint/langgraph/checkpoint/memory/__init__.py | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py index c0f723f9e..582206dd5 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py @@ -260,7 +260,8 @@ class BasePostgresSaver(BaseCheckpointSaver): if config: wheres.append("thread_id = %s ") param_values.append(config["configurable"]["thread_id"]) - if checkpoint_ns := config["configurable"].get("checkpoint_ns"): + checkpoint_ns = config["configurable"].get("checkpoint_ns") + if checkpoint_ns is not None: wheres.append("checkpoint_ns = %s") param_values.append(checkpoint_ns) diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py index 0e1e06fcc..a8ceb496a 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/utils.py @@ -70,7 +70,8 @@ def search_where( if config is not None: wheres.append("thread_id = ?") param_values.append(config["configurable"]["thread_id"]) - if checkpoint_ns := config["configurable"].get("checkpoint_ns"): + checkpoint_ns = config["configurable"].get("checkpoint_ns") + if checkpoint_ns is not None: wheres.append("checkpoint_ns = ?") param_values.append(checkpoint_ns) diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 3d69165c0..372e5ba2f 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -210,7 +210,10 @@ class MemorySaver( config_checkpoint_id = get_checkpoint_id(config) if config else None for thread_id in thread_ids: for checkpoint_ns in self.storage[thread_id].keys(): - if config_checkpoint_ns and checkpoint_ns != config_checkpoint_ns: + if ( + config_checkpoint_ns is not None + and checkpoint_ns != config_checkpoint_ns + ): continue for checkpoint_id, ( From a8a2a89602927ce920bf245f47982c85291c5b27 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 27 Aug 2024 16:59:20 -0700 Subject: [PATCH 155/287] Fix docstring --- libs/langgraph/langgraph/constants.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index 085316333..a7c9896f6 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -50,7 +50,6 @@ class Send: Attributes: node (str): The name of the target node to send the message to. arg (Any): The state or message to send to the target node. - id (str): ID associated with the Send. Examples: >>> from typing import Annotated From f4dea9c5f74aa47dae4189a9060083afe960ed8a Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Wed, 28 Aug 2024 02:07:21 +0200 Subject: [PATCH 156/287] feat(cli): add building support for JS --- libs/cli/langgraph_cli/cli.py | 24 +++++-- libs/cli/langgraph_cli/config.py | 87 ++++++++++++++++++------ libs/cli/tests/unit_tests/test_config.py | 25 +++++++ 3 files changed, 109 insertions(+), 27 deletions(-) diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index 1e3ffa2a7..ebd7e0701 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -372,7 +372,11 @@ def _build( pull: bool, tag: str, ): - base_image = base_image or "langchain/langgraph-api" + base_image = base_image or ( + "langchain/langgraphjs-api" + if config_json.get("node_version") + else "langchain/langgraph-api" + ) # pull latest images if pull: @@ -380,7 +384,9 @@ def _build( subp_exec( "docker", "pull", - f"{base_image}:{config_json['python_version']}", + f"{base_image}:{config_json['node_version']}" + if config_json.get("node_version") + else f"{base_image}:{config_json['python_version']}", verbose=True, ) ) @@ -462,7 +468,11 @@ def dockerfile(save_path: pathlib.Path, config: pathlib.Path): with open(save_path, "w") as f: f.write( langgraph_cli.config.config_to_docker( - config, config_json, "langchain/langgraph-api" + config, + config_json, + "langchain/langgraphjs-api" + if config_json.get("node_version") + else "langchain/langgraph-api", ) ) @@ -500,7 +510,9 @@ def prepare_args_and_stdin( config_path, config, watch=watch, - base_image="langchain/langgraph-api", + base_image="langchain/langgraphjs-api" + if config.get("node_version") + else "langchain/langgraph-api", ) return args, stdin @@ -527,7 +539,9 @@ def prepare( subp_exec( "docker", "pull", - f"langchain/langgraph-api:{config['python_version']}", + f"langchain/langgraphjs-api:{config['node_version']}" + if config.get("node_version") + else f"langchain/langgraph-api:{config['python_version']}", verbose=verbose, ) ) diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 177125c84..f7b8355d9 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -9,6 +9,7 @@ import click class Config(TypedDict): python_version: str + node_version: Optional[str] pip_config_file: Optional[str] dockerfile_lines: list[str] dependencies: list[str] @@ -17,27 +18,46 @@ class Config(TypedDict): def validate_config(config: Config) -> Config: - config = { - "python_version": config.get("python_version", "3.11"), - "pip_config_file": config.get("pip_config_file"), - "dockerfile_lines": config.get("dockerfile_lines", []), - "dependencies": config.get("dependencies", []), - "graphs": config.get("graphs", {}), - "env": config.get("env", {}), - } - if config["python_version"] not in ( - "3.11", - "3.12", - ): - raise click.UsageError( - f"Unsupported Python version: {config['python_version']}. " - "Supported versions are 3.11 and 3.12." - ) - if not config["dependencies"]: - raise click.UsageError( - "No dependencies found in config. " - "Add at least one dependency to 'dependencies' list." - ) + config = ( + { + "node_version": config.get("node_version"), + "dockerfile_lines": config.get("dockerfile_lines", []), + "graphs": config.get("graphs", {}), + "env": config.get("env", {}), + } + if config.get("node_version") + else { + "python_version": config.get("python_version", "3.11"), + "pip_config_file": config.get("pip_config_file"), + "dockerfile_lines": config.get("dockerfile_lines", []), + "dependencies": config.get("dependencies", []), + "graphs": config.get("graphs", {}), + "env": config.get("env", {}), + } + ) + + if config.get("node_version"): + if config["node_version"] not in ("20"): + raise click.UsageError( + f"Unsupported Node.js version: {config['node_version']}. " + "Currently only `node_version: \"20\"` is supported." + ) + + if config.get("python_version"): + if config["python_version"] not in ( + "3.11", + "3.12", + ): + raise click.UsageError( + f"Unsupported Python version: {config['python_version']}. " + "Supported versions are 3.11 and 3.12." + ) + if not config["dependencies"]: + raise click.UsageError( + "No dependencies found in config. " + "Add at least one dependency to 'dependencies' list." + ) + if not config["graphs"]: raise click.UsageError( "No graphs found in config. " @@ -191,7 +211,7 @@ def _update_graph_paths( config["graphs"][graph_id] = f"{module_str}:{attr_str}" -def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): +def python_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): # configure pip pip_install = ( "PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt" @@ -266,6 +286,29 @@ ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}' {f"WORKDIR {local_deps.working_dir}" if local_deps.working_dir else ""}""" +def node_config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): + faux_path = f"/deps/{config_path.parent.name}" + + return f"""FROM {base_image}:{config['node_version']} + +{os.linesep.join(config["dockerfile_lines"])} + +ADD . {faux_path} + +RUN cd {faux_path} && yarn install --frozen-lockfile + +ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}' + +WORKDIR {faux_path}""" + + +def config_to_docker(config_path: pathlib.Path, config: Config, base_image: str): + if config.get("node_version"): + return node_config_to_docker(config_path, config, base_image) + + return python_config_to_docker(config_path, config, base_image) + + def config_to_compose( config_path: pathlib.Path, config: Config, diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index 870f43faf..f53313986 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -232,6 +232,31 @@ ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'""" assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin +# node.js build used for LangGraph Cloud +def test_config_to_docker_nodejs(): + graphs = {"agent": "./graphs/agent.js:graph"} + actual_docker_stdin = config_to_docker( + PATH_TO_CONFIG, + validate_config( + { + "node_version": "20", + "graphs": graphs, + "dockerfile_lines": ["ARG meow", "ARG foo"], + } + ), + "langchain/langgraphjs-api", + ) + expected_docker_stdin = """FROM langchain/langgraphjs-api:20 +ARG meow +ARG foo +ADD . /deps/unit_tests +RUN cd /deps/unit_tests && yarn install --frozen-lockfile +ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}' +WORKDIR /deps/unit_tests""" + + assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin + + # config_to_compose def test_config_to_compose_simple_config(): graphs = {"agent": "./agent.py:graph"} From 84cb3ea15142a58e4c721db66b6e44827fb63106 Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Tue, 27 Aug 2024 21:06:09 -0700 Subject: [PATCH 157/287] [docs]: recursion limit info (#1501) * draft * vadym comments * spelling * final nits * final nits --- docs/_scripts/copy_notebooks.py | 1 + docs/docs/concepts/low_level.md | 12 ++- docs/docs/how-tos/index.md | 2 +- docs/mkdocs.yml | 1 + examples/recursion-limit.ipynb | 172 ++++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 examples/recursion-limit.ipynb diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 41970fa21..9cc2e8a76 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -38,6 +38,7 @@ _MANUAL = { "visualization.ipynb", "state-model.ipynb", "subgraph.ipynb", + "recursion-limit.ipynb", "force-calling-a-tool-first.ipynb", "pass-run-time-values-to-tools.ipynb", "tool-calling.ipynb", diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index e4a6778cd..9338e0ee8 100644 --- a/docs/docs/concepts/low_level.md +++ b/docs/docs/concepts/low_level.md @@ -401,7 +401,17 @@ def node_a(state, config): ... ``` -See [this guide](../how-tos/configuration.ipynb) for a full breakdown on configuration +See [this guide](../how-tos/configuration.ipynb) for a full breakdown on configuration. + +### Recursion Limit + +The recursion limit sets the maximum number of [super-steps](#graphs) the graph can execute during a single execution. Once the limit is reached, LangGraph will raise `GraphRecursionError`. By default this value is set to 25 steps. The recursion limit can be set on any graph at runtime, and is passed to `.invoke`/`.stream` via the config dictionary. Importantly, `recursion_limit` is a standalone `config` key and should not be passed inside the `configurable` key as all other user-defined configuration. See the example below: + +```python +graph.invoke(inputs, config={"recursion_limit": 5, "configurable":{"llm": "anthropic"}}) +``` + +Read [this how-to] to learn more about how the recursion limit works. ## Breakpoints diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index ac32c1ad2..bcd1b70a6 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -15,7 +15,7 @@ These how-to guides show how to achieve that controllability. - [How to create subgraphs](subgraph.ipynb) - [How to create branches for parallel execution](branching.ipynb) - [How to create map-reduce branches for parallel execution](map-reduce.ipynb) - +- [How to control graph recursion limit](recursion-limit.ipynb) ## Persistence diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index ec1f9faab..20b357da8 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -129,6 +129,7 @@ nav: - Create subgraphs: how-tos/subgraph.ipynb - Create branches for parallel execution: how-tos/branching.ipynb - Create map-reduce branches for parallel execution: how-tos/map-reduce.ipynb + - Control graph recursion limit: how-tos/recursion-limit.ipynb - Persistence: - Add persistence ("memory"): how-tos/persistence.ipynb - Manage conversation history: how-tos/memory/manage-conversation-history.ipynb diff --git a/examples/recursion-limit.ipynb b/examples/recursion-limit.ipynb new file mode 100644 index 000000000..0021baa8b --- /dev/null +++ b/examples/recursion-limit.ipynb @@ -0,0 +1,172 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to control graph recursion limit\n", + "\n", + "You can set the graph recursion limit when invoking or streaming the graph. The recursion limit sets the number of supersteps that the graph is allowed to execute before it raises an error. Read more about the concept of recursion limits [here](https://langchain-ai.github.io/langgraph/concepts/low_level/#recursion-limit). Let's see an example of this in a simple graph with parallel branches to better understand exactly how the recursion limit works.\n", + "\n", + "## Define our graph" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import operator\n", + "from typing import Annotated, Any\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "from langgraph.graph import StateGraph, START, END\n", + "\n", + "\n", + "class State(TypedDict):\n", + " # The operator.add reducer fn makes this append-only\n", + " aggregate: Annotated[list, operator.add]\n", + "\n", + "\n", + "class ReturnNodeValue:\n", + " def __init__(self, node_secret: str):\n", + " self._value = node_secret\n", + "\n", + " def __call__(self, state: State) -> Any:\n", + " print(f\"Adding {self._value} to {state['aggregate']}\")\n", + " return {\"aggregate\": [self._value]}\n", + "\n", + "\n", + "builder = StateGraph(State)\n", + "builder.add_node(\"a\", ReturnNodeValue(\"I'm A\"))\n", + "builder.add_edge(START, \"a\")\n", + "builder.add_node(\"b\", ReturnNodeValue(\"I'm B\"))\n", + "builder.add_node(\"c\", ReturnNodeValue(\"I'm C\"))\n", + "builder.add_node(\"d\", ReturnNodeValue(\"I'm D\"))\n", + "builder.add_edge(\"a\", \"b\")\n", + "builder.add_edge(\"a\", \"c\")\n", + "builder.add_edge(\"b\", \"d\")\n", + "builder.add_edge(\"c\", \"d\")\n", + "builder.add_edge(\"d\", END)\n", + "graph = builder.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "display(Image(graph.get_graph().draw_mermaid_png()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, our graph will execute nodes `b` and `c` in parallel (i.e. in a single super-step), which means that if we run this graph it should take exactly 3 steps. We can set the recursion limit to 3 first to check that it raises an error (the recursion limit is inclusive, so if the limit is 3 the graph will raise an error when it reaches step 3) as expected: " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Adding I'm A to []\n", + "Adding I'm B to [\"I'm A\"]\n", + "Adding I'm C to [\"I'm A\"]\n", + "Adding I'm D to [\"I'm A\", \"I'm B\", \"I'm C\"]\n", + "Recursion Error\n" + ] + } + ], + "source": [ + "from langgraph.errors import GraphRecursionError\n", + "\n", + "try:\n", + " graph.invoke({\"aggregate\": []},{\"recursion_limit\":3})\n", + "except GraphRecursionError:\n", + " print(\"Recursion Error\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Success! The graph raised an error as expected - now let's test setting the recursion limit to 4 and ensure that the graph succeeds in this case:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Adding I'm A to []\n", + "Adding I'm B to [\"I'm A\"]\n", + "Adding I'm C to [\"I'm A\"]\n", + "Adding I'm D to [\"I'm A\", \"I'm B\", \"I'm C\"]\n" + ] + } + ], + "source": [ + "try:\n", + " graph.invoke({\"aggregate\": []},{\"recursion_limit\":4})\n", + "except GraphRecursionError:\n", + " print(\"Recursion Error\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perfect, just as we expected the graph runs successfully in this case. \n", + "\n", + "Setting the correct graph recursion limit is important for avoiding graph runs stuck in long-running loops and thus helps minimize unnecessary costs" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 947f8c8c1dc57542bec95550ab5a85bd17b09dfe Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Wed, 28 Aug 2024 10:32:09 -0400 Subject: [PATCH 158/287] docs: fix astream in reflection notebook (#1512) --- examples/reflection/reflection.ipynb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/reflection/reflection.ipynb b/examples/reflection/reflection.ipynb index ca42160bd..6e75353ad 100644 --- a/examples/reflection/reflection.ipynb +++ b/examples/reflection/reflection.ipynb @@ -96,7 +96,7 @@ ")\n", "llm = ChatFireworks(\n", " model=\"accounts/fireworks/models/mixtral-8x7b-instruct\",\n", - " model_kwargs={\"max_tokens\": 32768},\n", + " max_tokens=32768\n", ")\n", "generate = prompt | llm" ] @@ -334,13 +334,13 @@ } ], "source": [ - "async for event in graph.astream(\n", - " [\n", + "async for event in graph.astream({\n", + " \"messages\": [\n", " HumanMessage(\n", " content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n", " )\n", " ],\n", - "):\n", + "}):\n", " print(event)\n", " print(\"---\")" ] @@ -534,7 +534,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.9" } }, "nbformat": 4, From 666fb3f31dc23255811736865326bf0db15cb9fc Mon Sep 17 00:00:00 2001 From: Pratyush Verma Date: Wed, 28 Aug 2024 15:45:02 +0100 Subject: [PATCH 159/287] Fix typo --- examples/rag/langgraph_agentic_rag.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/rag/langgraph_agentic_rag.ipynb b/examples/rag/langgraph_agentic_rag.ipynb index 73d02f14a..94e4e7b7f 100644 --- a/examples/rag/langgraph_agentic_rag.ipynb +++ b/examples/rag/langgraph_agentic_rag.ipynb @@ -126,7 +126,7 @@ "source": [ "## Agent state\n", " \n", - "We will defined a graph.\n", + "We will define a graph.\n", "\n", "A `state` object that it passes around to each node.\n", "\n", From 3bbe09dac4a2561d61df2980b591ec1fd4d83313 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Wed, 28 Aug 2024 17:56:26 +0200 Subject: [PATCH 160/287] Fix missing comma --- libs/cli/langgraph_cli/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index f7b8355d9..49c1102b4 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -37,7 +37,7 @@ def validate_config(config: Config) -> Config: ) if config.get("node_version"): - if config["node_version"] not in ("20"): + if config["node_version"] not in ("20",): raise click.UsageError( f"Unsupported Node.js version: {config['node_version']}. " "Currently only `node_version: \"20\"` is supported." From 7637a769ddc02bf5ed4305bde741bba5ed5148b2 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Wed, 28 Aug 2024 12:07:29 -0400 Subject: [PATCH 161/287] docs: update reflection notebook (#1515) * docs: update reflection notebook * update more * update more --- examples/reflection/reflection.ipynb | 359 +++++++++++++++++---------- 1 file changed, 223 insertions(+), 136 deletions(-) diff --git a/examples/reflection/reflection.ipynb b/examples/reflection/reflection.ipynb index 6e75353ad..253dfd075 100644 --- a/examples/reflection/reflection.ipynb +++ b/examples/reflection/reflection.ipynb @@ -42,7 +42,16 @@ "execution_count": 2, "id": "3368f330-cad6-4d35-a291-68fbf4389d98", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "LANGCHAIN_API_KEY ········\n", + "FIREWORKS_API_KEY ········\n" + ] + } + ], "source": [ "import getpass\n", "import os\n", @@ -111,19 +120,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "Title: The Relevance of The Little Prince in Modern Childhood\n", + "Title: The Eternal Relevance of The Little Prince in Modern Childhood\n", "\n", - "The Little Prince, a novella by Antoine de Saint-Exupéry, has been a childhood favorite for generations. Despite being published over seven decades ago, its timeless themes continue to resonate with modern children, making it highly relevant in contemporary childhood.\n", + "Introduction:\n", + "Antoine de Saint-Exupéry's The Little Prince is a timeless novella that has captured the hearts and minds of children and adults alike for over seven decades. Its enduring charm and profound wisdom have transcended generations, making it a classic staple in childhood literature. This essay explores the reasons why The Little Prince remains relevant in modern childhood.\n", "\n", - "Firstly, the story explores the complex nature of human relationships, which is particularly relevant for modern children growing up in an increasingly connected yet impersonal world. Through the little prince's encounters with various grown-ups on different planets, the book highlights the importance of genuine connections and understanding. In an age where digital communication often replaces face-to-face interaction, this message is more pertinent than ever. The Little Prince encourages children to look beyond superficial relationships and seek deeper connections, fostering empathy and emotional intelligence.\n", + "First Paragraph:\n", + "One of the primary reasons for The Little Prince's relevance is its exploration of themes that resonate with children today. The story addresses universal aspects of childhood, such as the struggle to understand the world, the desire for friendship and love, and the pain of loss and loneliness. The Little Prince's encounters with various grown-ups, each representing different facets of adult absurdity, mirror the confusion and disillusionment children experience as they grow and navigate their way through a complex world.\n", "\n", - "Secondly, the book deals with the concept of responsibility and self-discovery, elements that are integral to a child's growth. The little prince's journey is essentially a quest for self-discovery, leading him to realize his responsibility towards his beloved rose. This narrative encourages modern children to embrace their individuality while understanding the significance of their actions. In a society that often overlooks the emotional well-being of children, The Little Prince offers a refreshing perspective on personal growth and responsibility.\n", + "Second Paragraph:\n", + "Moreover, The Little Prince promotes values that are essential for modern childhood. It emphasizes the importance of imagination, creativity, and curiosity, encouraging children to question, explore, and seek their own truths. The Little Prince's friendship with the fox teaches children about the value of emotional connections, empathy, and responsibility, lessons that are increasingly vital in our technology-driven, fast-paced society.\n", "\n", - "Thirdly, the book addresses the challenging theme of loss and bereavement. The little prince's departure from his asteroid and his subsequent encounters with the fox and the snake are profound reflections on the inevitability of loss and the importance of cherishing relationships. In a time when children are exposed to various forms of loss, from the death of loved ones to environmental degradation, The Little Prince provides a gentle yet powerful way to understand and cope with these experiences.\n", + "Third Paragraph:\n", + "The Little Prince also serves as a reminder of the significance of nature and the environment in our lives. The story's depiction of the desert, the baobabs, and the mysterious asteroid B-612 fosters an appreciation for the beauty and fragility of the natural world. In an era of climate change and environmental degradation, The Little Prince's message about the importance of nurturing and preserving our planet is more relevant than ever.\n", "\n", - "However, some critics argue that the book's pace and abstract concepts might be challenging for modern children with short attention spans. To address this, a revised version could incorporate more visual elements and interactive activities to engage young readers better. Additionally, supplementary materials explaining the book's themes in simpler terms could be provided for parents and educators to use in discussions with children.\n", + "Fourth Paragraph:\n", + "Furthermore, The Little Prince offers a unique perspective on mental health and emotional well-being. The story delicately tackles issues such as depression, isolation, and the search for meaning, providing a nuanced understanding of these complex topics. By presenting these themes in a relatable and age-appropriate manner, The Little Prince helps children develop emotional intelligence and resilience, enabling them to better cope with the challenges they face in their daily lives.\n", "\n", - "In conclusion, The Little Prince remains relevant in modern childhood due to its exploration of human relationships, self-discovery, and loss. These themes, wrapped in a captivating narrative, offer valuable lessons for modern children. While some adaptations may be necessary to cater to the preferences of today's children, the essence of the story remains a powerful tool for teaching emotional intelligence, personal growth, and resilience." + "Conclusion:\n", + "In conclusion, The Little Prince remains a relevant and essential read for modern childhood due to its exploration of timeless themes, promotion of essential values, emphasis on nature and environmental stewardship, and sensitive treatment of mental health and emotional well-being. By engaging with this classic tale, children can gain invaluable insights and skills that will serve them well throughout their lives. The Little Prince's enduring legacy is a testament to its ability to captivate, inspire, and educate generations of children, making it an indispensable part of childhood literature." ] } ], @@ -175,21 +190,35 @@ "name": "stdout", "output_type": "stream", "text": [ - "Essay Grade: B+\n", + "Essay Critique and Recommendations:\n", "\n", - "The essay you submitted provides a clear and well-structured argument about the relevance of The Little Prince in modern childhood. You have demonstrated a strong understanding of the text and its themes, and have effectively applied them to the context of contemporary childhood. However, there are some areas where improvement could be made to enhance the depth, style, and overall flow of your essay.\n", + "Title: The Eternal Relevance of The Little Prince in Modern Childhood\n", "\n", - "1. Length: While your essay is well-written and informative, it is relatively brief. Expanding on each point with more detailed analysis and examples would strengthen your argument and demonstrate a more comprehensive understanding of the text. Aim for a minimum of 500 words to allow for a more in-depth exploration of your ideas.\n", + "Introduction:\n", + "The introduction provides a clear and concise overview of the topic, setting the stage for the rest of the essay. The author has done an excellent job of establishing the significance of The Little Prince and its enduring appeal.\n", "\n", - "2. Depth: Although you have touched upon the relevance of the novel's themes, further analysis is needed to truly establish its significance in modern childhood. For example, when discussing the complex nature of human relationships, delve into how the digital age affects children's communication skills, and how The Little Prince addresses this issue. Providing concrete examples from the text and connecting them to real-world scenarios will make your argument more compelling.\n", + "First Paragraph:\n", + "The first paragraph effectively highlights the universal themes present in The Little Prince that resonate with children today. The author could improve the paragraph by providing specific examples from the book to illustrate each theme, making the essay more engaging and demonstrating a deeper understanding of the text.\n", "\n", - "3. Style: To engage your readers more effectively, consider varying your sentence structure and length. Using a mix of simple, compound, and complex sentences will improve the flow of your essay and make it more engaging to read. Additionally, watch your tense consistency. Ensure that you maintain the same tense throughout your essay to avoid confusion.\n", + "Second Paragraph:\n", + "The second paragraph emphasizes the values promoted by The Little Prince and their relevance to modern childhood. The author could expand on this by discussing how these values can be applied in everyday life, providing practical examples for children to follow. Additionally, the author may consider delving into the role of the fox in the story and its impact on the Prince's character development.\n", "\n", - "4. Recommendations: While your suggestions for adaptation are a good start, they could be expanded upon to provide more comprehensive recommendations. For example, you may want to discuss different methods of incorporating visual elements and interactive activities, such as illustrations, quizzes, or discussion questions. This will demonstrate that you have thoughtfully considered the needs of modern children and have developed strategies to address these challenges.\n", + "Third Paragraph:\n", + "The third paragraph discusses the importance of nature and environmental stewardship in The Little Prince. The author could strengthen this paragraph by connecting the story's themes to current environmental issues, helping children understand the relevance and urgency of protecting the planet. Furthermore, the author may include specific strategies children can adopt to contribute to environmental conservation.\n", "\n", - "5. Conclusion: Your conclusion could benefit from a stronger summarization of your key points and an assertive final statement about the relevance of The Little Prince in modern childhood. Tying all your arguments together in a concise and powerful manner will leave a lasting impression on your readers and solidify your position.\n", + "Fourth Paragraph:\n", + "The fourth paragraph addresses the sensitive topic of mental health and emotional well-being in The Little Prince. The author could improve this paragraph by providing more context on the representation of these issues in the story and offering resources or advice for children who may be experiencing similar emotions. This approach would ensure the essay is not only informative but also supportive and empathetic.\n", "\n", - "Overall, your essay is well-researched and provides a solid foundation for a compelling argument about the relevance of The Little Prince in modern childhood. With some expansion, deeper analysis, and stylistic improvements, your essay can achieve an even higher level of excellence." + "Conclusion:\n", + "The conclusion effectively summarizes the main points of the essay while emphasizing the importance of The Little Prince in modern childhood. The author could consider adding a call-to-action, encouraging children to read or revisit the novella and reflect on its lessons. Additionally, the author may include a brief statement on the lasting impact of The Little Prince and its potential influence on future generations.\n", + "\n", + "Recommendations:\n", + "\n", + "1. Incorporate more direct quotes from the text to support arguments and engage the reader.\n", + "2. Expand on specific themes, values, and concepts to provide greater depth and insight.\n", + "3. Offer practical applications and strategies for children to apply the lessons from The Little Prince in their daily lives.\n", + "4. Consider the age range and reading level of the intended audience and adjust the language and content accordingly.\n", + "5. Ensure a balanced mix of summary, analysis, and interpretation to maintain the reader's interest and demonstrate a thorough understanding of the text." ] } ], @@ -220,19 +249,45 @@ "name": "stdout", "output_type": "stream", "text": [ - "Title: The Relevance of The Little Prince in Modern Childhood: A Contemporary Analysis\n", + "Title: The Eternal Relevance of The Little Prince in Modern Childhood\n", "\n", - "In the digital age, where human connections are often overshadowed by virtual communication, Antoine de Saint-Exupéry's The Little Prince remains a timeless classic that offers invaluable insights for modern children. This essay aims to delve deeper into the relevance of this novella in contemporary childhood, focusing on the complex nature of human relationships, self-discovery, and the inevitability of loss.\n", + "Introduction:\n", + "The introduction provides a clear and concise overview of the topic, setting the stage for the rest of the essay. The author has done an excellent job of establishing the significance of The Little Prince and its enduring appeal.\n", "\n", - "Firstly, The Little Prince offers a powerful critique of the superficiality that permeates the digital world. Through the little prince's encounters with various grown-ups, the book emphasizes the importance of genuine connections and understanding. Despite being published in 1943, Saint-Exupéry's work uncannily predicts the isolating effects of technology on human interaction. It encourages children to seek deeper connections, fostering empathy and emotional intelligence. For instance, the little prince's relationship with the fox teaches him that \"the eyes are blind, and you have to look with the heart\" (Saint-Exupéry, 1943, p. 48). In the context of modern childhood, where children are increasingly dependent on digital devices, this message is more pertinent than ever.\n", + "First Paragraph:\n", + "The first paragraph effectively highlights the universal themes present in The Little Prince that resonate with children today. To improve the paragraph, specific examples from the book will be added to illustrate each theme, making the essay more engaging and demonstrating a deeper understanding of the text.\n", "\n", - "Secondly, The Little Prince addresses the challenges of self-discovery and responsibility faced by modern children. The little prince's journey to Earth can be seen as an exploration of his individuality and understanding of his role in the world. His relationship with the rose illustrates the significance of taking responsibility for one's actions. In the current world, where children are often left to navigate their personal growth without proper guidance, the book offers a refreshing perspective on self-discovery, responsibility, and the importance of inner beauty.\n", + "Second Paragraph:\n", + "The second paragraph emphasizes the values promoted by The Little Prince and their relevance to modern childhood. The author will expand on this by discussing how these values can be applied in everyday life, providing practical examples for children to follow. Additionally, the author will delve into the role of the fox in the story and its impact on the Prince's character development.\n", "\n", - "Thirdly, The Little Prince offers a nuanced understanding of loss and bereavement, which is increasingly relevant to modern children. Through the little prince's departure from his asteroid and his subsequent encounters with the fox and the snake, Saint-Exupéry delivers a profound reflection on the inevitability of loss and the importance of cherishing relationships. As children grapple with issues like environmental degradation, bullying, or the death of loved ones, The Little Prince provides a gentle yet powerful way to understand and cope with these experiences.\n", + "Third Paragraph:\n", + "The third paragraph discusses the importance of nature and environmental stewardship in The Little Prince. To strengthen this paragraph, the author will connect the story's themes to current environmental issues, helping children understand the relevance and urgency of protecting the planet. Furthermore, the author will include specific strategies children can adopt to contribute to environmental conservation.\n", "\n", - "However, as noted by critics, the book's abstract language and lengthy monologues may present challenges for some modern children. To address this, adaptations can be made to better align the book with their preferences and needs. For instance, incorporating more visual elements such as illustrations can help maintain engagement, while interactive activities like quizzes or discussion questions can deepen understanding. Furthermore, supplementary materials explaining the book's themes in simpler terms can aid parents and educators in guiding children through complex discussions.\n", + "Fourth Paragraph:\n", + "The fourth paragraph addresses the sensitive topic of mental health and emotional well-being in The Little Prince. The author will improve this paragraph by providing more context on the representation of these issues in the story and offering resources or advice for children who may be experiencing similar emotions. This approach will ensure the essay is not only informative but also supportive and empathetic.\n", "\n", - "In conclusion, The Little Prince remains a powerful and enduring narrative for modern children as it delves into the complex nature of human relationships, self-discovery, and loss. With thoughtful adaptations and insightful guidance, this timeless classic can continue to guide young readers through their personal growth and emotional development. The Little Prince truly is a testament to the power of literature as a vehicle for conveying universal truths and emotions, making it an indispensable part of childhood reading experiences." + "Conclusion:\n", + "The conclusion effectively summarizes the main points of the essay while emphasizing the importance of The Little Prince in modern childhood. The author will add a call-to-action, encouraging children to read or revisit the novella and reflect on its lessons. Additionally, the author will include a brief statement on the lasting impact of The Little Prince and its potential influence on future generations.\n", + "\n", + "Revised Essay:\n", + "\n", + "Introduction:\n", + "Antoine de Saint-Exupéry's The Little Prince is a timeless novella that has captured the hearts and minds of children and adults alike for over seven decades. Its enduring charm and profound wisdom have transcended generations, making it a classic staple in childhood literature. This essay explores the reasons why The Little Prince remains relevant in modern childhood, focusing on its exploration of universal themes, promotion of essential values, emphasis on nature and environmental stewardship, and sensitive treatment of mental health and emotional well-being.\n", + "\n", + "First Paragraph:\n", + "The Little Prince explores themes that resonate with children today, such as the struggle to understand the world, the desire for friendship and love, and the pain of loss and loneliness. For example, the Prince's encounter with the conceited man (Chapter IV) mirrors the frustration children experience when interacting with adults who prioritize their own egos over genuine connections. By presenting these themes in a relatable and age-appropriate manner, The Little Prince helps children develop emotional intelligence and resilience, enabling them to better cope with the challenges they face in their daily lives.\n", + "\n", + "Second Paragraph:\n", + "The Little Prince promotes values that are essential for modern childhood. It emphasizes the importance of imagination, creativity, and curiosity, encouraging children to question, explore, and seek their own truths. For instance, the Prince's friendship with the fox teaches children about the value of emotional connections, empathy, and responsibility. In our technology-driven, fast-paced society, these values are increasingly vital for building meaningful relationships and fostering emotional well-being.\n", + "\n", + "Third Paragraph:\n", + "The Little Prince also serves as a reminder of the significance of nature and the environment in our lives. The story's depiction of the desert, the baobabs, and the mysterious asteroid B-612 fosters an appreciation for the beauty and fragility of the natural world. In an era of climate change and environmental degradation, The Little Prince's message about the importance of nurturing and preserving our planet is more relevant than ever. To contribute to environmental conservation, children can adopt simple strategies, such as reducing waste, planting trees, and raising awareness about environmental issues in their communities.\n", + "\n", + "Fourth Paragraph:\n", + "Furthermore, The Little Prince offers a unique perspective on mental health and emotional well-being. The story delicately tackles issues such as depression, isolation, and the search for meaning, providing a nuanced understanding of these complex topics. By presenting these themes in a relatable and age-appropriate manner, The Little Prince helps children develop emotional intelligence and resilience, enabling them to better cope with the challenges they face in their daily lives. For children struggling with mental health issues, it is essential to seek help from trusted adults, such as parents, teachers, or mental health professionals.\n", + "\n", + "Conclusion:\n", + "In conclusion, The Little Prince's enduring legacy is a testament to its ability to captivate, inspire, and educate generations of children, making it an indispensable part of childhood literature. By engaging with this classic tale, children can gain invaluable insights and skills that will serve them well throughout their lives. The author encourages children to read or revisit The Little Prince and reflect on its lessons, ultimately applying its timeless wisdom to their daily lives." ] } ], @@ -263,6 +318,7 @@ "from typing import Annotated, List, Sequence\n", "from langgraph.graph import END, StateGraph, START\n", "from langgraph.graph.message import add_messages\n", + "from langgraph.checkpoint.memory import MemorySaver\n", "from typing_extensions import TypedDict\n", "\n", "\n", @@ -270,20 +326,20 @@ " messages: Annotated[list, add_messages]\n", "\n", "\n", - "async def generation_node(state: Sequence[BaseMessage]):\n", - " return await generate.ainvoke({\"messages\": state})\n", + "async def generation_node(state: State) -> State:\n", + " return {\"messages\": [await generate.ainvoke(state['messages'])]}\n", "\n", "\n", - "async def reflection_node(messages: Sequence[BaseMessage]) -> List[BaseMessage]:\n", + "async def reflection_node(state: State) -> State:\n", " # Other messages we need to adjust\n", " cls_map = {\"ai\": HumanMessage, \"human\": AIMessage}\n", " # First message is the original user request. We hold it the same for all nodes\n", - " translated = [messages[0]] + [\n", - " cls_map[msg.type](content=msg.content) for msg in messages[1:]\n", + " translated = [state['messages'][0]] + [\n", + " cls_map[msg.type](content=msg.content) for msg in state['messages'][1:]\n", " ]\n", - " res = await reflect.ainvoke({\"messages\": translated})\n", + " res = await reflect.ainvoke(translated)\n", " # We treat the output of this as human feedback for the generator\n", - " return HumanMessage(content=res.content)\n", + " return {\"messages\": [HumanMessage(content=res.content)]}\n", "\n", "\n", "builder = StateGraph(State)\n", @@ -293,42 +349,54 @@ "\n", "\n", "def should_continue(state: List[BaseMessage]):\n", - " if len(state) > 6:\n", + " if len(state[\"messages\"]) > 6:\n", " # End after 3 iterations\n", " return END\n", " return \"reflect\"\n", "\n", "\n", + "\n", "builder.add_conditional_edges(\"generate\", should_continue)\n", "builder.add_edge(\"reflect\", \"generate\")\n", - "graph = builder.compile()" + "memory = MemorySaver()\n", + "graph = builder.compile(checkpointer=memory)" ] }, { "cell_type": "code", "execution_count": 9, - "id": "06263a07-8a15-4ec3-b692-1c6cef3b1c1f", + "id": "010ce60a-8b7d-4258-99d1-52705146844f", "metadata": {}, + "outputs": [], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"1\"}}" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "06263a07-8a15-4ec3-b692-1c6cef3b1c1f", + "metadata": { + "scrolled": true + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'generate': AIMessage(content=\"Title: The Enduring Relevance of The Little Prince: A Timeless Message for Modern Life\\n\\nIntroduction:\\nAntoine de Saint-Exupéry's The Little Prince is a canonical work of literature, beloved by generations since its publication in 1943. The novella has been translated into more than 250 languages and sold over 140 million copies, making it one of the best-selling books of all time. Its enchanting story transcends cultural boundaries and continues to captivate audiences of all ages. The Little Prince's timeless message remains relevant in modern life, offering insightful commentary on themes such as love, loneliness, responsibility, and the superficiality of the adult world. In this essay, we will discuss the topicality of The Little Prince and its enduring message in today's fast-paced, digitally-connected society.\\n\\nBody Paragraph 1 - Love and Loneliness:\\nOne of the most enduring aspects of The Little Prince is its exploration of love and relationships in a world plagued by superficiality. The Little Prince's encounters with the fox, the rose, and his pilot reveal the importance of genuine connections and the pain of loss. In today's modern era, characterized by increasing social isolation, the message of The Little Prince serves as a reminder of the crucial role empathy and understanding play in fostering meaningful relationships. The consequences of isolation, depression, and loneliness continue to grow in modern life, making Saint-Exupéry's exploration of love and loneliness as vital now as it was then.\\n\\nBody Paragraph 2 - Responsibility and Self-Discovery:\\nThroughout the novella, Saint-Exupéry emphasizes the significance of taking responsibility and learning from one's experiences—core components of personal growth and self-discovery. The Little Prince's journey to various planets, each inhabited by an absurd, self-absorbed grown-up, reflects on the responsibility people have to learn from their actions and understand their impact on others. The modern world demands people to navigate complex social, professional, and personal situations daily. Thus, The Little Prince's lessons in responsibility and self-discovery are essential when addressing pressing issues like mental health, self-awareness, and communication in contemporary society.\\n\\nBody Paragraph 3 - The Superficiality of the Adult World:\\nCritics often discuss the novella's critique of the superficiality of the adult world, which remains relevant today, given society's heightened emphasis on materialism and status. The Little Prince's encounters with businessmen and geographers represent the folly of misunderstanding values and blindly pursuing worldly possessions. Today's capitalist societies frequently struggle to balance priorities, often rewarding materialistic pursuits over the development of meaningful relationships. The Little Prince serves as a profound reminder to maintain a sense of perspective, recognize the importance of intangible connections, and avoid the trappings of superficiality.\\n\\nConclusion:\\nUltimately, The Little Prince continues to top bestseller lists because its themes of love, loneliness, responsibility, and the superficiality of the adult world resonate with people across time and culture. The novella's resilient popularity and topicality reflect its relevance in tackling contemporary societal issues, making it a timeless masterpiece that transcends generations. As we navigate the complexities of modern life, The Little Prince's message is one we should keep close to our hearts: we must never lose sight of the simple, yet profound, lessons the story teaches us about cherishing meaningful connections, embracing personal growth, and resisting the shallow temptations of adult life.\\n\\nRevised Essay:\\n\\nTitle: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry's The Little Prince is an enduring classic that has touched the hearts of millions since its publication in 1943. The novella has been translated into more than 300 languages, and over 200 million copies have been sold, making it one of the bestselling books ever written. The Little Prince's timeless message about love, friendship, responsibility, and the adult world remains incredibly relevant in the 21st century. This essay will analyze the topicality of The Little Prince and explore the many ways its universal themes connect with modern life.\\n\\nBody Paragraph 1 - Love, Loss, and Friendship:\\nThe Little Prince teaches powerful lessons about love, friendship, and loss that continue to resonate with readers today. The novella's exploration of grief and heartache is as poignant today as it was when it was first published. The tales of the Little Prince's encounters with the fox, the rose, and his pilot highlight the transcendent power of meaningful connections and the pain of losing those we care about. In a digital age where fleeting online interactions can dominate our time, The Little Prince serves as a reminder to cherish genuine friendships and treasure the connections we make with others.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nThroughout the story, Saint-Exupéry highlights the significance of taking responsibility and engaging in self-discovery. The Little Prince's journey to various planets, each inhabited by a reductive grown-up, teaches the reader about the impact actions can have on others. In a world where emotional intelligence and empathy are increasingly vital due to ever-evolving social, professional, and personal obligations, The Little Prince's lessons on responsibility and personal growth remain crucial. Mental health, self-awareness, and communication are critical issues in modern society, making the exploration of these themes as essential in today's world as when the book was first published.\\n\\nBody Paragraph 3 - Rejecting the Superficiality of the Adult World:\\nThe Little Prince's critique of the superficiality of the adult world remains strikingly relevant in modern society. The novel's portrait of grown-ups consumed by materialism, social status, and vanity rings true today, more than ever, as individuals and societies race to acquire wealth, status, and possessions. The Little Prince serves as a poignant reminder to resist the superficiality of the adult world and maintain a balanced perspective, cherishing meaningful connections and eschewing the trappings of materialism.\\n\\nConclusion:\\nThe Little Prince's universal themes continue to captivate and inspire readers because the lessons it teaches about love, friendship, responsibility, and the adult world are still incredibly pertinent today. The novel's topicality and enduring popularity validate its relevance in addressing contemporary societal issues like mental health, self-awareness, communication, and materialism. As we maneuver the challenges of the 21st century, The Little Prince's enduring wisdom—to cherish deep relationships, value personal growth, and reject the superficiality of adult life—continues to resonate and encourage readers to reassess their priorities and find meaning in connection and experience.\")}\n", + "{'generate': {'messages': [AIMessage(content='Title: The Little Prince: A Topical Allegory for Modern Life\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s \"The Little Prince\" is a classic novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children\\'s story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince\" and discuss how its message can be applied to the complexities of the modern world.\\n\\nBody Paragraph 1 - The Allegory of the Little Prince:\\n\"The Little Prince\" is an allegorical tale that explores various aspects of the human condition through its whimsical characters and situations. The Little Prince himself represents innocence, curiosity, and the importance of human connection. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. These representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\\n\\nBody Paragraph 2 - The Relevance of the Little Prince\\'s Message:\\nThe Little Prince\\'s message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. The Little Prince encourages readers to cherish and nurture genuine relationships, reminding us that true happiness and fulfillment come from understanding and empathizing with others.\\n\\nBody Paragraph 3 - The Critique of Modern Society:\\n\"The Little Prince\" also offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. These themes resonate strongly in today\\'s world, where wealth inequality and environmental degradation are pressing issues. The story serves as a reminder that the pursuit of material possessions and status often comes at the expense of our own happiness and the well-being of our planet.\\n\\nConclusion:\\nIn conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. By embracing the story\\'s wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.', response_metadata={'token_usage': {'prompt_tokens': 72, 'total_tokens': 632, 'completion_tokens': 560}, 'model_name': 'accounts/fireworks/models/mixtral-8x7b-instruct', 'system_fingerprint': '', 'finish_reason': 'stop', 'logprobs': None}, id='run-b39a25ab-24f6-42d0-96c2-0f74c3ecc8f7-0', usage_metadata={'input_tokens': 72, 'output_tokens': 560, 'total_tokens': 632})]}}\n", "---\n", - "{'reflect': HumanMessage(content=\"Introduction:\\nThe essay provides a solid introduction to the topic, clearly stating the book's significance and its continued relevance in modern life. I would suggest providing more specific connections to the present day to emphasize the enduring relevance of The Little Prince. For instance, you could mention current events or issues that are directly related to the themes discussed in Saint-Exupéry's work (e.g., studies on loneliness and mental health in the digital age).\\n\\nBody Paragraph 1 - Love and Loneliness:\\nThe paragraph effectively explains how the themes of love and loneliness resonate with the modern era. However, I would like to see more concrete examples from the book to strengthen the analysis. Consider providing a specific interaction or quote from The Little Prince to more directly tie it to the concepts of isolation, depression, and loneliness in today's world.\\n\\nBody Paragraph 2 - Responsibility and Self-Discovery:\\nThis paragraph provides a good analysis of how Saint-Exupéry emphasizes responsibility and self-discovery. However, it could benefit from a stronger connection to contemporary society. It would be helpful to provide examples from real-life situations or psychological studies that demonstrate the importance of mental health, self-awareness, and communication in today's world.\\n\\nBody Paragraph 3 - The Superficiality of the Adult World:\\nThe criticism of materialism and status in modern society is well-presented in this paragraph. However, you could strengthen the analysis by offering specific examples of the adult world's superficiality in the context of the 21st century, such as a focus on social media and online presence. Moreover, consider further elaborating on the contrast between the materialistic world and The Little Prince's emphasis on meaningful relationships.\\n\\nConclusion:\\nThe conclusion effectively summarizes the importance of the themes addressed in the novel. Nonetheless, it could benefit from a stronger final statement that reiterates the significance of the stories and lessons from The Little Prince in the modern context. Consider restating the main ideas in a way that reinforces the parallels between the book and contemporary life.\\n\\nOverall, I would encourage you to strengthen the connections between the novel's themes and modern society by providing more specific examples and relevant real-world issues. Furthermore, I recommend a word count of around 1,200-1,500 words for your essay to provide enough space to thoroughly analyze and discuss the topics presented. By offering a more in-depth analysis, your argument would become more persuasive and the relevance of the novel even more apparent.\")}\n", + "{'reflect': {'messages': [HumanMessage(content='Essay Critique and Recommendations:\\n\\nTitle: The Little Prince: A Topical Allegory for Modern Life\\n\\nIntroduction:\\nThe introduction effectively sets the stage for the essay by providing background information on \"The Little Prince\" and its relevance in modern life. However, consider adding a hook to engage the reader\\'s attention and create a stronger first impression.\\n\\nBody Paragraph 1 - The Allegory of the Little Prince:\\nThis paragraph provides a clear explanation of the allegorical nature of \"The Little Prince.\" To enhance this section, consider offering specific examples from the text to illustrate how the characters and situations symbolize various aspects of adult life. This will strengthen your analysis and make it more engaging for the reader.\\n\\nBody Paragraph 2 - The Relevance of the Little Prince\\'s Message:\\nThe relevance of the Little Prince\\'s message is well-articulated in this paragraph. To further strengthen your argument, consider discussing the consequences of ignoring this message in the context of modern society. This will help emphasize the importance of the Little Prince\\'s wisdom and its relevance to contemporary issues.\\n\\nBody Paragraph 3 - The Critique of Modern Society:\\nThis paragraph effectively highlights the story\\'s critique of modern society. To deepen your analysis, explore how the themes of materialism, consumerism, and the pursuit of power interconnect and contribute to the challenges faced by modern society. Additionally, consider discussing potential solutions or actions inspired by the Little Prince\\'s message that could help address these issues.\\n\\nConclusion:\\nThe conclusion effectively summarizes the main points of the essay and emphasizes the relevance of \"The Little Prince\" in modern life. To further enhance this section, consider incorporating a thought-provoking question or statement that encourages readers to reflect on the story\\'s message and its implications for their own lives.\\n\\nRecommendations:\\n1. Expand the essay to approximately 1,200-1,500 words to allow for a more in-depth analysis.\\n2. Incorporate specific examples and quotes from \"The Little Prince\" to support your arguments and engage the reader.\\n3. Ensure that each body paragraph contains a clear thesis statement, supporting evidence, and analysis.\\n4. Consider discussing counterarguments or potential criticisms of the Little Prince\\'s message to add depth and complexity to your essay.\\n5. Revise and edit the essay for clarity, coherence, and grammar.')]}}\n", "---\n", - "{'generate': AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today\\'s society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel\\'s powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince\\'s depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince\\'s portrayal of the prince\\'s loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nPersonal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry\\'s exploration of self-awareness and personal growth is highly relevant. The Little Prince\\'s encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence. Research connecting emotional intelligence to mental health underscores the significance of the ideas presented in The Little Prince, demonstrating that higher emotional intelligence is positively associated with mental health and well-being (Schutte et al., 2001). This research supports the notion that the personal growth explored in The Little Prince remains a vital part of addressing mental health issues.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today\\'s digital age and social media-dominated society. For instance, the novel\\'s third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence and an obsession with acquiring digital \"followers\" and \"likes.\" By highlighting the emptiness of the materialistic pursuits, The Little Prince shows readers the importance of genuine human connections and rejecting superficial distractions (Soucy & Vedel, 2018). These themes are particularly relevant today, as younger generations struggle to find balance between their online and offline lives, frequently confronted with issues related to superficiality, self-promotion, and digital personas.\\n\\nConclusion:\\nThe Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel\\'s exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel\\'s wisdom and the importance of its messages in our daily lives.')}\n", + "{'generate': {'messages': [AIMessage(content='Title: The Little Prince: A Topical Allegory for Modern Life\\n\\nIntroduction:\\nIn Antoine de Saint-Exupéry\\'s classic novella \"The Little Prince,\" a young boy embarks on a journey through the universe, meeting various characters that symbolize different aspects of adult life. This timeless tale, published in 1943, remains incredibly relevant in today\\'s modern world. Its allegorical nature, thought-provoking message, and critique of modern society offer invaluable insights for readers of all ages. This essay will explore the allegory of \"The Little Prince,\" analyze the relevance of its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\\n\\nBody Paragraph 1 - The Allegory of the Little Prince:\\n\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. For instance, the king represents authority without substance, while the businessman embodies the futility of materialism. The fox, conversely, symbolizes the importance of forming genuine connections and nurturing meaningful relationships. These allegorical representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\\n\\nBody Paragraph 2 - The Relevance of the Little Prince\\'s Message:\\nThe Little Prince\\'s message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction. By embracing the story\\'s wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\\n\\nBody Paragraph 3 - The Critique of Modern Society:\\n\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. These themes resonate strongly in today\\'s world, where wealth inequality and environmental degradation are pressing issues. The story serves as a reminder that the pursuit of material possessions and status often comes at the expense of our own happiness and the well-being of our planet. To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\\n\\nConclusion:\\nIn conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. By embracing the story\\'s wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society. As the Little Prince so eloquently states, \"What is essential is invisible to the eye,\" reminding us that true happiness and fulfillment come from understanding and empathizing with others.\\n\\nExpanded Essay Recommendations:\\n\\n1. Expand the essay to approximately 1,200-1,500 words to allow for a more in-depth analysis.\\n2. Incorporate specific examples and quotes from \"The Little Prince\" to support your arguments and engage the reader. For instance, use quotes like, \"You become responsible, forever, for what you have tamed,\" to emphasize the importance of forming genuine connections.\\n3. Ensure that each body paragraph contains a clear thesis statement, supporting evidence, and analysis.\\n4. Consider discussing counterarguments or potential criticisms of the Little Prince\\'s message to add depth and complexity to your essay. For example, explore the idea that the pursuit of material possessions can provide a sense of security and comfort.\\n5. Revise and edit the essay for clarity, coherence, and grammar. Ensure that transitions between paragraphs are smooth and that your arguments flow logically.', response_metadata={'token_usage': {'prompt_tokens': 1168, 'total_tokens': 2044, 'completion_tokens': 876}, 'model_name': 'accounts/fireworks/models/mixtral-8x7b-instruct', 'system_fingerprint': '', 'finish_reason': 'stop', 'logprobs': None}, id='run-9bfc9ff2-3186-43f5-8b75-498d532d8d1a-0', usage_metadata={'input_tokens': 1168, 'output_tokens': 876, 'total_tokens': 2044})]}}\n", "---\n", - "{'reflect': HumanMessage(content=\"The revised essay now provides a more in-depth analysis of the novel's themes and their relevance in the context of modern society, studies on loneliness, personal growth, and superficiality. The addition of specific examples from both the book and real-world research strengthens the argument, bolstering the claim that The Little Prince remains a timeless and relevant work in the 21st century. Overall, the essay conveys a thorough exploration of the novel's impact and significance.\")}\n", + "{'reflect': {'messages': [HumanMessage(content='Your revised essay demonstrates a clear understanding of the assignment and the source material. Here are some additional recommendations to further enhance your essay:\\n\\n1. Consider adding more nuance to your analysis of the allegory in Body Paragraph 1. You could explore how the Little Prince himself evolves throughout the story, representing not just innocence and curiosity, but also the capacity for growth and self-discovery.\\n\\n2. In Body Paragraph 2, you could delve deeper into the psychological consequences of neglecting genuine relationships. Research has shown that loneliness and social isolation can have significant impacts on mental and physical health. Incorporating these findings would strengthen your argument about the importance of the Little Prince\\'s message.\\n\\n3. For Body Paragraph 3, you could provide specific examples of how materialism and consumerism contribute to wealth inequality and environmental degradation. This would make your critique of modern society more concrete and compelling.\\n\\n4. In your conclusion, you could discuss how the Little Prince\\'s message can be applied to various aspects of modern life, such as education, politics, and personal relationships. This would demonstrate the wide-ranging relevance of the story and inspire readers to reflect on its implications for their own lives.\\n\\n5. Throughout the essay, make sure to cite secondary sources to support your analysis. This will add credibility to your arguments and demonstrate your engagement with existing scholarship on \"The Little Prince.\"\\n\\n6. Finally, proofread your essay carefully to ensure that it is free of grammatical errors and awkward phrasing. Consider asking a peer or mentor to review your work and provide feedback. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.')]}}\n", "---\n", - "{'generate': AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today\\'s society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel\\'s powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince\\'s depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince\\'s portrayal of the prince\\'s loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\\n\\nOne scene that highlights the emotional impact of loneliness is the Little Prince\\'s relationship with his rose, which illustrates the often-complex nature of human relationships. The prince\\'s devotion to the rose, despite her shortcomings, underscores how even the most frustrating relationships can bring solace to those yearning for connection. In the digital age, social media and other online platforms can be sources of isolation, rather than connection, and The Little Prince challenges readers to cherish in-person interactions and prioritize genuine human relationships over superficial online exchanges.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nPersonal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry\\'s exploration of self-awareness and personal growth is highly relevant. The Little Prince\\'s encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence.\\n\\nStudies have consistently linked emotional intelligence to mental health, providing further support for the themes present in The Little Prince. Research conducted by Schutte and colleagues (2001) found that higher emotional intelligence was positively associated with mental health and well-being, suggesting that the novel\\'s focus on personal growth and responsibility provides valuable insights for today\\'s 21st-century society. The novel challenges readers to question the adult world\\'s superficiality, pursue self-awareness, and foster emotional intelligence as a means of developing resilience in the face of modern-day challenges.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today\\'s digital age and social media-dominated society. For instance, the novel\\'s third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence, where people often focus on the accumulation of \"likes\" and \"followers.\" \\n\\nResearch suggests that Facebook, Instagram, and Twitter use may contribute to decreased well-being and increased loneliness, underscoring Saint-Exupéry\\'s prescient examination of the superficiality of modern society (Kross et al., 2013). The Little Prince encourages its readers to seek genuine connections and engage with the world around them, minimizing the allure of superficial distractions. As digital natives grapple with maintaining healthy digital personas, the novel\\'s messages about the importance of meaningful relationships and personal responsibility remain more relevant than ever.\\n\\nConclusion:\\nThe Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel\\'s exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel\\'s wisdom and the importance of its messages in our daily lives.')}\n", + "{'generate': {'messages': [AIMessage(content='Title: The Little Prince: A Topical Allegory for Modern Life\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s \"The Little Prince\" is a timeless novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children\\'s story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince,\" analyze the psychological and societal consequences of neglecting its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\\n\\nBody Paragraph 1 - The Allegory of the Little Prince:\\n\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. The Little Prince himself represents innocence, curiosity, and the importance of human connection, but he also embodies the capacity for growth and self-discovery. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. These representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\\n\\nBody Paragraph 2 - The Relevance of the Little Prince\\'s Message:\\nThe Little Prince\\'s message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction, which can have significant impacts on mental and physical health. By embracing the story\\'s wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\\n\\nBody Paragraph 3 - The Critique of Modern Society:\\n\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. Materialism and consumerism contribute to wealth inequality and environmental degradation by promoting unsustainable practices and exacerbating social and economic disparities. For instance, the overconsumption of resources leads to deforestation, climate change, and the exploitation of marginalized communities. To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\\n\\nConclusion:\\nIn conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. The Little Prince\\'s message can be applied to various aspects of modern life, such as education, politics, and personal relationships, inspiring readers to reflect on its implications for their own lives. By embracing the story\\'s wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.\\n\\nTo further enhance your essay, consider incorporating secondary sources to support your analysis, and proofread your work carefully to ensure that it is free of grammatical errors and awkward phrasing. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.', response_metadata={'token_usage': {'prompt_tokens': 2419, 'total_tokens': 3164, 'completion_tokens': 745}, 'model_name': 'accounts/fireworks/models/mixtral-8x7b-instruct', 'system_fingerprint': '', 'finish_reason': 'stop', 'logprobs': None}, id='run-eabbd349-2b3a-4bcf-a89b-716b25471846-0', usage_metadata={'input_tokens': 2419, 'output_tokens': 745, 'total_tokens': 3164})]}}\n", "---\n", - "{'reflect': HumanMessage(content=\"The revised essay expands on the themes presented in the novel and their relevance to modern society, integrating real-world research, specific examples from The Little Prince, and addressing the issues of social media and materialism in an insightful manner. The essay demonstrates a thorough understanding of the novel's impact and significance in the 21st century, offering a compelling analysis of its continued relevance.\")}\n", + "{'reflect': {'messages': [HumanMessage(content='Thank you for the feedback and recommendations. I have incorporated some of the suggestions to further enhance the essay:\\n\\nTitle: The Little Prince: A Topical Allegory for Modern Life\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s \"The Little Prince\" is a timeless novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children\\'s story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince,\" analyze the psychological and societal consequences of neglecting its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\\n\\nBody Paragraph 1 - The Allegory of the Little Prince:\\n\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. The Little Prince himself represents innocence, curiosity, and the importance of human connection, but he also embodies the capacity for growth and self-discovery. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. For instance, the king represents authority without substance, while the businessman embodies the futility of materialism. The fox, conversely, symbolizes the importance of forming genuine connections and nurturing meaningful relationships. These allegorical representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\\n\\nBody Paragraph 2 - The Relevance of the Little Prince\\'s Message:\\nThe Little Prince\\'s message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction, which can have significant impacts on mental and physical health. Research has shown that loneliness and social isolation can increase the risk of depression, anxiety, and heart disease (Holt-Lunstad, 2015). By embracing the story\\'s wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\\n\\nBody Paragraph 3 - The Critique of Modern Society:\\n\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. Materialism and consumerism contribute to wealth inequality and environmental degradation by promoting unsustainable practices and exacerbating social and economic disparities. For instance, the overconsumption of resources leads to deforestation, climate change, and the exploitation of marginalized communities (Jackson, 2017). To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\\n\\nConclusion:\\nIn conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. The Little Prince\\'s message can be applied to various aspects of modern life, such as education, politics, and personal relationships, inspiring readers to reflect on its implications for their own lives. By embracing the story\\'s wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.\\n\\nReferences:\\nHolt-Lunstad, J. (2015). The Loneliness Paradox. American Psychological Association.\\nJackson, T. (2017). Prosperity without Growth: Economics for a Finite Planet. Routledge.')]}}\n", "---\n", - "{'generate': AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to captivate readers as a classic tale that carries significant implications for contemporary society. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the superficiality of the adult world remain profoundly relevant in the 21st century. As society grapples with increasing social isolation, mental health issues, and materialism, this essay explores the novel\\'s powerful impact by discussing its themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince addresses themes of love and loneliness that still resonate strongly in today\\'s world. The novel\\'s portrayal of the prince\\'s relationships emphasizes the significance of in-person connections in a time when digital communication dominates many aspects of everyday life. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), the authors revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. The Little Prince challenges readers to prioritize genuine human relationships over superficial online exchanges.\\n\\nOne notable scene in The Little Prince portrays the emotional impact of loneliness. The little prince\\'s devotion to his rose, despite her flaws, highlights the value of even the most frustrating relationships in providing solace to those yearning for connection. The novel encourages readers to seek and maintain in-person interactions and forge emotional bonds that can help mitigate the feelings of loneliness and isolation that may arise in the modern age.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nThe Little Prince emphasizes responsibility, self-awareness, and personal growth as critical components of emotional intelligence, which remains salient in modern society. Research consistently links emotional intelligence to mental health and well-being. A 2001 study conducted by Schutte and colleagues found that higher emotional intelligence was associated with fewer symptoms of anxiety and depression, suggesting that the novel\\'s focus on personal growth and self-awareness offers valuable insights in the face of today\\'s challenges.\\n\\nIn response to the pressures of adulthood and rigid expectations, the novel underscores the importance of pursuing personal growth and responsibility, embracing self-discovery, and nurturing emotional intelligence as a means of coping with the complexities of life in contemporary society. According to Salovey and Mayer (1990), growing emotional intelligence allows individuals to understand their own emotions and those of others more deeply, which contributes to overall mental well-being.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which becomes more apparent in the digital age and social media-dominated society. The novel introduces characters like the businessman, who devotes his life to counting stars while prioritizing material possessions and wealth over genuine relationships. This behavior can be likened to the modern trend of cultivating an online presence and seeking validation through the accumulation of \"likes\" and \"followers.\"\\n\\nResearch suggests that social media use may have detrimental effects on mental health and well-being. For example, a study conducted by Kross et al. (2013) found that frequent Facebook use was associated with decreased well-being and increased loneliness, supporting The Little Prince\\'s assertion that superficiality and materialism can have damaging consequences on mental health. The novel encourages readers to engage with the world around them and seek genuine connections that transcend superficial distractions.\\n\\nConclusion:\\nThe Little Prince remains a timeless and relevant work in the 21st century. The novel\\'s exploration of topics such as personal growth, mental health, materialism, and loneliness continues to offer valuable insights for contemporary society. The novel challenges readers to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality and materialism prevalent in today\\'s world. By doing so, The Little Prince reminds us of the wisdom it possesses and the importance of its themes in our daily lives.')}\n", - "---\n", - "{'__end__': [HumanMessage(content='Generate an essay on the topicality of The Little Prince and its message in modern life'), AIMessage(content=\"Title: The Enduring Relevance of The Little Prince: A Timeless Message for Modern Life\\n\\nIntroduction:\\nAntoine de Saint-Exupéry's The Little Prince is a canonical work of literature, beloved by generations since its publication in 1943. The novella has been translated into more than 250 languages and sold over 140 million copies, making it one of the best-selling books of all time. Its enchanting story transcends cultural boundaries and continues to captivate audiences of all ages. The Little Prince's timeless message remains relevant in modern life, offering insightful commentary on themes such as love, loneliness, responsibility, and the superficiality of the adult world. In this essay, we will discuss the topicality of The Little Prince and its enduring message in today's fast-paced, digitally-connected society.\\n\\nBody Paragraph 1 - Love and Loneliness:\\nOne of the most enduring aspects of The Little Prince is its exploration of love and relationships in a world plagued by superficiality. The Little Prince's encounters with the fox, the rose, and his pilot reveal the importance of genuine connections and the pain of loss. In today's modern era, characterized by increasing social isolation, the message of The Little Prince serves as a reminder of the crucial role empathy and understanding play in fostering meaningful relationships. The consequences of isolation, depression, and loneliness continue to grow in modern life, making Saint-Exupéry's exploration of love and loneliness as vital now as it was then.\\n\\nBody Paragraph 2 - Responsibility and Self-Discovery:\\nThroughout the novella, Saint-Exupéry emphasizes the significance of taking responsibility and learning from one's experiences—core components of personal growth and self-discovery. The Little Prince's journey to various planets, each inhabited by an absurd, self-absorbed grown-up, reflects on the responsibility people have to learn from their actions and understand their impact on others. The modern world demands people to navigate complex social, professional, and personal situations daily. Thus, The Little Prince's lessons in responsibility and self-discovery are essential when addressing pressing issues like mental health, self-awareness, and communication in contemporary society.\\n\\nBody Paragraph 3 - The Superficiality of the Adult World:\\nCritics often discuss the novella's critique of the superficiality of the adult world, which remains relevant today, given society's heightened emphasis on materialism and status. The Little Prince's encounters with businessmen and geographers represent the folly of misunderstanding values and blindly pursuing worldly possessions. Today's capitalist societies frequently struggle to balance priorities, often rewarding materialistic pursuits over the development of meaningful relationships. The Little Prince serves as a profound reminder to maintain a sense of perspective, recognize the importance of intangible connections, and avoid the trappings of superficiality.\\n\\nConclusion:\\nUltimately, The Little Prince continues to top bestseller lists because its themes of love, loneliness, responsibility, and the superficiality of the adult world resonate with people across time and culture. The novella's resilient popularity and topicality reflect its relevance in tackling contemporary societal issues, making it a timeless masterpiece that transcends generations. As we navigate the complexities of modern life, The Little Prince's message is one we should keep close to our hearts: we must never lose sight of the simple, yet profound, lessons the story teaches us about cherishing meaningful connections, embracing personal growth, and resisting the shallow temptations of adult life.\\n\\nRevised Essay:\\n\\nTitle: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry's The Little Prince is an enduring classic that has touched the hearts of millions since its publication in 1943. The novella has been translated into more than 300 languages, and over 200 million copies have been sold, making it one of the bestselling books ever written. The Little Prince's timeless message about love, friendship, responsibility, and the adult world remains incredibly relevant in the 21st century. This essay will analyze the topicality of The Little Prince and explore the many ways its universal themes connect with modern life.\\n\\nBody Paragraph 1 - Love, Loss, and Friendship:\\nThe Little Prince teaches powerful lessons about love, friendship, and loss that continue to resonate with readers today. The novella's exploration of grief and heartache is as poignant today as it was when it was first published. The tales of the Little Prince's encounters with the fox, the rose, and his pilot highlight the transcendent power of meaningful connections and the pain of losing those we care about. In a digital age where fleeting online interactions can dominate our time, The Little Prince serves as a reminder to cherish genuine friendships and treasure the connections we make with others.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nThroughout the story, Saint-Exupéry highlights the significance of taking responsibility and engaging in self-discovery. The Little Prince's journey to various planets, each inhabited by a reductive grown-up, teaches the reader about the impact actions can have on others. In a world where emotional intelligence and empathy are increasingly vital due to ever-evolving social, professional, and personal obligations, The Little Prince's lessons on responsibility and personal growth remain crucial. Mental health, self-awareness, and communication are critical issues in modern society, making the exploration of these themes as essential in today's world as when the book was first published.\\n\\nBody Paragraph 3 - Rejecting the Superficiality of the Adult World:\\nThe Little Prince's critique of the superficiality of the adult world remains strikingly relevant in modern society. The novel's portrait of grown-ups consumed by materialism, social status, and vanity rings true today, more than ever, as individuals and societies race to acquire wealth, status, and possessions. The Little Prince serves as a poignant reminder to resist the superficiality of the adult world and maintain a balanced perspective, cherishing meaningful connections and eschewing the trappings of materialism.\\n\\nConclusion:\\nThe Little Prince's universal themes continue to captivate and inspire readers because the lessons it teaches about love, friendship, responsibility, and the adult world are still incredibly pertinent today. The novel's topicality and enduring popularity validate its relevance in addressing contemporary societal issues like mental health, self-awareness, communication, and materialism. As we maneuver the challenges of the 21st century, The Little Prince's enduring wisdom—to cherish deep relationships, value personal growth, and reject the superficiality of adult life—continues to resonate and encourage readers to reassess their priorities and find meaning in connection and experience.\"), HumanMessage(content=\"Introduction:\\nThe essay provides a solid introduction to the topic, clearly stating the book's significance and its continued relevance in modern life. I would suggest providing more specific connections to the present day to emphasize the enduring relevance of The Little Prince. For instance, you could mention current events or issues that are directly related to the themes discussed in Saint-Exupéry's work (e.g., studies on loneliness and mental health in the digital age).\\n\\nBody Paragraph 1 - Love and Loneliness:\\nThe paragraph effectively explains how the themes of love and loneliness resonate with the modern era. However, I would like to see more concrete examples from the book to strengthen the analysis. Consider providing a specific interaction or quote from The Little Prince to more directly tie it to the concepts of isolation, depression, and loneliness in today's world.\\n\\nBody Paragraph 2 - Responsibility and Self-Discovery:\\nThis paragraph provides a good analysis of how Saint-Exupéry emphasizes responsibility and self-discovery. However, it could benefit from a stronger connection to contemporary society. It would be helpful to provide examples from real-life situations or psychological studies that demonstrate the importance of mental health, self-awareness, and communication in today's world.\\n\\nBody Paragraph 3 - The Superficiality of the Adult World:\\nThe criticism of materialism and status in modern society is well-presented in this paragraph. However, you could strengthen the analysis by offering specific examples of the adult world's superficiality in the context of the 21st century, such as a focus on social media and online presence. Moreover, consider further elaborating on the contrast between the materialistic world and The Little Prince's emphasis on meaningful relationships.\\n\\nConclusion:\\nThe conclusion effectively summarizes the importance of the themes addressed in the novel. Nonetheless, it could benefit from a stronger final statement that reiterates the significance of the stories and lessons from The Little Prince in the modern context. Consider restating the main ideas in a way that reinforces the parallels between the book and contemporary life.\\n\\nOverall, I would encourage you to strengthen the connections between the novel's themes and modern society by providing more specific examples and relevant real-world issues. Furthermore, I recommend a word count of around 1,200-1,500 words for your essay to provide enough space to thoroughly analyze and discuss the topics presented. By offering a more in-depth analysis, your argument would become more persuasive and the relevance of the novel even more apparent.\"), AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today\\'s society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel\\'s powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince\\'s depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince\\'s portrayal of the prince\\'s loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nPersonal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry\\'s exploration of self-awareness and personal growth is highly relevant. The Little Prince\\'s encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence. Research connecting emotional intelligence to mental health underscores the significance of the ideas presented in The Little Prince, demonstrating that higher emotional intelligence is positively associated with mental health and well-being (Schutte et al., 2001). This research supports the notion that the personal growth explored in The Little Prince remains a vital part of addressing mental health issues.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today\\'s digital age and social media-dominated society. For instance, the novel\\'s third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence and an obsession with acquiring digital \"followers\" and \"likes.\" By highlighting the emptiness of the materialistic pursuits, The Little Prince shows readers the importance of genuine human connections and rejecting superficial distractions (Soucy & Vedel, 2018). These themes are particularly relevant today, as younger generations struggle to find balance between their online and offline lives, frequently confronted with issues related to superficiality, self-promotion, and digital personas.\\n\\nConclusion:\\nThe Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel\\'s exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel\\'s wisdom and the importance of its messages in our daily lives.'), HumanMessage(content=\"The revised essay now provides a more in-depth analysis of the novel's themes and their relevance in the context of modern society, studies on loneliness, personal growth, and superficiality. The addition of specific examples from both the book and real-world research strengthens the argument, bolstering the claim that The Little Prince remains a timeless and relevant work in the 21st century. Overall, the essay conveys a thorough exploration of the novel's impact and significance.\"), AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today\\'s society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel\\'s powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince\\'s depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince\\'s portrayal of the prince\\'s loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\\n\\nOne scene that highlights the emotional impact of loneliness is the Little Prince\\'s relationship with his rose, which illustrates the often-complex nature of human relationships. The prince\\'s devotion to the rose, despite her shortcomings, underscores how even the most frustrating relationships can bring solace to those yearning for connection. In the digital age, social media and other online platforms can be sources of isolation, rather than connection, and The Little Prince challenges readers to cherish in-person interactions and prioritize genuine human relationships over superficial online exchanges.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nPersonal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry\\'s exploration of self-awareness and personal growth is highly relevant. The Little Prince\\'s encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence.\\n\\nStudies have consistently linked emotional intelligence to mental health, providing further support for the themes present in The Little Prince. Research conducted by Schutte and colleagues (2001) found that higher emotional intelligence was positively associated with mental health and well-being, suggesting that the novel\\'s focus on personal growth and responsibility provides valuable insights for today\\'s 21st-century society. The novel challenges readers to question the adult world\\'s superficiality, pursue self-awareness, and foster emotional intelligence as a means of developing resilience in the face of modern-day challenges.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today\\'s digital age and social media-dominated society. For instance, the novel\\'s third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence, where people often focus on the accumulation of \"likes\" and \"followers.\" \\n\\nResearch suggests that Facebook, Instagram, and Twitter use may contribute to decreased well-being and increased loneliness, underscoring Saint-Exupéry\\'s prescient examination of the superficiality of modern society (Kross et al., 2013). The Little Prince encourages its readers to seek genuine connections and engage with the world around them, minimizing the allure of superficial distractions. As digital natives grapple with maintaining healthy digital personas, the novel\\'s messages about the importance of meaningful relationships and personal responsibility remain more relevant than ever.\\n\\nConclusion:\\nThe Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel\\'s exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel\\'s wisdom and the importance of its messages in our daily lives.'), HumanMessage(content=\"The revised essay expands on the themes presented in the novel and their relevance to modern society, integrating real-world research, specific examples from The Little Prince, and addressing the issues of social media and materialism in an insightful manner. The essay demonstrates a thorough understanding of the novel's impact and significance in the 21st century, offering a compelling analysis of its continued relevance.\"), AIMessage(content='Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\\n\\nIntroduction:\\nAntoine de Saint-Exupéry\\'s The Little Prince continues to captivate readers as a classic tale that carries significant implications for contemporary society. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the superficiality of the adult world remain profoundly relevant in the 21st century. As society grapples with increasing social isolation, mental health issues, and materialism, this essay explores the novel\\'s powerful impact by discussing its themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\\n\\nBody Paragraph 1 - Love, Loneliness, and Isolation:\\nThe Little Prince addresses themes of love and loneliness that still resonate strongly in today\\'s world. The novel\\'s portrayal of the prince\\'s relationships emphasizes the significance of in-person connections in a time when digital communication dominates many aspects of everyday life. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), the authors revealed an alarming decline in the number of confidants in individuals\\' lives, indicating growing isolation. The Little Prince challenges readers to prioritize genuine human relationships over superficial online exchanges.\\n\\nOne notable scene in The Little Prince portrays the emotional impact of loneliness. The little prince\\'s devotion to his rose, despite her flaws, highlights the value of even the most frustrating relationships in providing solace to those yearning for connection. The novel encourages readers to seek and maintain in-person interactions and forge emotional bonds that can help mitigate the feelings of loneliness and isolation that may arise in the modern age.\\n\\nBody Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\\nThe Little Prince emphasizes responsibility, self-awareness, and personal growth as critical components of emotional intelligence, which remains salient in modern society. Research consistently links emotional intelligence to mental health and well-being. A 2001 study conducted by Schutte and colleagues found that higher emotional intelligence was associated with fewer symptoms of anxiety and depression, suggesting that the novel\\'s focus on personal growth and self-awareness offers valuable insights in the face of today\\'s challenges.\\n\\nIn response to the pressures of adulthood and rigid expectations, the novel underscores the importance of pursuing personal growth and responsibility, embracing self-discovery, and nurturing emotional intelligence as a means of coping with the complexities of life in contemporary society. According to Salovey and Mayer (1990), growing emotional intelligence allows individuals to understand their own emotions and those of others more deeply, which contributes to overall mental well-being.\\n\\nBody Paragraph 3 - Materialism, Superficiality, and Social Media:\\nThe Little Prince critiques the materialistic and superficial nature of the adult world, which becomes more apparent in the digital age and social media-dominated society. The novel introduces characters like the businessman, who devotes his life to counting stars while prioritizing material possessions and wealth over genuine relationships. This behavior can be likened to the modern trend of cultivating an online presence and seeking validation through the accumulation of \"likes\" and \"followers.\"\\n\\nResearch suggests that social media use may have detrimental effects on mental health and well-being. For example, a study conducted by Kross et al. (2013) found that frequent Facebook use was associated with decreased well-being and increased loneliness, supporting The Little Prince\\'s assertion that superficiality and materialism can have damaging consequences on mental health. The novel encourages readers to engage with the world around them and seek genuine connections that transcend superficial distractions.\\n\\nConclusion:\\nThe Little Prince remains a timeless and relevant work in the 21st century. The novel\\'s exploration of topics such as personal growth, mental health, materialism, and loneliness continues to offer valuable insights for contemporary society. The novel challenges readers to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality and materialism prevalent in today\\'s world. By doing so, The Little Prince reminds us of the wisdom it possesses and the importance of its themes in our daily lives.')]}\n", + "{'generate': {'messages': [AIMessage(content='Your revised essay demonstrates a clear understanding of the assignment and the source material, and you have effectively incorporated the suggestions provided. The addition of research findings and specific examples has strengthened your argument and added credibility to your analysis. Your essay now provides a more nuanced exploration of the allegory, the relevance of the Little Prince\\'s message, and the critique of modern society.\\n\\nHere are some final recommendations to further enhance your essay:\\n\\n1. Ensure that your essay adheres to the required citation style (e.g., MLA, APA, or Chicago) and that all in-text citations and references are formatted correctly.\\n2. Double-check your essay for any grammatical errors, awkward phrasing, or unclear sentences. A well-written essay is not only easier to read but also more persuasive and engaging.\\n3. Consider adding a brief introduction to each body paragraph to provide context and guide the reader through your analysis. This will help ensure that your essay flows logically and that your arguments are easy to follow.\\n4. As a final step, ask a peer or mentor to review your work and provide feedback. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.\\n\\nOverall, your essay provides a thoughtful and engaging exploration of \"The Little Prince\" and its relevance in modern life. By incorporating the recommendations provided, you can further enhance your analysis and create a truly exceptional piece of writing.', response_metadata={'token_usage': {'prompt_tokens': 4034, 'total_tokens': 4354, 'completion_tokens': 320}, 'model_name': 'accounts/fireworks/models/mixtral-8x7b-instruct', 'system_fingerprint': '', 'finish_reason': 'stop', 'logprobs': None}, id='run-9c805bb5-01f4-4461-acf8-509f7440d31d-0', usage_metadata={'input_tokens': 4034, 'output_tokens': 320, 'total_tokens': 4354})]}}\n", "---\n" ] } @@ -340,14 +408,24 @@ " content=\"Generate an essay on the topicality of The Little Prince and its message in modern life\"\n", " )\n", " ],\n", - "}):\n", + "}, config):\n", " print(event)\n", " print(\"---\")" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, + "id": "ced83251-8edc-483d-a03f-5bd884ea8d28", + "metadata": {}, + "outputs": [], + "source": [ + "state = graph.get_state(config)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, "id": "394bf0df-fc28-4104-a278-a56c9cb8b10c", "metadata": {}, "outputs": [ @@ -361,161 +439,170 @@ "\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Title: The Enduring Relevance of The Little Prince: A Timeless Message for Modern Life\n", + "Title: The Little Prince: A Topical Allegory for Modern Life\n", "\n", "Introduction:\n", - "Antoine de Saint-Exupéry's The Little Prince is a canonical work of literature, beloved by generations since its publication in 1943. The novella has been translated into more than 250 languages and sold over 140 million copies, making it one of the best-selling books of all time. Its enchanting story transcends cultural boundaries and continues to captivate audiences of all ages. The Little Prince's timeless message remains relevant in modern life, offering insightful commentary on themes such as love, loneliness, responsibility, and the superficiality of the adult world. In this essay, we will discuss the topicality of The Little Prince and its enduring message in today's fast-paced, digitally-connected society.\n", + "Antoine de Saint-Exupéry's \"The Little Prince\" is a classic novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children's story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince\" and discuss how its message can be applied to the complexities of the modern world.\n", "\n", - "Body Paragraph 1 - Love and Loneliness:\n", - "One of the most enduring aspects of The Little Prince is its exploration of love and relationships in a world plagued by superficiality. The Little Prince's encounters with the fox, the rose, and his pilot reveal the importance of genuine connections and the pain of loss. In today's modern era, characterized by increasing social isolation, the message of The Little Prince serves as a reminder of the crucial role empathy and understanding play in fostering meaningful relationships. The consequences of isolation, depression, and loneliness continue to grow in modern life, making Saint-Exupéry's exploration of love and loneliness as vital now as it was then.\n", + "Body Paragraph 1 - The Allegory of the Little Prince:\n", + "\"The Little Prince\" is an allegorical tale that explores various aspects of the human condition through its whimsical characters and situations. The Little Prince himself represents innocence, curiosity, and the importance of human connection. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. These representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\n", "\n", - "Body Paragraph 2 - Responsibility and Self-Discovery:\n", - "Throughout the novella, Saint-Exupéry emphasizes the significance of taking responsibility and learning from one's experiences—core components of personal growth and self-discovery. The Little Prince's journey to various planets, each inhabited by an absurd, self-absorbed grown-up, reflects on the responsibility people have to learn from their actions and understand their impact on others. The modern world demands people to navigate complex social, professional, and personal situations daily. Thus, The Little Prince's lessons in responsibility and self-discovery are essential when addressing pressing issues like mental health, self-awareness, and communication in contemporary society.\n", + "Body Paragraph 2 - The Relevance of the Little Prince's Message:\n", + "The Little Prince's message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. The Little Prince encourages readers to cherish and nurture genuine relationships, reminding us that true happiness and fulfillment come from understanding and empathizing with others.\n", "\n", - "Body Paragraph 3 - The Superficiality of the Adult World:\n", - "Critics often discuss the novella's critique of the superficiality of the adult world, which remains relevant today, given society's heightened emphasis on materialism and status. The Little Prince's encounters with businessmen and geographers represent the folly of misunderstanding values and blindly pursuing worldly possessions. Today's capitalist societies frequently struggle to balance priorities, often rewarding materialistic pursuits over the development of meaningful relationships. The Little Prince serves as a profound reminder to maintain a sense of perspective, recognize the importance of intangible connections, and avoid the trappings of superficiality.\n", + "Body Paragraph 3 - The Critique of Modern Society:\n", + "\"The Little Prince\" also offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. These themes resonate strongly in today's world, where wealth inequality and environmental degradation are pressing issues. The story serves as a reminder that the pursuit of material possessions and status often comes at the expense of our own happiness and the well-being of our planet.\n", "\n", "Conclusion:\n", - "Ultimately, The Little Prince continues to top bestseller lists because its themes of love, loneliness, responsibility, and the superficiality of the adult world resonate with people across time and culture. The novella's resilient popularity and topicality reflect its relevance in tackling contemporary societal issues, making it a timeless masterpiece that transcends generations. As we navigate the complexities of modern life, The Little Prince's message is one we should keep close to our hearts: we must never lose sight of the simple, yet profound, lessons the story teaches us about cherishing meaningful connections, embracing personal growth, and resisting the shallow temptations of adult life.\n", - "\n", - "Revised Essay:\n", - "\n", - "Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\n", - "\n", - "Introduction:\n", - "Antoine de Saint-Exupéry's The Little Prince is an enduring classic that has touched the hearts of millions since its publication in 1943. The novella has been translated into more than 300 languages, and over 200 million copies have been sold, making it one of the bestselling books ever written. The Little Prince's timeless message about love, friendship, responsibility, and the adult world remains incredibly relevant in the 21st century. This essay will analyze the topicality of The Little Prince and explore the many ways its universal themes connect with modern life.\n", - "\n", - "Body Paragraph 1 - Love, Loss, and Friendship:\n", - "The Little Prince teaches powerful lessons about love, friendship, and loss that continue to resonate with readers today. The novella's exploration of grief and heartache is as poignant today as it was when it was first published. The tales of the Little Prince's encounters with the fox, the rose, and his pilot highlight the transcendent power of meaningful connections and the pain of losing those we care about. In a digital age where fleeting online interactions can dominate our time, The Little Prince serves as a reminder to cherish genuine friendships and treasure the connections we make with others.\n", - "\n", - "Body Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\n", - "Throughout the story, Saint-Exupéry highlights the significance of taking responsibility and engaging in self-discovery. The Little Prince's journey to various planets, each inhabited by a reductive grown-up, teaches the reader about the impact actions can have on others. In a world where emotional intelligence and empathy are increasingly vital due to ever-evolving social, professional, and personal obligations, The Little Prince's lessons on responsibility and personal growth remain crucial. Mental health, self-awareness, and communication are critical issues in modern society, making the exploration of these themes as essential in today's world as when the book was first published.\n", - "\n", - "Body Paragraph 3 - Rejecting the Superficiality of the Adult World:\n", - "The Little Prince's critique of the superficiality of the adult world remains strikingly relevant in modern society. The novel's portrait of grown-ups consumed by materialism, social status, and vanity rings true today, more than ever, as individuals and societies race to acquire wealth, status, and possessions. The Little Prince serves as a poignant reminder to resist the superficiality of the adult world and maintain a balanced perspective, cherishing meaningful connections and eschewing the trappings of materialism.\n", - "\n", - "Conclusion:\n", - "The Little Prince's universal themes continue to captivate and inspire readers because the lessons it teaches about love, friendship, responsibility, and the adult world are still incredibly pertinent today. The novel's topicality and enduring popularity validate its relevance in addressing contemporary societal issues like mental health, self-awareness, communication, and materialism. As we maneuver the challenges of the 21st century, The Little Prince's enduring wisdom—to cherish deep relationships, value personal growth, and reject the superficiality of adult life—continues to resonate and encourage readers to reassess their priorities and find meaning in connection and experience.\n", + "In conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. By embracing the story's wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.\n", "\n", "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", + "Essay Critique and Recommendations:\n", + "\n", + "Title: The Little Prince: A Topical Allegory for Modern Life\n", + "\n", "Introduction:\n", - "The essay provides a solid introduction to the topic, clearly stating the book's significance and its continued relevance in modern life. I would suggest providing more specific connections to the present day to emphasize the enduring relevance of The Little Prince. For instance, you could mention current events or issues that are directly related to the themes discussed in Saint-Exupéry's work (e.g., studies on loneliness and mental health in the digital age).\n", + "The introduction effectively sets the stage for the essay by providing background information on \"The Little Prince\" and its relevance in modern life. However, consider adding a hook to engage the reader's attention and create a stronger first impression.\n", "\n", - "Body Paragraph 1 - Love and Loneliness:\n", - "The paragraph effectively explains how the themes of love and loneliness resonate with the modern era. However, I would like to see more concrete examples from the book to strengthen the analysis. Consider providing a specific interaction or quote from The Little Prince to more directly tie it to the concepts of isolation, depression, and loneliness in today's world.\n", + "Body Paragraph 1 - The Allegory of the Little Prince:\n", + "This paragraph provides a clear explanation of the allegorical nature of \"The Little Prince.\" To enhance this section, consider offering specific examples from the text to illustrate how the characters and situations symbolize various aspects of adult life. This will strengthen your analysis and make it more engaging for the reader.\n", "\n", - "Body Paragraph 2 - Responsibility and Self-Discovery:\n", - "This paragraph provides a good analysis of how Saint-Exupéry emphasizes responsibility and self-discovery. However, it could benefit from a stronger connection to contemporary society. It would be helpful to provide examples from real-life situations or psychological studies that demonstrate the importance of mental health, self-awareness, and communication in today's world.\n", + "Body Paragraph 2 - The Relevance of the Little Prince's Message:\n", + "The relevance of the Little Prince's message is well-articulated in this paragraph. To further strengthen your argument, consider discussing the consequences of ignoring this message in the context of modern society. This will help emphasize the importance of the Little Prince's wisdom and its relevance to contemporary issues.\n", "\n", - "Body Paragraph 3 - The Superficiality of the Adult World:\n", - "The criticism of materialism and status in modern society is well-presented in this paragraph. However, you could strengthen the analysis by offering specific examples of the adult world's superficiality in the context of the 21st century, such as a focus on social media and online presence. Moreover, consider further elaborating on the contrast between the materialistic world and The Little Prince's emphasis on meaningful relationships.\n", + "Body Paragraph 3 - The Critique of Modern Society:\n", + "This paragraph effectively highlights the story's critique of modern society. To deepen your analysis, explore how the themes of materialism, consumerism, and the pursuit of power interconnect and contribute to the challenges faced by modern society. Additionally, consider discussing potential solutions or actions inspired by the Little Prince's message that could help address these issues.\n", "\n", "Conclusion:\n", - "The conclusion effectively summarizes the importance of the themes addressed in the novel. Nonetheless, it could benefit from a stronger final statement that reiterates the significance of the stories and lessons from The Little Prince in the modern context. Consider restating the main ideas in a way that reinforces the parallels between the book and contemporary life.\n", + "The conclusion effectively summarizes the main points of the essay and emphasizes the relevance of \"The Little Prince\" in modern life. To further enhance this section, consider incorporating a thought-provoking question or statement that encourages readers to reflect on the story's message and its implications for their own lives.\n", "\n", - "Overall, I would encourage you to strengthen the connections between the novel's themes and modern society by providing more specific examples and relevant real-world issues. Furthermore, I recommend a word count of around 1,200-1,500 words for your essay to provide enough space to thoroughly analyze and discuss the topics presented. By offering a more in-depth analysis, your argument would become more persuasive and the relevance of the novel even more apparent.\n", + "Recommendations:\n", + "1. Expand the essay to approximately 1,200-1,500 words to allow for a more in-depth analysis.\n", + "2. Incorporate specific examples and quotes from \"The Little Prince\" to support your arguments and engage the reader.\n", + "3. Ensure that each body paragraph contains a clear thesis statement, supporting evidence, and analysis.\n", + "4. Consider discussing counterarguments or potential criticisms of the Little Prince's message to add depth and complexity to your essay.\n", + "5. Revise and edit the essay for clarity, coherence, and grammar.\n", "\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\n", + "Title: The Little Prince: A Topical Allegory for Modern Life\n", "\n", "Introduction:\n", - "Antoine de Saint-Exupéry's The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today's society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel's powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\n", + "In Antoine de Saint-Exupéry's classic novella \"The Little Prince,\" a young boy embarks on a journey through the universe, meeting various characters that symbolize different aspects of adult life. This timeless tale, published in 1943, remains incredibly relevant in today's modern world. Its allegorical nature, thought-provoking message, and critique of modern society offer invaluable insights for readers of all ages. This essay will explore the allegory of \"The Little Prince,\" analyze the relevance of its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\n", "\n", - "Body Paragraph 1 - Love, Loneliness, and Isolation:\n", - "The Little Prince's depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince's portrayal of the prince's loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\n", + "Body Paragraph 1 - The Allegory of the Little Prince:\n", + "\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. For instance, the king represents authority without substance, while the businessman embodies the futility of materialism. The fox, conversely, symbolizes the importance of forming genuine connections and nurturing meaningful relationships. These allegorical representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\n", "\n", - "Body Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\n", - "Personal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry's exploration of self-awareness and personal growth is highly relevant. The Little Prince's encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence. Research connecting emotional intelligence to mental health underscores the significance of the ideas presented in The Little Prince, demonstrating that higher emotional intelligence is positively associated with mental health and well-being (Schutte et al., 2001). This research supports the notion that the personal growth explored in The Little Prince remains a vital part of addressing mental health issues.\n", + "Body Paragraph 2 - The Relevance of the Little Prince's Message:\n", + "The Little Prince's message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction. By embracing the story's wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\n", "\n", - "Body Paragraph 3 - Materialism, Superficiality, and Social Media:\n", - "The Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today's digital age and social media-dominated society. For instance, the novel's third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence and an obsession with acquiring digital \"followers\" and \"likes.\" By highlighting the emptiness of the materialistic pursuits, The Little Prince shows readers the importance of genuine human connections and rejecting superficial distractions (Soucy & Vedel, 2018). These themes are particularly relevant today, as younger generations struggle to find balance between their online and offline lives, frequently confronted with issues related to superficiality, self-promotion, and digital personas.\n", + "Body Paragraph 3 - The Critique of Modern Society:\n", + "\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. These themes resonate strongly in today's world, where wealth inequality and environmental degradation are pressing issues. The story serves as a reminder that the pursuit of material possessions and status often comes at the expense of our own happiness and the well-being of our planet. To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\n", "\n", "Conclusion:\n", - "The Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel's exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel's wisdom and the importance of its messages in our daily lives.\n", + "In conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. By embracing the story's wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society. As the Little Prince so eloquently states, \"What is essential is invisible to the eye,\" reminding us that true happiness and fulfillment come from understanding and empathizing with others.\n", + "\n", + "Expanded Essay Recommendations:\n", + "\n", + "1. Expand the essay to approximately 1,200-1,500 words to allow for a more in-depth analysis.\n", + "2. Incorporate specific examples and quotes from \"The Little Prince\" to support your arguments and engage the reader. For instance, use quotes like, \"You become responsible, forever, for what you have tamed,\" to emphasize the importance of forming genuine connections.\n", + "3. Ensure that each body paragraph contains a clear thesis statement, supporting evidence, and analysis.\n", + "4. Consider discussing counterarguments or potential criticisms of the Little Prince's message to add depth and complexity to your essay. For example, explore the idea that the pursuit of material possessions can provide a sense of security and comfort.\n", + "5. Revise and edit the essay for clarity, coherence, and grammar. Ensure that transitions between paragraphs are smooth and that your arguments flow logically.\n", "\n", "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", - "The revised essay now provides a more in-depth analysis of the novel's themes and their relevance in the context of modern society, studies on loneliness, personal growth, and superficiality. The addition of specific examples from both the book and real-world research strengthens the argument, bolstering the claim that The Little Prince remains a timeless and relevant work in the 21st century. Overall, the essay conveys a thorough exploration of the novel's impact and significance.\n", + "Your revised essay demonstrates a clear understanding of the assignment and the source material. Here are some additional recommendations to further enhance your essay:\n", + "\n", + "1. Consider adding more nuance to your analysis of the allegory in Body Paragraph 1. You could explore how the Little Prince himself evolves throughout the story, representing not just innocence and curiosity, but also the capacity for growth and self-discovery.\n", + "\n", + "2. In Body Paragraph 2, you could delve deeper into the psychological consequences of neglecting genuine relationships. Research has shown that loneliness and social isolation can have significant impacts on mental and physical health. Incorporating these findings would strengthen your argument about the importance of the Little Prince's message.\n", + "\n", + "3. For Body Paragraph 3, you could provide specific examples of how materialism and consumerism contribute to wealth inequality and environmental degradation. This would make your critique of modern society more concrete and compelling.\n", + "\n", + "4. In your conclusion, you could discuss how the Little Prince's message can be applied to various aspects of modern life, such as education, politics, and personal relationships. This would demonstrate the wide-ranging relevance of the story and inspire readers to reflect on its implications for their own lives.\n", + "\n", + "5. Throughout the essay, make sure to cite secondary sources to support your analysis. This will add credibility to your arguments and demonstrate your engagement with existing scholarship on \"The Little Prince.\"\n", + "\n", + "6. Finally, proofread your essay carefully to ensure that it is free of grammatical errors and awkward phrasing. Consider asking a peer or mentor to review your work and provide feedback. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.\n", "\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\n", + "Title: The Little Prince: A Topical Allegory for Modern Life\n", "\n", "Introduction:\n", - "Antoine de Saint-Exupéry's The Little Prince continues to hold significance in modern life, touching the hearts of millions since its publication in 1943. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the adult world resonate profoundly today (Soucy & Vedel, 2018). Today's society faces a myriad of challenges, including increasing social isolation, mental health issues, and materialism. This essay will explore the novel's powerful impact by offering concrete examples of its relevance in modern life and discussing the themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\n", + "Antoine de Saint-Exupéry's \"The Little Prince\" is a timeless novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children's story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince,\" analyze the psychological and societal consequences of neglecting its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\n", "\n", - "Body Paragraph 1 - Love, Loneliness, and Isolation:\n", - "The Little Prince's depiction of love and loneliness in various forms—between the prince and his rose, the fox, and the pilot—provides powerful insights into addressing isolation in the 21st century. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), they revealed an alarming decline in the number of confidants in individuals' lives, indicating growing isolation. Specifically, over the past two decades, the percentage of people who claim to have no one they can discuss important issues with has doubled (McPherson, Smith-Lovin, & Brashears, 2006). The Little Prince's portrayal of the prince's loneliness and his encounters with a variety of inhabitants emphasizes the importance of genuine companionship, transcending cultural barriers.\n", + "Body Paragraph 1 - The Allegory of the Little Prince:\n", + "\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. The Little Prince himself represents innocence, curiosity, and the importance of human connection, but he also embodies the capacity for growth and self-discovery. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. These representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\n", "\n", - "One scene that highlights the emotional impact of loneliness is the Little Prince's relationship with his rose, which illustrates the often-complex nature of human relationships. The prince's devotion to the rose, despite her shortcomings, underscores how even the most frustrating relationships can bring solace to those yearning for connection. In the digital age, social media and other online platforms can be sources of isolation, rather than connection, and The Little Prince challenges readers to cherish in-person interactions and prioritize genuine human relationships over superficial online exchanges.\n", + "Body Paragraph 2 - The Relevance of the Little Prince's Message:\n", + "The Little Prince's message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction, which can have significant impacts on mental and physical health. By embracing the story's wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\n", "\n", - "Body Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\n", - "Personal growth, responsibility, and self-awareness are vital themes in The Little Prince, which remain crucial for navigating the challenges of the 21st century. With increasing emphasis on mental health and well-being worldwide, Saint-Exupéry's exploration of self-awareness and personal growth is highly relevant. The Little Prince's encounters with grown-ups on various planets reveal the trappings of vanity, authority, and materialism (Soucy & Vedel, 2018). In response to the pressures of adulthood and rigid expectations, the novel advocates for personal growth and responsibility as essential ingredients for emotional intelligence.\n", - "\n", - "Studies have consistently linked emotional intelligence to mental health, providing further support for the themes present in The Little Prince. Research conducted by Schutte and colleagues (2001) found that higher emotional intelligence was positively associated with mental health and well-being, suggesting that the novel's focus on personal growth and responsibility provides valuable insights for today's 21st-century society. The novel challenges readers to question the adult world's superficiality, pursue self-awareness, and foster emotional intelligence as a means of developing resilience in the face of modern-day challenges.\n", - "\n", - "Body Paragraph 3 - Materialism, Superficiality, and Social Media:\n", - "The Little Prince critiques the materialistic and superficial nature of the adult world, which is acutely visible in today's digital age and social media-dominated society. For instance, the novel's third chapter introduces the businessman, who spends his life counting stars, believing that \"owning\" them brings him both fame and fortune. This behavior can be likened to the modern obsession with online presence, where people often focus on the accumulation of \"likes\" and \"followers.\" \n", - "\n", - "Research suggests that Facebook, Instagram, and Twitter use may contribute to decreased well-being and increased loneliness, underscoring Saint-Exupéry's prescient examination of the superficiality of modern society (Kross et al., 2013). The Little Prince encourages its readers to seek genuine connections and engage with the world around them, minimizing the allure of superficial distractions. As digital natives grapple with maintaining healthy digital personas, the novel's messages about the importance of meaningful relationships and personal responsibility remain more relevant than ever.\n", + "Body Paragraph 3 - The Critique of Modern Society:\n", + "\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. Materialism and consumerism contribute to wealth inequality and environmental degradation by promoting unsustainable practices and exacerbating social and economic disparities. For instance, the overconsumption of resources leads to deforestation, climate change, and the exploitation of marginalized communities. To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\n", "\n", "Conclusion:\n", - "The Little Prince is an enduring classic that offers timeless lessons on love, friendship, responsibility, and the superficiality of the adult world, which remain highly relevant today. In the context of the digital age and its myriad challenges, the novel's exploration of personal growth, mental health, materialism, and loneliness provides critical insights for contemporary society. The Little Prince reminds us to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality of the adult world. By doing so, we can preserve the essence of human connection and continue to find relevance in the novel's wisdom and the importance of its messages in our daily lives.\n", + "In conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. The Little Prince's message can be applied to various aspects of modern life, such as education, politics, and personal relationships, inspiring readers to reflect on its implications for their own lives. By embracing the story's wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.\n", + "\n", + "To further enhance your essay, consider incorporating secondary sources to support your analysis, and proofread your work carefully to ensure that it is free of grammatical errors and awkward phrasing. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.\n", "\n", "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", - "The revised essay expands on the themes presented in the novel and their relevance to modern society, integrating real-world research, specific examples from The Little Prince, and addressing the issues of social media and materialism in an insightful manner. The essay demonstrates a thorough understanding of the novel's impact and significance in the 21st century, offering a compelling analysis of its continued relevance.\n", + "Thank you for the feedback and recommendations. I have incorporated some of the suggestions to further enhance the essay:\n", + "\n", + "Title: The Little Prince: A Topical Allegory for Modern Life\n", + "\n", + "Introduction:\n", + "Antoine de Saint-Exupéry's \"The Little Prince\" is a timeless novella that has captured the hearts of millions since its publication in 1943. While it might be easy to dismiss this work as a children's story, its profound themes and timeless message make it a relevant and topical piece in modern life. This essay will explore the allegorical nature of \"The Little Prince,\" analyze the psychological and societal consequences of neglecting its message, and discuss its critique of modern society, demonstrating its topicality in contemporary life.\n", + "\n", + "Body Paragraph 1 - The Allegory of the Little Prince:\n", + "\"The Little Prince\" is an allegorical tale that uses whimsical characters and situations to explore various aspects of the human condition. The Little Prince himself represents innocence, curiosity, and the importance of human connection, but he also embodies the capacity for growth and self-discovery. As the story unfolds, readers encounter different characters that symbolize various aspects of adult life, such as vanity, materialism, and authority. For instance, the king represents authority without substance, while the businessman embodies the futility of materialism. The fox, conversely, symbolizes the importance of forming genuine connections and nurturing meaningful relationships. These allegorical representations allow the story to transcend age and culture, making it relatable to a wide range of readers, even in the modern context.\n", + "\n", + "Body Paragraph 2 - The Relevance of the Little Prince's Message:\n", + "The Little Prince's message is centered around the importance of looking beyond superficial appearances and forming meaningful connections with others. In a world increasingly dominated by technology and social media, where surface-level interactions are commonplace, this message is more relevant than ever. Neglecting this message can lead to feelings of isolation, loneliness, and dissatisfaction, which can have significant impacts on mental and physical health. Research has shown that loneliness and social isolation can increase the risk of depression, anxiety, and heart disease (Holt-Lunstad, 2015). By embracing the story's wisdom, we can prioritize genuine relationships, fostering a more compassionate and interconnected society.\n", + "\n", + "Body Paragraph 3 - The Critique of Modern Society:\n", + "\"The Little Prince\" offers a critique of modern society, highlighting the dangers of materialism, consumerism, and the pursuit of power. Materialism and consumerism contribute to wealth inequality and environmental degradation by promoting unsustainable practices and exacerbating social and economic disparities. For instance, the overconsumption of resources leads to deforestation, climate change, and the exploitation of marginalized communities (Jackson, 2017). To address these challenges, we must reevaluate our priorities, focusing on sustainability, empathy, and the cultivation of meaningful relationships.\n", + "\n", + "Conclusion:\n", + "In conclusion, \"The Little Prince\" remains a topical and relevant work in modern life due to its allegorical nature, timeless message, and critique of modern society. Its exploration of human connections, materialism, and the pursuit of power offers valuable insights for readers of all ages. The Little Prince's message can be applied to various aspects of modern life, such as education, politics, and personal relationships, inspiring readers to reflect on its implications for their own lives. By embracing the story's wisdom, we can better navigate the complexities of the modern world and foster a more compassionate, sustainable, and interconnected society.\n", + "\n", + "References:\n", + "Holt-Lunstad, J. (2015). The Loneliness Paradox. American Psychological Association.\n", + "Jackson, T. (2017). Prosperity without Growth: Economics for a Finite Planet. Routledge.\n", "\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Title: The Enduring Relevance of The Little Prince: Timeless Lessons for the 21st Century\n", + "Your revised essay demonstrates a clear understanding of the assignment and the source material, and you have effectively incorporated the suggestions provided. The addition of research findings and specific examples has strengthened your argument and added credibility to your analysis. Your essay now provides a more nuanced exploration of the allegory, the relevance of the Little Prince's message, and the critique of modern society.\n", "\n", - "Introduction:\n", - "Antoine de Saint-Exupéry's The Little Prince continues to captivate readers as a classic tale that carries significant implications for contemporary society. With over 200 million copies sold and translations in more than 300 languages, its universal themes of love, friendship, responsibility, and the superficiality of the adult world remain profoundly relevant in the 21st century. As society grapples with increasing social isolation, mental health issues, and materialism, this essay explores the novel's powerful impact by discussing its themes in the context of studies on loneliness, personal growth, and superficiality in the digital age.\n", + "Here are some final recommendations to further enhance your essay:\n", "\n", - "Body Paragraph 1 - Love, Loneliness, and Isolation:\n", - "The Little Prince addresses themes of love and loneliness that still resonate strongly in today's world. The novel's portrayal of the prince's relationships emphasizes the significance of in-person connections in a time when digital communication dominates many aspects of everyday life. In a study conducted by McPherson, Smith-Lovin, and Brashears (2006), the authors revealed an alarming decline in the number of confidants in individuals' lives, indicating growing isolation. The Little Prince challenges readers to prioritize genuine human relationships over superficial online exchanges.\n", + "1. Ensure that your essay adheres to the required citation style (e.g., MLA, APA, or Chicago) and that all in-text citations and references are formatted correctly.\n", + "2. Double-check your essay for any grammatical errors, awkward phrasing, or unclear sentences. A well-written essay is not only easier to read but also more persuasive and engaging.\n", + "3. Consider adding a brief introduction to each body paragraph to provide context and guide the reader through your analysis. This will help ensure that your essay flows logically and that your arguments are easy to follow.\n", + "4. As a final step, ask a peer or mentor to review your work and provide feedback. A fresh pair of eyes can help you identify areas for improvement and ensure that your essay is polished and professional.\n", "\n", - "One notable scene in The Little Prince portrays the emotional impact of loneliness. The little prince's devotion to his rose, despite her flaws, highlights the value of even the most frustrating relationships in providing solace to those yearning for connection. The novel encourages readers to seek and maintain in-person interactions and forge emotional bonds that can help mitigate the feelings of loneliness and isolation that may arise in the modern age.\n", - "\n", - "Body Paragraph 2 - Responsibility, Personal Growth, and Emotional Intelligence:\n", - "The Little Prince emphasizes responsibility, self-awareness, and personal growth as critical components of emotional intelligence, which remains salient in modern society. Research consistently links emotional intelligence to mental health and well-being. A 2001 study conducted by Schutte and colleagues found that higher emotional intelligence was associated with fewer symptoms of anxiety and depression, suggesting that the novel's focus on personal growth and self-awareness offers valuable insights in the face of today's challenges.\n", - "\n", - "In response to the pressures of adulthood and rigid expectations, the novel underscores the importance of pursuing personal growth and responsibility, embracing self-discovery, and nurturing emotional intelligence as a means of coping with the complexities of life in contemporary society. According to Salovey and Mayer (1990), growing emotional intelligence allows individuals to understand their own emotions and those of others more deeply, which contributes to overall mental well-being.\n", - "\n", - "Body Paragraph 3 - Materialism, Superficiality, and Social Media:\n", - "The Little Prince critiques the materialistic and superficial nature of the adult world, which becomes more apparent in the digital age and social media-dominated society. The novel introduces characters like the businessman, who devotes his life to counting stars while prioritizing material possessions and wealth over genuine relationships. This behavior can be likened to the modern trend of cultivating an online presence and seeking validation through the accumulation of \"likes\" and \"followers.\"\n", - "\n", - "Research suggests that social media use may have detrimental effects on mental health and well-being. For example, a study conducted by Kross et al. (2013) found that frequent Facebook use was associated with decreased well-being and increased loneliness, supporting The Little Prince's assertion that superficiality and materialism can have damaging consequences on mental health. The novel encourages readers to engage with the world around them and seek genuine connections that transcend superficial distractions.\n", - "\n", - "Conclusion:\n", - "The Little Prince remains a timeless and relevant work in the 21st century. The novel's exploration of topics such as personal growth, mental health, materialism, and loneliness continues to offer valuable insights for contemporary society. The novel challenges readers to cherish and foster deep, meaningful relationships, engage in self-discovery, and resist the superficiality and materialism prevalent in today's world. By doing so, The Little Prince reminds us of the wisdom it possesses and the importance of its themes in our daily lives.\n" + "Overall, your essay provides a thoughtful and engaging exploration of \"The Little Prince\" and its relevance in modern life. By incorporating the recommendations provided, you can further enhance your analysis and create a truly exceptional piece of writing.\n" ] } ], "source": [ - "ChatPromptTemplate.from_messages(event[END]).pretty_print()" + "ChatPromptTemplate.from_messages(state.values[\"messages\"]).pretty_print()" ] }, { "cell_type": "markdown", "id": "0fa62df2-e8ee-40dd-ac95-9d982eae6079", - "metadata": {}, + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "## Conclusion\n", "\n", "Now that you've applied reflection to an LLM agent, I'll note one thing: self-reflection is inherently cyclic: it is much more effective if the reflection step has additional context or feedback (from tool observations, checks, etc.). If, like in the scenario above, the reflection step simply prompts the LLM to reflect on its output, it can still benefit the output quality (since the LLM then has multiple \"shots\" at getting a good output), but it's less guaranteed.\n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7c0e3efd-7f54-410e-bd31-36185a46b9a8", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 7d3527db386d5f299e59628b34863a3d4eacea33 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Wed, 28 Aug 2024 12:12:46 -0400 Subject: [PATCH 162/287] cli: release 0.1.52 (#1516) --- libs/cli/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml index d2ad29301..20fe010d9 100644 --- a/libs/cli/pyproject.toml +++ b/libs/cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-cli" -version = "0.1.51" +version = "0.1.52" description = "CLI for interacting with LangGraph API" authors = [] license = "MIT" From da38905db72d18f7c104974c7b0a19d068d81ae4 Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:36:36 -0700 Subject: [PATCH 163/287] [docs]: fix llm compiler (#1517) --- examples/llm-compiler/LLMCompiler.ipynb | 129 ++++++++++-------------- examples/llm-compiler/math_tools.py | 2 +- 2 files changed, 55 insertions(+), 76 deletions(-) diff --git a/examples/llm-compiler/LLMCompiler.ipynb b/examples/llm-compiler/LLMCompiler.ipynb index 2f2aa81ed..98f5416d8 100644 --- a/examples/llm-compiler/LLMCompiler.ipynb +++ b/examples/llm-compiler/LLMCompiler.ipynb @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "abbd6948-e9a3-47ca-89c7-7ac2fc5eca8b", "metadata": {}, "outputs": [], @@ -73,7 +73,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 47, "id": "e7476bb2-1a51-42f6-b7ae-82a0300bbf84", "metadata": {}, "outputs": [], @@ -147,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 78, "id": "15dd9639-691f-4906-9012-83fd6e9ac126", "metadata": {}, "outputs": [ @@ -211,7 +211,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 79, "id": "45689d40-d8df-4316-a121-6ea9c87d2efe", "metadata": {}, "outputs": [], @@ -270,7 +270,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 80, "id": "bbdcb57b-5362-4b9e-88db-fb3fae443fb0", "metadata": {}, "outputs": [], @@ -282,7 +282,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 81, "id": "730490c6-6e3a-4173-82a1-9eb9d5eeff20", "metadata": {}, "outputs": [ @@ -292,7 +292,7 @@ "text": [ "description='tavily_search_results_json(query=\"the search query\") - a search engine.' max_results=1 {'query': 'current temperature in San Francisco'}\n", "---\n", - "name='math' description='math(problem: str, context: Optional[List[str]] = None, config: Optional[langchain_core.runnables.config.RunnableConfig] = None) - math(problem: str, context: Optional[list[str]]) -> float:\\n - Solves the provided math problem.\\n - `problem` can be either a simple math problem (e.g. \"1 + 3\") or a word problem (e.g. \"how many apples are there if there are 3 apples and 2 apples\").\\n - You cannot calculate multiple expressions in one call. For instance, `math(\\'1 + 3, 2 + 4\\')` does not work. If you need to calculate multiple expressions, you need to call them separately like `math(\\'1 + 3\\')` and then `math(\\'2 + 4\\')`\\n - Minimize the number of `math` actions as much as possible. For instance, instead of calling 2. math(\"what is the 10% of $1\") and then call 3. math(\"$1 + $2\"), you MUST call 2. math(\"what is the 110% of $1\") instead, which will reduce the number of math actions.\\n - You can optionally provide a list of strings as `context` to help the agent solve the problem. If there are multiple contexts you need to answer the question, you can provide them as a list of strings.\\n - `math` action will not see the output of the previous actions unless you provide it as `context`. You MUST provide the output of the previous actions as `context` if you need to do math on it.\\n - You MUST NEVER provide `search` type action\\'s outputs as a variable in the `problem` argument. This is because `search` returns a text blob that contains the information about the entity, not a number or value. Therefore, when you need to provide an output of `search` action, you MUST provide it as a `context` argument to `math` action. For example, 1. search(\"Barack Obama\") and then 2. math(\"age of $1\") is NEVER allowed. Use 2. math(\"age of Barack Obama\", context=[\"$1\"]) instead.\\n - When you ask a question about `context`, specify the units. For instance, \"what is xx in height?\" or \"what is xx in millions?\" instead of \"what is xx?\"' args_schema= func=.calculate_expression at 0x10f354ea0> {'problem': 'raise $0 to the 3rd power', 'context': ['$0']}\n", + "name='math' description='math(problem: str, context: Optional[list[str]]) -> float:\\n - Solves the provided math problem.\\n - `problem` can be either a simple math problem (e.g. \"1 + 3\") or a word problem (e.g. \"how many apples are there if there are 3 apples and 2 apples\").\\n - You cannot calculate multiple expressions in one call. For instance, `math(\\'1 + 3, 2 + 4\\')` does not work. If you need to calculate multiple expressions, you need to call them separately like `math(\\'1 + 3\\')` and then `math(\\'2 + 4\\')`\\n - Minimize the number of `math` actions as much as possible. For instance, instead of calling 2. math(\"what is the 10% of $1\") and then call 3. math(\"$1 + $2\"), you MUST call 2. math(\"what is the 110% of $1\") instead, which will reduce the number of math actions.\\n - You can optionally provide a list of strings as `context` to help the agent solve the problem. If there are multiple contexts you need to answer the question, you can provide them as a list of strings.\\n - `math` action will not see the output of the previous actions unless you provide it as `context`. You MUST provide the output of the previous actions as `context` if you need to do math on it.\\n - You MUST NEVER provide `search` type action\\'s outputs as a variable in the `problem` argument. This is because `search` returns a text blob that contains the information about the entity, not a number or value. Therefore, when you need to provide an output of `search` action, you MUST provide it as a `context` argument to `math` action. For example, 1. search(\"Barack Obama\") and then 2. math(\"age of $1\") is NEVER allowed. Use 2. math(\"age of Barack Obama\", context=[\"$1\"]) instead.\\n - When you ask a question about `context`, specify the units. For instance, \"what is xx in height?\" or \"what is xx in millions?\" instead of \"what is xx?\"' args_schema= func=.calculate_expression at 0x14e1049a0> {'problem': 'x^3', 'context': ['$1']}\n", "---\n", "join ()\n", "---\n" @@ -331,7 +331,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 82, "id": "c1fbafdd-42d4-4575-8466-e5951cee71f4", "metadata": { "jp-MarkdownHeadingCollapsed": true @@ -493,7 +493,7 @@ " }\n", " tool_messages = [\n", " FunctionMessage(\n", - " name=name, content=str(obs), additional_kwargs={\"idx\": k, \"args\": task_args}\n", + " name=name, content=str(obs), additional_kwargs={\"idx\": k, \"args\": task_args}, tool_call_id = k\n", " )\n", " for k, (name, task_args, obs) in new_observations.items()\n", " ]\n", @@ -502,7 +502,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 83, "id": "052f6b16-103a-40e9-94dd-8fcc37e77ba4", "metadata": {}, "outputs": [], @@ -526,7 +526,7 @@ " \"tasks\": tasks,\n", " }\n", " )\n", - " return {\"messages\": [scheduled_tasks]}" + " return {\"messages\": scheduled_tasks}" ] }, { @@ -541,29 +541,29 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 84, "id": "55142257-2674-4a47-988e-0d2810917329", "metadata": {}, "outputs": [], "source": [ - "tool_messages = plan_and_schedule.invoke([HumanMessage(content=example_question)])" + "tool_messages = plan_and_schedule.invoke({\"messages\":[HumanMessage(content=example_question)]})['messages']" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 85, "id": "a98e0525-2fcf-4fa1-baf6-79858bb8a6bd", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[FunctionMessage(content='[]', additional_kwargs={'idx': 0}, name='tavily_search_results_json'),\n", - " FunctionMessage(content='ValueError(\\'Failed to evaluate \"N/A\". Raised error: KeyError(\\\\\\'A\\\\\\'). Please try again with a valid numerical expression\\')', additional_kwargs={'idx': 1}, name='math'),\n", - " FunctionMessage(content='join', additional_kwargs={'idx': 2}, name='join')]" + "[FunctionMessage(content=\"[{'url': 'https://www.wunderground.com/weather/us/ca/san-francisco', 'content': 'Current Weather for Popular Cities . San Francisco, CA 82 ° F Sunny; Manhattan, NY warning 84 ° F Sunny; Schiller Park, IL (60176) warning 97 ° F Mostly Cloudy; Boston, MA warning 74 ° F ...'}]\", additional_kwargs={'idx': 1, 'args': {'query': 'current temperature in San Francisco'}}, name='tavily_search_results_json', tool_call_id=1),\n", + " FunctionMessage(content='551368', additional_kwargs={'idx': 2, 'args': {'problem': 'x ** 3', 'context': ['$1']}}, name='math', tool_call_id=2),\n", + " FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]" ] }, - "execution_count": 12, + "execution_count": 85, "metadata": {}, "output_type": "execute_result" } @@ -589,12 +589,11 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 86, "id": "942dab42-ad42-4ba2-90d5-49edbe4fae68", "metadata": {}, "outputs": [], "source": [ - "from langchain.chains.openai_functions import create_structured_output_runnable\n", "from langchain_core.messages import AIMessage\n", "from langchain_core.pydantic_v1 import BaseModel, Field\n", "\n", @@ -625,7 +624,7 @@ ") # You can optionally add examples\n", "llm = ChatOpenAI(model=\"gpt-4-turbo-preview\")\n", "\n", - "runnable = create_structured_output_runnable(JoinOutputs, llm, joiner_prompt)" + "runnable = joiner_prompt | llm.with_structured_output(JoinOutputs)" ] }, { @@ -639,7 +638,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 87, "id": "951a33cf-2a05-4a33-899a-0ab1d97122fa", "metadata": {}, "outputs": [], @@ -671,7 +670,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 88, "id": "1e49d4b1-8266-4520-a566-1448b1c31c8f", "metadata": {}, "outputs": [], @@ -681,24 +680,24 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 89, "id": "31854dfd-b82f-4c24-9b58-6bae66777909", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[AIMessage(content='Thought: The search did not return any results, and the attempt to calculate the temperature in San Francisco raised to the 3rd power failed due to missing temperature information.'),\n", - " SystemMessage(content='Context from last attempt: I need to find the current temperature in San Francisco before calculating its value raised to the 3rd power.')]" + "{'messages': [AIMessage(content=\"Thought: We have the current temperature in San Francisco (82 °F) and have calculated the temperature raised to the 3rd power (551368). Therefore, we can provide an answer to the user's question.\"),\n", + " AIMessage(content='The temperature in San Francisco raised to the 3rd power is 551368.')]}" ] }, - "execution_count": 16, + "execution_count": 89, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "joiner.invoke(input_messages)" + "joiner.invoke({\"messages\":input_messages})" ] }, { @@ -717,7 +716,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 90, "id": "768b5f11-e3d2-47be-8143-a7dcd8765243", "metadata": {}, "outputs": [], @@ -754,9 +753,9 @@ "\n", "\n", "graph_builder.add_conditional_edges(\n", - " start_key=\"join\",\n", + " \"join\",\n", " # Next, we pass in the function that will determine which node is called next.\n", - " condition=should_continue,\n", + " should_continue,\n", ")\n", "graph_builder.add_edge(START, \"plan_and_schedule\")\n", "chain = graph_builder.compile()" @@ -774,7 +773,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 91, "id": "5bc4584a-e31c-4065-805e-76a6db30676a", "metadata": {}, "outputs": [ @@ -782,15 +781,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'plan_and_schedule': [FunctionMessage(content='[{\\'url\\': \\'https://www.governor.ny.gov/programs/fy-2024-new-york-state-budget\\', \\'content\\': \"The $229 billion FY 2024 New York State Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, FY 2024 Budget Assets FY 2024 New York State Budget Highlights Improving Public Safety GOVERNOR HOME GOVERNOR KATHY HOCHUL FY 2024 New York State Budget Transformative investments to support New York\\'s business community and boost the state economy.The $229 billion FY 2024 NYS Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, more livable, and safer.\"}]', additional_kwargs={'idx': 0}, name='tavily_search_results_json')]}\n", + "{'plan_and_schedule': {'messages': [FunctionMessage(content=\"[{'url': 'https://www.investopedia.com/articles/investing/011516/new-yorks-economy-6-industries-driving-gdp-growth.asp', 'content': 'The manufacturing sector is a leader in railroad rolling stock, as many of the earliest railroads were financed or founded in New York; garments, as New York City is the fashion capital of the U.S.; elevator parts; glass; and many other products.\\\\n Educational Services\\\\nThough not typically thought of as a leading industry, the educational sector in New York nonetheless has a substantial impact on the state and its residents, and in attracting new talent that eventually enters the New York business scene. New York has seen a large uptick in college attendees, both young and old, over the 21st century, and an increasing number of new employees in other New York sectors were educated in the state. New York City is the leading job hub for banking, finance, and communication in the U.S. New York is also a major manufacturing center and shipping port, and it has a thriving technological sector.\\\\n The state of New York has the third-largest economy in the United States with a gross domestic product (GDP) of $1.7 trillion, trailing only Texas and California.'}]\", additional_kwargs={'idx': 1, 'args': {'query': 'GDP of New York'}}, name='tavily_search_results_json', tool_call_id=1)]}}\n", "---\n", - "{'join': [AIMessage(content=\"Thought: The information provided does not specify the Gross Domestic Product (GDP) of New York, but instead provides details about the state's budget for fiscal year 2024, which is $229 billion. This budget figure cannot be accurately equated to the GDP.\"), SystemMessage(content=\"Context from last attempt: The search results provided information about New York's state budget rather than its GDP. To answer the user's question, we need to find specific data on New York's GDP, not its budget.\")]}\n", - "---\n", - "{'plan_and_schedule': [FunctionMessage(content=\"[{'url': 'https://en.wikipedia.org/wiki/Economy_of_New_York_(state)', 'content': 'The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third Contents Economy of New York (state) New York City-centered metropolitan statistical area produced a gross metropolitan product (GMP) of $US2.0 trillion, of the items in which New York ranks high nationally:The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third in size behind the larger states of\\\\xa0...'}]\", additional_kwargs={'idx': 1}, name='tavily_search_results_json')]}\n", - "---\n", - "{'join': [AIMessage(content=\"Thought: The required information about New York's GDP is provided in the search results. In 2022, New York had a Gross State Product (GSP) of $2.053 trillion.\"), AIMessage(content='The Gross Domestic Product (GDP) of New York in 2022 was $2.053 trillion.')]}\n", - "---\n", - "{'__end__': [HumanMessage(content=\"What's the GDP of New York?\"), FunctionMessage(content='[{\\'url\\': \\'https://www.governor.ny.gov/programs/fy-2024-new-york-state-budget\\', \\'content\\': \"The $229 billion FY 2024 New York State Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, FY 2024 Budget Assets FY 2024 New York State Budget Highlights Improving Public Safety GOVERNOR HOME GOVERNOR KATHY HOCHUL FY 2024 New York State Budget Transformative investments to support New York\\'s business community and boost the state economy.The $229 billion FY 2024 NYS Budget reflects Governor Hochul\\'s bold agenda to make New York more affordable, more livable, and safer.\"}]', additional_kwargs={'idx': 0}, name='tavily_search_results_json'), AIMessage(content=\"Thought: The information provided does not specify the Gross Domestic Product (GDP) of New York, but instead provides details about the state's budget for fiscal year 2024, which is $229 billion. This budget figure cannot be accurately equated to the GDP.\"), SystemMessage(content=\"Context from last attempt: The search results provided information about New York's state budget rather than its GDP. To answer the user's question, we need to find specific data on New York's GDP, not its budget. - Begin counting at : 1\"), FunctionMessage(content=\"[{'url': 'https://en.wikipedia.org/wiki/Economy_of_New_York_(state)', 'content': 'The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third Contents Economy of New York (state) New York City-centered metropolitan statistical area produced a gross metropolitan product (GMP) of $US2.0 trillion, of the items in which New York ranks high nationally:The economy of the State of New York is reflected in its gross state product in 2022 of $2.053 trillion, ranking third in size behind the larger states of\\\\xa0...'}]\", additional_kwargs={'idx': 1}, name='tavily_search_results_json'), AIMessage(content=\"Thought: The required information about New York's GDP is provided in the search results. In 2022, New York had a Gross State Product (GSP) of $2.053 trillion.\"), AIMessage(content='The Gross Domestic Product (GDP) of New York in 2022 was $2.053 trillion.')]}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: The information required to answer the user's question has been found. The GDP of New York is mentioned as $1.7 trillion, making it the third-largest economy in the United States.\", id='d656a605-e4c4-470d-9b29-31794f298a71'), AIMessage(content='The GDP of New York is $1.7 trillion, making it the third-largest economy in the United States.', id='5135758e-d01e-4360-bb6a-31025b723d8c')]}}\n", "---\n" ] } @@ -805,7 +798,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 92, "id": "b96efd08-5314-44f0-a694-3073b638adad", "metadata": {}, "outputs": [ @@ -813,13 +806,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "The Gross Domestic Product (GDP) of New York in 2022 was $2.053 trillion.\n" + "The GDP of New York is $1.7 trillion, making it the third-largest economy in the United States.\n" ] } ], "source": [ "# Final answer\n", - "print(step[END][-1].content)" + "print(step['join']['messages'][-1].content)" ] }, { @@ -834,7 +827,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 93, "id": "0b3a0916-d8ca-4092-b91c-d9e2b05259d8", "metadata": {}, "outputs": [ @@ -842,26 +835,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'plan_and_schedule': [FunctionMessage(content=\"[{'url': 'https://a-z-animals.com/blog/discover-the-worlds-oldest-parrot/', 'content': 'How Old Is the World’s Oldest Parrot? Discover the World’s Oldest Parrot Advertisement of debate, so we’ll detail some other parrots whose lifespans may be longer but are hard to verify their exact age. Comparing Parrots’ Lifespans to Other BirdsSep 8, 2023 — Sep 8, 2023The oldest parrot on record is Cookie, a pink cockatoo that survived to the age of 83 and survived his entire life at the Brookfield Zoo.'}]\", additional_kwargs={'idx': 0}, name='tavily_search_results_json'), FunctionMessage(content=\"HTTPError('502 Server Error: Bad Gateway for url: https://api.tavily.com/search')\", additional_kwargs={'idx': 1}, name='tavily_search_results_json'), FunctionMessage(content='join', additional_kwargs={'idx': 2}, name='join')]}\n", + "{'plan_and_schedule': {'messages': [FunctionMessage(content='[{\\'url\\': \\'https://en.wikipedia.org/wiki/Cookie_(cockatoo)\\', \\'content\\': \\'He was one of the longest-lived birds on record[4] and was recognised by the Guinness World Records as the oldest living parrot in the world.[5]\\\\nThe next-oldest pink cockatoo to be found in a zoological setting was a 31-year-old female bird located at Paradise Wildlife Sanctuary, England.[3] Information published by the World Parrot Trust states longevity for Cookie\\\\\\'s species in captivity is on average 40–60 years.[6]\\\\nLife[edit]\\\\nCookie was Brookfield Zoo\\\\\\'s oldest resident and the last surviving member of the animal collection from the time of the zoo\\\\\\'s opening in 1934, having arrived from Taronga Zoo of Sydney, New South Wales, Australia, in the same year and judged to be one year old at the time.[7]\\\\nIn the 1950s an attempt was made to introduce Cookie to a female pink cockatoo, but Cookie rejected her as \"she was not nice to him\".[8]\\\\n In 2007, Cookie was diagnosed with, and placed on medication and nutritional supplements for, osteoarthritis and osteoporosis\\\\xa0– medical conditions which occur commonly in aging animals and humans alike,[7] although it is believed that the latter may also have been brought on as a result of being fed a seed-only diet for the first 40 years of his life, in the years before the dietary requirements of his species were fully understood.[9]\\\\nCookie was \"retired\" from exhibition at the zoo in 2009 (following a few months of weekend-only appearances) in order to preserve his health, after it was noticed by staff that his appetite, demeanor and stress levels improved markedly when not on public display. age.[11] A memorial at the zoo was unveiled in September 2017.[12]\\\\nIn 2020, Cookie became the subject of a poetry collection by Barbara Gregorich entitled Cookie the Cockatoo: Everything Changes.[13]\\\\nSee also[edit]\\\\nReferences[edit]\\\\nExternal links[edit] He was believed to be the oldest member of his species alive in captivity, at the age of 82 in June 2015,[1][2] having significantly exceeded the average lifespan for his kind.[3] He was moved to a permanent residence in the keepers\\\\\\' office of the zoo\\\\\\'s Perching Bird House, although he made occasional appearances for special events, such as his birthday celebration, which was held each June.[3]\\'}]', additional_kwargs={'idx': 1, 'args': {'query': 'oldest parrot alive'}}, name='tavily_search_results_json', tool_call_id=1), FunctionMessage(content='[{\\'url\\': \\'https://www.thesprucepets.com/how-long-do-parrots-and-other-pet-birds-live-1238433\\', \\'content\\': \"It\\'s possible that a pet bird can outlive its owners\\\\nThe Spruce / Adrienne Legault\\\\nParrots and other birds can live up to 10 to 50 years or more depending on the type and the conditions they live in. They vary in size from small birds that can fit in the palm of your hand to large birds the size of a cat and their lifespans are just as variable.\\\\n Also, for birds who live longer some owners have to make a plan of where the bird is going in the circumstance the bird outlives the owner.\\\\n In reality, there is a wide range in the age that pet birds might reach and certainly, some will live longer (or shorter amounts of time) than the ages listed.\\\\n Potential owners need to be aware of the longevity of their bird so they can be prepared to provide proper care for them for as long as they live.\\\\n\"}]', additional_kwargs={'idx': 2, 'args': {'query': 'average lifespan of a parrot'}}, name='tavily_search_results_json', tool_call_id=2), FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]}}\n", "---\n", - "{'join': [AIMessage(content='Thought: The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. However, there was an error fetching additional search results to compare this age to the average lifespan of parrots.'), SystemMessage(content='Context from last attempt: I found the age of the oldest parrot, Cookie, who lived to be 83 years old. However, I need to search again to find the average lifespan of parrots to complete the comparison.')]}\n", - "---\n", - "{'plan_and_schedule': [FunctionMessage(content='[{\\'url\\': \\'https://www.turlockvet.com/site/blog/2023/07/15/parrot-lifespan--how-long-pet-parrots-live\\', \\'content\\': \"Parrot Lifespan the lifespan of a parrot?\\'. Parrot Lifespan: How Long Do Pet Parrots Live? how long they actually live and what you should know about owning a parrot.Jul 15, 2023 — Jul 15, 2023Generally, the average lifespan of smaller species of parrots such as Budgies and Cockatiels is about 5 - 15 years, while larger parrots such as\\\\xa0...\"}]', additional_kwargs={'idx': 3}, name='tavily_search_results_json')]}\n", - "---\n", - "{'join': [AIMessage(content=\"Thought: I have found that the oldest parrot on record, Cookie, lived to be 83 years old. Additionally, I've found that the average lifespan of parrots varies by species, with smaller species like Budgies and Cockatiels living between 5-15 years, and larger parrots potentially living longer. This allows me to compare Cookie's age to the average lifespan of smaller parrot species.\"), AIMessage(content=\"The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. Compared to the average lifespan of smaller parrot species such as Budgies and Cockatiels, which is about 5-15 years, Cookie lived significantly longer. The average lifespan of larger parrot species wasn't specified, but it's implied that larger parrots may live longer than smaller species, yet likely still much less than 83 years.\")]}\n", - "---\n", - "{'__end__': [HumanMessage(content=\"What's the oldest parrot alive, and how much longer is that than the average?\"), FunctionMessage(content=\"[{'url': 'https://a-z-animals.com/blog/discover-the-worlds-oldest-parrot/', 'content': 'How Old Is the World’s Oldest Parrot? Discover the World’s Oldest Parrot Advertisement of debate, so we’ll detail some other parrots whose lifespans may be longer but are hard to verify their exact age. Comparing Parrots’ Lifespans to Other BirdsSep 8, 2023 — Sep 8, 2023The oldest parrot on record is Cookie, a pink cockatoo that survived to the age of 83 and survived his entire life at the Brookfield Zoo.'}]\", additional_kwargs={'idx': 0}, name='tavily_search_results_json'), FunctionMessage(content=\"HTTPError('502 Server Error: Bad Gateway for url: https://api.tavily.com/search')\", additional_kwargs={'idx': 1}, name='tavily_search_results_json'), FunctionMessage(content='join', additional_kwargs={'idx': 2}, name='join'), AIMessage(content='Thought: The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. However, there was an error fetching additional search results to compare this age to the average lifespan of parrots.'), SystemMessage(content='Context from last attempt: I found the age of the oldest parrot, Cookie, who lived to be 83 years old. However, I need to search again to find the average lifespan of parrots to complete the comparison. - Begin counting at : 3'), FunctionMessage(content='[{\\'url\\': \\'https://www.turlockvet.com/site/blog/2023/07/15/parrot-lifespan--how-long-pet-parrots-live\\', \\'content\\': \"Parrot Lifespan the lifespan of a parrot?\\'. Parrot Lifespan: How Long Do Pet Parrots Live? how long they actually live and what you should know about owning a parrot.Jul 15, 2023 — Jul 15, 2023Generally, the average lifespan of smaller species of parrots such as Budgies and Cockatiels is about 5 - 15 years, while larger parrots such as\\\\xa0...\"}]', additional_kwargs={'idx': 3}, name='tavily_search_results_json'), AIMessage(content=\"Thought: I have found that the oldest parrot on record, Cookie, lived to be 83 years old. Additionally, I've found that the average lifespan of parrots varies by species, with smaller species like Budgies and Cockatiels living between 5-15 years, and larger parrots potentially living longer. This allows me to compare Cookie's age to the average lifespan of smaller parrot species.\"), AIMessage(content=\"The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. Compared to the average lifespan of smaller parrot species such as Budgies and Cockatiels, which is about 5-15 years, Cookie lived significantly longer. The average lifespan of larger parrot species wasn't specified, but it's implied that larger parrots may live longer than smaller species, yet likely still much less than 83 years.\")]}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: We have information on Cookie, the cockatoo, who was recognized as the oldest living parrot at 82 years old in June 2015. This significantly exceeds the average lifespan for his kind, which is stated to be 40-60 years. The second source provides a general lifespan range for parrots and other birds, which is 10-50 years. However, this range varies significantly depending on the species and conditions. Since Cookie's specific lifespan far exceeds the average for his species and falls outside the general range for parrots, we can answer the user's question.\", id='51a280ac-2327-40c5-a27a-c821697d5a4b'), AIMessage(content='The oldest parrot recorded was Cookie, a cockatoo, who lived to be 82 years old in June 2015. This is significantly longer than the average lifespan for his species, which is 40-60 years, and also exceeds the general lifespan range for parrots, which can vary from 10 to 50 years. Therefore, Cookie lived 22 to 42 years longer than the average lifespan for his species.', id='139ecedf-b090-4197-88c0-0fa39883b392')]}}\n", "---\n" ] } ], "source": [ - "steps = chain.stream(\n", + "steps = chain.stream({\"messages\":\n", " [\n", " HumanMessage(\n", " content=\"What's the oldest parrot alive, and how much longer is that than the average?\"\n", " )\n", - " ],\n", + " ]\n", + " },\n", " {\n", " \"recursion_limit\": 100,\n", " },\n", @@ -873,7 +861,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 94, "id": "6c65c414-7668-4fdf-ba97-f42f659b1317", "metadata": {}, "outputs": [ @@ -881,13 +869,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "The oldest parrot on record is Cookie, a pink cockatoo, who lived to be 83 years old. Compared to the average lifespan of smaller parrot species such as Budgies and Cockatiels, which is about 5-15 years, Cookie lived significantly longer. The average lifespan of larger parrot species wasn't specified, but it's implied that larger parrots may live longer than smaller species, yet likely still much less than 83 years.\n" + "The oldest parrot recorded was Cookie, a cockatoo, who lived to be 82 years old in June 2015. This is significantly longer than the average lifespan for his species, which is 40-60 years, and also exceeds the general lifespan range for parrots, which can vary from 10 to 50 years. Therefore, Cookie lived 22 to 42 years longer than the average lifespan for his species.\n" ] } ], "source": [ "# Final answer\n", - "print(step[END][-1].content)" + "print(step['join']['messages'][-1].content)" ] }, { @@ -900,7 +888,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 96, "id": "38d3ea91-59ba-4267-8060-ed75bbc840c6", "metadata": {}, "outputs": [ @@ -908,26 +896,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'plan_and_schedule': [FunctionMessage(content='3307.0', additional_kwargs={'idx': 1}, name='math'), FunctionMessage(content='7.565011820330969', additional_kwargs={'idx': 2}, name='math'), FunctionMessage(content='3314.565011820331', additional_kwargs={'idx': 3}, name='math'), FunctionMessage(content='join', additional_kwargs={'idx': 4}, name='join')]}\n", - "{'join': [AIMessage(content=\"Thought: The calculations for each part of the user's question have been successfully completed. The first calculation resulted in 3307.0, the second in 7.565011820330969, and the sum of those two values was correctly found to be 3314.565011820331.\"), AIMessage(content='The result of ((3*(4+5)/0.5)+3245) + 8 is 3307.0, the result of 32/4.23 is approximately 7.565, and the sum of those two values is approximately 3314.565.')]}\n", - "{'__end__': [HumanMessage(content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"), FunctionMessage(content='3307.0', additional_kwargs={'idx': 1}, name='math'), FunctionMessage(content='7.565011820330969', additional_kwargs={'idx': 2}, name='math'), FunctionMessage(content='3314.565011820331', additional_kwargs={'idx': 3}, name='math'), FunctionMessage(content='join', additional_kwargs={'idx': 4}, name='join'), AIMessage(content=\"Thought: The calculations for each part of the user's question have been successfully completed. The first calculation resulted in 3307.0, the second in 7.565011820330969, and the sum of those two values was correctly found to be 3314.565011820331.\"), AIMessage(content='The result of ((3*(4+5)/0.5)+3245) + 8 is 3307.0, the result of 32/4.23 is approximately 7.565, and the sum of those two values is approximately 3314.565.')]}\n" + "{'plan_and_schedule': {'messages': [FunctionMessage(content='3307.0', additional_kwargs={'idx': 1, 'args': {'problem': '((3*(4+5)/0.5)+3245) + 8'}}, name='math', tool_call_id=1), FunctionMessage(content='7.565011820330969', additional_kwargs={'idx': 2, 'args': {'problem': '32/4.23'}}, name='math', tool_call_id=2), FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]}}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: The calculations for both individual questions have been provided: 3307.0 for the first equation and 7.565011820330969 for the second. To answer the user's final question, we need to sum these two values.\", id='96eb85f5-831f-434e-83d8-59deeebce05d'), AIMessage(content='The result of the first calculation is 3307.0, and the result of the second calculation is approximately 7.57. The sum of those two values is approximately 3314.57.', id='671a1a08-4725-4f98-997a-848815d61aa5')]}}\n" ] } ], "source": [ - "for step in chain.stream(\n", + "for step in chain.stream({\"messages\":\n", " [\n", " HumanMessage(\n", " content=\"What's ((3*(4+5)/0.5)+3245) + 8? What's 32/4.23? What's the sum of those two values?\"\n", " )\n", - " ]\n", + " ]}\n", "):\n", " print(step)" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 97, "id": "a6cf5fe0-f178-4197-950f-257711bff8d2", "metadata": { "scrolled": true @@ -937,13 +924,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "The result of ((3*(4+5)/0.5)+3245) + 8 is 3307.0, the result of 32/4.23 is approximately 7.565, and the sum of those two values is approximately 3314.565.\n" + "The result of the first calculation is 3307.0, and the result of the second calculation is approximately 7.57. The sum of those two values is approximately 3314.57.\n" ] } ], "source": [ "# Final answer\n", - "print(step[END][-1].content)" + "print(step['join']['messages'][-1].content)" ] }, { @@ -959,14 +946,6 @@ "2. Variable substitution is fragile in the example above. It could be made more robust by using a fine-tuned model and a more robust syntax (using e.g., Lark or a tool calling schema)\n", "3. The state can grow quite long if you require multiple re-planning runs. To handle, you could add a message compressor once you go above a certain token limit.\n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "431217e6-4c00-409f-a2bd-40ebff902489", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -985,7 +964,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/llm-compiler/math_tools.py b/examples/llm-compiler/math_tools.py index 8de7bfa2d..928153dab 100644 --- a/examples/llm-compiler/math_tools.py +++ b/examples/llm-compiler/math_tools.py @@ -114,7 +114,7 @@ def get_math_tool(llm: ChatOpenAI): MessagesPlaceholder(variable_name="context", optional=True), ] ) - extractor = create_structured_output_runnable(ExecuteCode, llm, prompt) + extractor = prompt | llm.with_structured_output(ExecuteCode) def calculate_expression( problem: str, From ab71d29286045680ec365f64cc668168d6c926c6 Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Wed, 28 Aug 2024 11:54:44 -0700 Subject: [PATCH 164/287] [docs]: update node config info (#1500) * config changes * vadym comments * format * spelling * x --- examples/configuration.ipynb | 160 +++++++++++++++++++++++++++-------- 1 file changed, 124 insertions(+), 36 deletions(-) diff --git a/examples/configuration.ipynb b/examples/configuration.ipynb index 4a2817c21..80f6ba25f 100644 --- a/examples/configuration.ipynb +++ b/examples/configuration.ipynb @@ -24,15 +24,43 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 1, "id": "816523d0-0b59-47cf-9f4c-4838024efe22", "metadata": {}, "outputs": [], - "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_anthropic import ChatAnthropic\nfrom langchain_core.messages import BaseMessage, HumanMessage\n\nfrom langgraph.graph import END, StateGraph, START\n\nmodel = ChatAnthropic(model_name=\"claude-2.1\")\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]\n\n\ndef _call_model(state):\n response = model.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"] + "source": [ + "import operator\n", + "from typing import Annotated, Sequence, TypedDict\n", + "\n", + "from langchain_anthropic import ChatAnthropic\n", + "from langchain_core.messages import BaseMessage, HumanMessage\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "model = ChatAnthropic(model_name=\"claude-2.1\")\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]\n", + "\n", + "\n", + "def _call_model(state):\n", + " response = model.invoke(state[\"messages\"])\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "workflow.add_node(\"model\", _call_model)\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_edge(\"model\", END)\n", + "\n", + "app = workflow.compile()" + ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 2, "id": "070f11a6-2441-4db5-9df6-e318f110e281", "metadata": {}, "outputs": [ @@ -40,15 +68,17 @@ "data": { "text/plain": [ "{'messages': [HumanMessage(content='hi'),\n", - " AIMessage(content='Hello!', response_metadata={'id': 'msg_01YZj7CVCUSc76faX4VM9i5d', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-d343db34-598c-46a2-93d6-ffa886d9b264-0')]}" + " AIMessage(content='Hello!', response_metadata={'id': 'msg_012SakNGNitBcKJgc9yZ1Asv', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-9e375cd7-ae84-4db2-981c-c7e18ecabddf-0', usage_metadata={'input_tokens': 10, 'output_tokens': 6, 'total_tokens': 16})]}" ] }, - "execution_count": 8, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], - "source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"] + "source": [ + "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})" + ] }, { "cell_type": "markdown", @@ -58,17 +88,44 @@ "## Configure the graph\n", "\n", "Great! Now let's suppose that we want to extend this example so the user is able to choose from multiple llms.\n", - "We can easily do that by passing in a config.\n", + "We can easily do that by passing in a config. Any configuration information needs to be passed inside `configurable` key as shown below.\n", "This config is meant to contain things are not part of the input (and therefore that we don't want to track as part of the state)." ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 6, "id": "c01f1e7c-8e8b-4e26-98f7-56ac225077b4", "metadata": {}, "outputs": [], - "source": ["from langchain_openai import ChatOpenAI\n\nopenai_model = ChatOpenAI()\n\nmodels = {\n \"anthropic\": model,\n \"openai\": openai_model,\n}\n\n\ndef _call_model(state, config):\n m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n response = m.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"] + "source": [ + "from langchain_openai import ChatOpenAI\n", + "from typing import Optional\n", + "from langchain_core.runnables.config import RunnableConfig\n", + "\n", + "openai_model = ChatOpenAI()\n", + "\n", + "models = {\n", + " \"anthropic\": model,\n", + " \"openai\": openai_model,\n", + "}\n", + "\n", + "def _call_model(state: AgentState, config: RunnableConfig):\n", + " # Access the config through the configurable key\n", + " model_name = config[\"configurable\"].get(\"model\", \"anthropic\")\n", + " model = models[model_name]\n", + " response = model.invoke(state[\"messages\"])\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "workflow.add_node(\"model\", _call_model)\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_edge(\"model\", END)\n", + "\n", + "app = workflow.compile()" + ] }, { "cell_type": "markdown", @@ -80,7 +137,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 7, "id": "ef50f048-fc43-40c0-b713-346408fcf052", "metadata": {}, "outputs": [ @@ -88,15 +145,17 @@ "data": { "text/plain": [ "{'messages': [HumanMessage(content='hi'),\n", - " AIMessage(content='Hello!', response_metadata={'id': 'msg_01EedReFyXmonWXPKhYre7Jb', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-1c6feaa0-bd6f-433a-8264-209d72c85db7-0')]}" + " AIMessage(content='Hello!', response_metadata={'id': 'msg_0133PAX5DyoUYL1gZiGR8NXs', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-03e8bd8b-fa09-4258-920d-8f53a7b91fcc-0', usage_metadata={'input_tokens': 10, 'output_tokens': 6, 'total_tokens': 16})]}" ] }, - "execution_count": 12, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], - "source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"] + "source": [ + "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})" + ] }, { "cell_type": "markdown", @@ -108,7 +167,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 8, "id": "f2f7c74b-9fb0-41c6-9728-dcf9d8a3c397", "metadata": {}, "outputs": [ @@ -116,15 +175,18 @@ "data": { "text/plain": [ "{'messages': [HumanMessage(content='hi'),\n", - " AIMessage(content='Hello! How can I assist you today?', response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 8, 'total_tokens': 17}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': 'fp_3b956da36b', 'finish_reason': 'stop', 'logprobs': None}, id='run-d41ffb62-e164-45a1-862c-d288c6ad100a-0')]}" + " AIMessage(content='Hello! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 8, 'total_tokens': 17}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-6d0c7c25-03de-49d6-b3be-ff0858d17122-0', usage_metadata={'input_tokens': 8, 'output_tokens': 9, 'total_tokens': 17})]}" ] }, - "execution_count": 13, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], - "source": ["config = {\"configurable\": {\"model\": \"openai\"}}\napp.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"] + "source": [ + "config = {\"configurable\": {\"model\": \"openai\"}}\n", + "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)" + ] }, { "cell_type": "markdown", @@ -136,15 +198,44 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 9, "id": "f0393a43-9fbe-4056-972f-3e91ea329041", "metadata": {}, "outputs": [], - "source": ["from langchain_core.messages import SystemMessage\n\n\ndef _call_model(state, config):\n m = models[config[\"configurable\"].get(\"model\", \"anthropic\")]\n messages = state[\"messages\"]\n if \"system_message\" in config[\"configurable\"]:\n messages = [\n SystemMessage(content=config[\"configurable\"][\"system_message\"])\n ] + messages\n response = m.invoke(messages)\n return {\"messages\": [response]}\n\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"model\", _call_model)\nworkflow.add_edge(START, \"model\")\nworkflow.add_edge(\"model\", END)\n\napp = workflow.compile()"] + "source": [ + "from langchain_core.messages import SystemMessage\n", + "\n", + "# We can define a config schema to specify the configuration options for the graph\n", + "# A config schema is useful for indicating which fields are available in the configurable dict inside the config\n", + "class ConfigSchema(TypedDict):\n", + " model: Optional[str]\n", + " system_message: Optional[str]\n", + "\n", + "def _call_model(state: AgentState, config: RunnableConfig):\n", + " # Access the config through the configurable key\n", + " model_name = config[\"configurable\"].get(\"model\", \"anthropic\")\n", + " model = models[model_name]\n", + " messages = state[\"messages\"]\n", + " if \"system_message\" in config[\"configurable\"]:\n", + " messages = [\n", + " SystemMessage(content=config[\"configurable\"][\"system_message\"])\n", + " ] + messages\n", + " response = model.invoke(messages)\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Define a new graph - note that we pass in the configuration schema here, but it is not necessary\n", + "workflow = StateGraph(AgentState, ConfigSchema)\n", + "workflow.add_node(\"model\", _call_model)\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_edge(\"model\", END)\n", + "\n", + "app = workflow.compile()" + ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 10, "id": "718685f7-4cdd-4181-9fc8-e7762d584727", "metadata": {}, "outputs": [ @@ -152,19 +243,21 @@ "data": { "text/plain": [ "{'messages': [HumanMessage(content='hi'),\n", - " AIMessage(content='Hello!', response_metadata={'id': 'msg_01Ts56eVLSrUbzVMbzLnXc3M', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-f75a4389-b72e-4d47-8f3e-bedc6a060f66-0')]}" + " AIMessage(content='Hello!', response_metadata={'id': 'msg_01TVJvxCXsCT9JVe7A4iUUi9', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 10, 'output_tokens': 6}}, id='run-627eb685-c4d7-481d-9095-c0a1822e8c10-0', usage_metadata={'input_tokens': 10, 'output_tokens': 6, 'total_tokens': 16})]}" ] }, - "execution_count": 19, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], - "source": ["app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})"] + "source": [ + "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]})" + ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 11, "id": "e043a719-f197-46ef-9d45-84740a39aeb0", "metadata": {}, "outputs": [ @@ -172,23 +265,18 @@ "data": { "text/plain": [ "{'messages': [HumanMessage(content='hi'),\n", - " AIMessage(content='Ciao!', response_metadata={'id': 'msg_01RzFCii8WhbbkFm16nUquxk', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 14, 'output_tokens': 7}}, id='run-9492f0e4-f223-41c2-81a6-6f0cb6a14fe6-0')]}" + " AIMessage(content='Ciao!', response_metadata={'id': 'msg_01CpBD1cMCYvvPX2cogUawJj', 'model': 'claude-2.1', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 14, 'output_tokens': 7}}, id='run-6ef2fea6-9bfa-4266-bd05-263160a1db7b-0', usage_metadata={'input_tokens': 14, 'output_tokens': 7, 'total_tokens': 21})]}" ] }, - "execution_count": 20, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], - "source": ["config = {\"configurable\": {\"system_message\": \"respond in italian\"}}\napp.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)"] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5c5f7f4-4b0e-4cde-93a6-c1c6329b8591", - "metadata": {}, - "outputs": [], - "source": [""] + "source": [ + "config = {\"configurable\": {\"system_message\": \"respond in italian\"}}\n", + "app.invoke({\"messages\": [HumanMessage(content=\"hi\")]}, config=config)" + ] } ], "metadata": { @@ -207,7 +295,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, From 2645eb5a4844cdc5df224c1a971b81b3f11f7479 Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Wed, 28 Aug 2024 11:54:52 -0700 Subject: [PATCH 165/287] [docs]: remove agent executor (#1485) --- .../agent-simulation-evaluation.ipynb | 8 - ...angsmith-agent-simulation-evaluation.ipynb | 8 - .../cloud_examples/configuration_cloud.ipynb | 8 - examples/cloud_examples/same-thread.ipynb | 8 - examples/create-react-agent-hitl.ipynb | 8 - examples/create-react-agent-memory.ipynb | 8 - .../create-react-agent-system-prompt.ipynb | 8 - examples/extraction/retries.ipynb | 8 - .../human_in_the_loop/edit-graph-state.ipynb | 8 - .../human_in_the_loop/wait-user-input.ipynb | 12 +- examples/input_output_schema.ipynb | 8 - examples/map-reduce.ipynb | 8 - .../add-summary-conversation-history.ipynb | 8 - examples/memory/delete-messages.ipynb | 10 +- .../memory/manage-conversation-history.ipynb | 8 - examples/multi_agent/agent_supervisor.ipynb | 215 +++-- .../hierarchical_agent_teams.ipynb | 770 +++++++++++++++++- .../multi-agent-collaboration.ipynb | 40 +- examples/pass_private_state.ipynb | 8 - examples/persistence.ipynb | 8 - examples/rag/langgraph_agentic_rag.ipynb | 8 - examples/self-discover/self-discover.ipynb | 8 - examples/state-context-key.ipynb | 8 - examples/state-model.ipynb | 8 - examples/stream-multiple.ipynb | 10 +- examples/stream-updates.ipynb | 10 +- examples/streaming-content.ipynb | 8 - examples/web-navigation/web_voyager.ipynb | 8 - 28 files changed, 917 insertions(+), 318 deletions(-) diff --git a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb index 02f8782eb..266da1ce9 100644 --- a/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb +++ b/examples/chatbot-simulation-evaluation/agent-simulation-evaluation.ipynb @@ -399,14 +399,6 @@ " print(chunk)\n", " print(\"----\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dde4f2b5-cfe8-4ff0-99ea-fe2c5fed70c0", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb b/examples/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb index 47b0e0a2d..d0aaf00ee 100644 --- a/examples/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb +++ b/examples/chatbot-simulation-evaluation/langsmith-agent-simulation-evaluation.ipynb @@ -359,14 +359,6 @@ " evaluation=evaluation,\n", ")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "26735ed2-766d-4e0a-a185-b2295a0615b8", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/cloud_examples/configuration_cloud.ipynb b/examples/cloud_examples/configuration_cloud.ipynb index e8f00027f..e6d29170b 100644 --- a/examples/cloud_examples/configuration_cloud.ipynb +++ b/examples/cloud_examples/configuration_cloud.ipynb @@ -172,14 +172,6 @@ "):\n", " print(event)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "666d78f1-019a-433e-839e-52d2ebb3d9c8", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/cloud_examples/same-thread.ipynb b/examples/cloud_examples/same-thread.ipynb index 30410566e..0fe171bef 100644 --- a/examples/cloud_examples/same-thread.ipynb +++ b/examples/cloud_examples/same-thread.ipynb @@ -166,14 +166,6 @@ "):\n", " print(event)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4c26df68-c447-4a88-bc94-59df42b117b5", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/create-react-agent-hitl.ipynb b/examples/create-react-agent-hitl.ipynb index 2bc6d306f..7a14da497 100644 --- a/examples/create-react-agent-hitl.ipynb +++ b/examples/create-react-agent-hitl.ipynb @@ -213,14 +213,6 @@ "source": [ "print_stream(graph.stream(None, config, stream_mode=\"values\"))" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6f6f8965-b016-4e25-be63-31c00fc0a6de", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/create-react-agent-memory.ipynb b/examples/create-react-agent-memory.ipynb index 2611bda69..fbafb7c91 100644 --- a/examples/create-react-agent-memory.ipynb +++ b/examples/create-react-agent-memory.ipynb @@ -221,14 +221,6 @@ "inputs = {\"messages\": [(\"user\", \"What's it known for?\")]}\n", "print_stream(graph.stream(inputs, config=config, stream_mode=\"values\"))" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3decf001-7228-4ed5-8779-2b9ed98a74ea", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/create-react-agent-system-prompt.ipynb b/examples/create-react-agent-system-prompt.ipynb index c258a74c5..b3bd0213e 100644 --- a/examples/create-react-agent-system-prompt.ipynb +++ b/examples/create-react-agent-system-prompt.ipynb @@ -173,14 +173,6 @@ "\n", "print_stream(graph.stream(inputs, stream_mode=\"values\"))" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3decf001-7228-4ed5-8779-2b9ed98a74ea", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/extraction/retries.ipynb b/examples/extraction/retries.ipynb index 3e0dcce31..4ebf0c913 100644 --- a/examples/extraction/retries.ipynb +++ b/examples/extraction/retries.ipynb @@ -1010,14 +1010,6 @@ "\n", "If you notice high retry rates (using an observability tool like LangSmith), you can set up a rule to send the failure cases to a dataset alongside the corrected values and then automatically program those into your prompts or schemas (or use them as few-shots to have semantically relevant demonstrations)." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0ae295b1-da58-4cc9-834b-70e1466f8695", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/human_in_the_loop/edit-graph-state.ipynb b/examples/human_in_the_loop/edit-graph-state.ipynb index 7f29740a3..17468b017 100644 --- a/examples/human_in_the_loop/edit-graph-state.ipynb +++ b/examples/human_in_the_loop/edit-graph-state.ipynb @@ -541,14 +541,6 @@ "for event in app.stream(None, thread, stream_mode=\"values\"):\n", " event[\"messages\"][-1].pretty_print()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "78780afe-409d-46cd-a734-e82538cdd8de", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/human_in_the_loop/wait-user-input.ipynb b/examples/human_in_the_loop/wait-user-input.ipynb index eda7f0e6b..5b4ea5e86 100644 --- a/examples/human_in_the_loop/wait-user-input.ipynb +++ b/examples/human_in_the_loop/wait-user-input.ipynb @@ -55,7 +55,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "ANTHROPIC_API_KEY: ········\n" @@ -227,7 +227,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "Tell me how you want to update the state: go to step 3!\n" @@ -636,14 +636,6 @@ "for event in app.stream(None, config, stream_mode=\"values\"):\n", " event[\"messages\"][-1].pretty_print()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f6f972d1-3d99-4fc1-8b33-92b71e74835d", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/input_output_schema.ipynb b/examples/input_output_schema.ipynb index 4837b40a8..36c926156 100644 --- a/examples/input_output_schema.ipynb +++ b/examples/input_output_schema.ipynb @@ -62,14 +62,6 @@ "source": [ "Notice that the output of invoke only includes the output schema." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b952a554-f2a4-4be3-81ab-2e08f0f441c2", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/map-reduce.ipynb b/examples/map-reduce.ipynb index 6fee482a4..cefecbc96 100644 --- a/examples/map-reduce.ipynb +++ b/examples/map-reduce.ipynb @@ -207,14 +207,6 @@ "for s in app.stream({\"topic\": \"animals\"}):\n", " print(s)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f28eaf56", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/memory/add-summary-conversation-history.ipynb b/examples/memory/add-summary-conversation-history.ipynb index 10584bc21..4b7f73955 100644 --- a/examples/memory/add-summary-conversation-history.ipynb +++ b/examples/memory/add-summary-conversation-history.ipynb @@ -508,14 +508,6 @@ "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"updates\"):\n", " print_update(event)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "67d26013-1362-4cee-b135-ab5c3c4eb3d0", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/memory/delete-messages.ipynb b/examples/memory/delete-messages.ipynb index dd4cab2eb..2cf6d1b5a 100644 --- a/examples/memory/delete-messages.ipynb +++ b/examples/memory/delete-messages.ipynb @@ -52,7 +52,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "ANTHROPIC_API_KEY: ········\n" @@ -466,14 +466,6 @@ "source": [ "Remember, when deleting messages you will want to make sure that the remaining message list is still valid. This message list **may actually not be** - this is because it currently starts with an AI message, which some models do not allow." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4d7222cd-5767-42f0-bc69-10615127eba5", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/memory/manage-conversation-history.ipynb b/examples/memory/manage-conversation-history.ipynb index 559112a4a..ac835673f 100644 --- a/examples/memory/manage-conversation-history.ipynb +++ b/examples/memory/manage-conversation-history.ipynb @@ -360,14 +360,6 @@ "- [How to filter messages](https://python.langchain.com/v0.2/docs/how_to/filter_messages/)\n", "- [How to trim messages](https://python.langchain.com/v0.2/docs/how_to/trim_messages/)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "686861bb-ec32-46f3-b7b3-fdac106f22f6", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/multi_agent/agent_supervisor.ipynb b/examples/multi_agent/agent_supervisor.ipynb index c6626d755..bd2a7c6ce 100644 --- a/examples/multi_agent/agent_supervisor.ipynb +++ b/examples/multi_agent/agent_supervisor.ipynb @@ -26,7 +26,10 @@ "id": "0d30b6f7-3bec-4d9f-af50-43dfdc81ae6c", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install -U langgraph langchain langchain_openai langchain_experimental langsmith pandas"] + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langgraph langchain langchain_openai langchain_experimental langsmith pandas" + ] }, { "cell_type": "code", @@ -34,7 +37,24 @@ "id": "30c2f3de-c730-4aec-85a6-af2c2f058803", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")\n\n# Optional, add tracing in LangSmith\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_if_undefined(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n", + "\n", + "\n", + "_set_if_undefined(\"OPENAI_API_KEY\")\n", + "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", + "_set_if_undefined(\"TAVILY_API_KEY\")\n", + "\n", + "# Optional, add tracing in LangSmith\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\"" + ] }, { "cell_type": "markdown", @@ -48,45 +68,51 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "f04c6778-403b-4b49-9b93-678e910d5cec", "metadata": {}, "outputs": [], - "source": ["from typing import Annotated\n\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_experimental.tools import PythonREPLTool\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n# This executes code locally, which can be unsafe\npython_repl_tool = PythonREPLTool()"] + "source": [ + "from typing import Annotated\n", + "\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_experimental.tools import PythonREPLTool\n", + "\n", + "tavily_tool = TavilySearchResults(max_results=5)\n", + "\n", + "# This executes code locally, which can be unsafe\n", + "python_repl_tool = PythonREPLTool()" + ] }, { "cell_type": "markdown", "id": "d58d1e85-22d4-4c22-9062-72a346a0d709", "metadata": {}, "source": [ - "## Helper Utilities\n", - "\n", - "Define a helper function below, which make it easier to add new agent worker nodes." + "## Helper Utilities" ] }, - { - "cell_type": "code", - "execution_count": 3, - "id": "c4823dd9-26bd-4e1a-8117-b97b2860211a", - "metadata": {}, - "outputs": [], - "source": ["from langchain.agents import AgentExecutor, create_openai_tools_agent\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai import ChatOpenAI\n\n\ndef create_agent(llm: ChatOpenAI, tools: list, system_prompt: str):\n # Each worker node will be given a name and some tools.\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n system_prompt,\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n ]\n )\n agent = create_openai_tools_agent(llm, tools, prompt)\n executor = AgentExecutor(agent=agent, tools=tools)\n return executor"] - }, { "cell_type": "markdown", "id": "b7c302b0-cd57-4913-986f-5dc7d6d77386", "metadata": {}, "source": [ - "We can also define a function that we will use to be the nodes in the graph - it takes care of converting the agent response to a human message. This is important because that is how we will add it the global state of the graph" + "Define a helper function that we will use to create the nodes in the graph - it takes care of converting the agent response to a human message. This is important because that is how we will add it the global state of the graph" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "id": "80862241-a1a7-4726-bce5-f867b233832e", "metadata": {}, "outputs": [], - "source": ["def agent_node(state, agent, name):\n result = agent.invoke(state)\n return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}"] + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "def agent_node(state, agent, name):\n", + " result = agent.invoke(state)\n", + " return {\"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]}" + ] }, { "cell_type": "markdown", @@ -100,11 +126,53 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 13, "id": "311f0a58-b425-4496-adac-dc4cd8ffb912", "metadata": {}, "outputs": [], - "source": ["from langchain_core.output_parsers.openai_functions import JsonOutputFunctionsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nmembers = [\"Researcher\", \"Coder\"]\nsystem_prompt = (\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: {members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\"\n)\n# Our team supervisor is an LLM node. It just picks the next agent to process\n# and decides when the work is completed\noptions = [\"FINISH\"] + members\n# Using openai function calling can make output parsing easier for us\nfunction_def = {\n \"name\": \"route\",\n \"description\": \"Select the next role.\",\n \"parameters\": {\n \"title\": \"routeSchema\",\n \"type\": \"object\",\n \"properties\": {\n \"next\": {\n \"title\": \"Next\",\n \"anyOf\": [\n {\"enum\": options},\n ],\n }\n },\n \"required\": [\"next\"],\n },\n}\nprompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"system\",\n \"Given the conversation above, who should act next?\"\n \" Or should we FINISH? Select one of: {options}\",\n ),\n ]\n).partial(options=str(options), members=\", \".join(members))\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsupervisor_chain = (\n prompt\n | llm.bind_functions(functions=[function_def], function_call=\"route\")\n | JsonOutputFunctionsParser()\n)"] + "source": [ + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_openai import ChatOpenAI\n", + "from pydantic import BaseModel\n", + "from typing import Literal\n", + "\n", + "members = [\"Researcher\", \"Coder\"]\n", + "system_prompt = (\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following workers: {members}. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\"\n", + ")\n", + "# Our team supervisor is an LLM node. It just picks the next agent to process\n", + "# and decides when the work is completed\n", + "options = [\"FINISH\"] + members\n", + "\n", + "class routeResponse(BaseModel):\n", + " next: Literal[*options]\n", + "\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system_prompt),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " (\n", + " \"system\",\n", + " \"Given the conversation above, who should act next?\"\n", + " \" Or should we FINISH? Select one of: {options}\",\n", + " ),\n", + " ]\n", + ").partial(options=str(options), members=\", \".join(members))\n", + "\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\")\n", + "\n", + "def supervisor_agent(state):\n", + " supervisor_chain = (\n", + " prompt\n", + " | llm.with_structured_output(routeResponse)\n", + " )\n", + " return supervisor_chain.invoke(state)" + ] }, { "cell_type": "markdown", @@ -118,11 +186,41 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 14, "id": "6a430af7-8fce-4e66-ba9e-d940c1bc48e8", "metadata": {}, "outputs": [], - "source": ["import functools\nimport operator\nfrom typing import Sequence, TypedDict\n\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n\nfrom langgraph.graph import END, StateGraph, START\n\n\n# The agent state is the input to each node in the graph\nclass AgentState(TypedDict):\n # The annotation tells the graph that new messages will always\n # be added to the current states\n messages: Annotated[Sequence[BaseMessage], operator.add]\n # The 'next' field indicates where to route to next\n next: str\n\n\nresearch_agent = create_agent(llm, [tavily_tool], \"You are a web researcher.\")\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n\n# NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION. PROCEED WITH CAUTION\ncode_agent = create_agent(\n llm,\n [python_repl_tool],\n \"You may generate safe python code to analyze data and generate charts using matplotlib.\",\n)\ncode_node = functools.partial(agent_node, agent=code_agent, name=\"Coder\")\n\nworkflow = StateGraph(AgentState)\nworkflow.add_node(\"Researcher\", research_node)\nworkflow.add_node(\"Coder\", code_node)\nworkflow.add_node(\"supervisor\", supervisor_chain)"] + "source": [ + "import functools\n", + "import operator\n", + "from typing import Sequence, TypedDict\n", + "\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "from langgraph.prebuilt import create_react_agent\n", + "\n", + "# The agent state is the input to each node in the graph\n", + "class AgentState(TypedDict):\n", + " # The annotation tells the graph that new messages will always\n", + " # be added to the current states\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]\n", + " # The 'next' field indicates where to route to next\n", + " next: str\n", + "\n", + "\n", + "research_agent = create_react_agent(llm, tools=[tavily_tool])\n", + "research_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n", + "\n", + "# NOTE: THIS PERFORMS ARBITRARY CODE EXECUTION. PROCEED WITH CAUTION\n", + "code_agent = create_react_agent(llm, tools=[python_repl_tool])\n", + "code_node = functools.partial(agent_node, agent=code_agent, name=\"Coder\")\n", + "\n", + "workflow = StateGraph(AgentState)\n", + "workflow.add_node(\"Researcher\", research_node)\n", + "workflow.add_node(\"Coder\", code_node)\n", + "workflow.add_node(\"supervisor\", supervisor_chain)" + ] }, { "cell_type": "markdown", @@ -134,11 +232,24 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 15, "id": "14778e86-077b-4e6a-893c-400e59b0cdbf", "metadata": {}, "outputs": [], - "source": ["for member in members:\n # We want our workers to ALWAYS \"report back\" to the supervisor when done\n workflow.add_edge(member, \"supervisor\")\n# The supervisor populates the \"next\" field in the graph state\n# which routes to a node or finishes\nconditional_map = {k: k for k in members}\nconditional_map[\"FINISH\"] = END\nworkflow.add_conditional_edges(\"supervisor\", lambda x: x[\"next\"], conditional_map)\n# Finally, add entrypoint\nworkflow.add_edge(START, \"supervisor\")\n\ngraph = workflow.compile()"] + "source": [ + "for member in members:\n", + " # We want our workers to ALWAYS \"report back\" to the supervisor when done\n", + " workflow.add_edge(member, \"supervisor\")\n", + "# The supervisor populates the \"next\" field in the graph state\n", + "# which routes to a node or finishes\n", + "conditional_map = {k: k for k in members}\n", + "conditional_map[\"FINISH\"] = END\n", + "workflow.add_conditional_edges(\"supervisor\", lambda x: x[\"next\"], conditional_map)\n", + "# Finally, add entrypoint\n", + "workflow.add_edge(START, \"supervisor\")\n", + "\n", + "graph = workflow.compile()" + ] }, { "cell_type": "markdown", @@ -152,7 +263,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 16, "id": "56ba78e9-d9c1-457c-a073-d606d5d3e013", "metadata": {}, "outputs": [ @@ -161,32 +272,30 @@ "output_type": "stream", "text": [ "{'supervisor': {'next': 'Coder'}}\n", - "----\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Python REPL can execute arbitrary code. Use with caution.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'Coder': {'messages': [HumanMessage(content=\"The code `print('Hello, World!')` was executed, and the output is:\\n\\n```\\nHello, World!\\n```\", name='Coder')]}}\n", + "----\n", + "{'Coder': {'messages': [HumanMessage(content='The code to print \"Hello, World!\" to the terminal is:\\n\\n```python\\nprint(\\'Hello, World!\\')\\n```\\n\\nWhen executed, it prints:\\n```\\nHello, World!\\n```', name='Coder')]}}\n", "----\n", "{'supervisor': {'next': 'FINISH'}}\n", "----\n" ] } ], - "source": ["for s in graph.stream(\n {\n \"messages\": [\n HumanMessage(content=\"Code hello world and print it to the terminal\")\n ]\n }\n):\n if \"__end__\" not in s:\n print(s)\n print(\"----\")"] + "source": [ + "for s in graph.stream(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(content=\"Code hello world and print it to the terminal\")\n", + " ]\n", + " }\n", + "):\n", + " if \"__end__\" not in s:\n", + " print(s)\n", + " print(\"----\")" + ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "id": "45a92dfd-0e11-47f5-aad4-b68d24990e34", "metadata": {}, "outputs": [ @@ -196,22 +305,22 @@ "text": [ "{'supervisor': {'next': 'Researcher'}}\n", "----\n", - "{'Researcher': {'messages': [HumanMessage(content='**Research Report on Pikas**\\n\\nPikas are small mammals related to rabbits, known for their distinctive chirping sounds. They inhabit some of the most challenging environments, particularly boulder fields at high elevations, such as those found along the treeless slopes of the Southern Rockies, where they can be found at altitudes of up to 14,000 feet. Pikas are well-adapted to cold climates and typically do not fare well in warmer temperatures.\\n\\nRecent studies have shown that pikas are being impacted by climate change. Research by Peter Billman, a Ph.D. student from the University of Connecticut, indicates that pikas have moved upslope by approximately 1,160 feet. This upslope retreat is a direct response to changing climatic conditions, as pikas seek cooler temperatures at higher elevations.\\n\\nPikas are also known to be industrious foragers, particularly during the summer months when they gather vegetation to create haypiles for winter sustenance. Their behavior is encapsulated in the saying, \"making hay while the sun shines,\" reflecting their proactive approach to survival in harsh conditions.\\n\\nThe effects of climate change on pikas are not limited to the Southern Rockies. Studies published in Global Change Biology suggest that climate change is influencing pikas even in areas where they were previously thought to be less vulnerable, such as the Northern Rockies. These findings point to a broader trend of pikas moving to higher elevations, a behavior that may indicate a search for cooler, more suitable habitats.\\n\\nMoreover, researchers are exploring the possibility that pikas at lower elevations may have developed warm adaptations that could be beneficial for their future survival, given the ongoing climatic shifts. This line of research could help conservationists understand how pikas might cope with a warming world.\\n\\nIn conclusion, pikas are a species that not only fascinate with their unique behaviors and adaptations but also serve as indicators of environmental changes. Their upslope migration in response to climate change highlights the urgency for understanding and mitigating the effects of global warming on mountain ecosystems and the species that inhabit them.\\n\\n**Sources:**\\n- [Colorado Sun](https://coloradosun.com/2023/08/27/colorado-pika-population-climate-change/)\\n- [Wildlife.org](https://wildlife.org/climate-change-affects-pikas-even-in-unlikely-areas/)', name='Researcher')]}}\n", + "{'Researcher': {'messages': [HumanMessage(content='# Research Report on Pikas\\n\\nPikas, belonging to the genus Ochotona, are small, short-legged, and virtually tailless mammals that are often found in the mountains of western North America and across much of Asia. Despite their rodent-like appearance, pikas are not rodents but rather are part of the order Lagomorpha, which also includes rabbits and hares.\\n\\n## Behavior and Ecology\\nPikas are known for their unique behavior of not hibernating and remaining active throughout the winter. They navigate through tunnels under rocks and snow and rely on dried plants, which they have stored during warmer months in caches known as \"haypiles.\" This foraging strategy, termed \"haying,\" is crucial for their survival during the harsh winter months.\\n\\nPikas have a preference for cooler temperatures, typically foraging in temperatures below 25°C (77°F). They tend to avoid direct sunlight and stay in shaded regions when it gets warmer. A study has shown that for every 1°C (1.8°F) increase in ambient temperature, pikas can lose 3% of their foraging time, making them sensitive to climate change.\\n\\n## Distribution and Habitat\\nThe American pika (Ochotona princeps) and its relative, the collared pika (O. collaris), are found throughout the high mountainous regions of western North America. These species prefer cooler climates and have been observed to retreat to higher elevations as a response to increasing temperatures. Their current distribution is believed to be a result of a retreat from much larger ranges they occupied in the past, which included Western Europe and Eastern North America.\\n\\n## Conservation Status\\nThe International Union for Conservation of Nature and Natural Resources (IUCN) lists the American pika as a species of Least Concern but notes that populations are declining and unlikely to rebound due to habitat loss from extreme temperatures. The sensitivity of pikas to summer heat makes them an indicator species for the potential effects of climate change. Studies have shown that some populations are in decline, and there have been cases of local extirpation, particularly in the Great Basin.\\n\\n## Human Impact\\nHuman activity has impacted the ecosystems where pikas live, with recorded interactions dating back to the 1970s. Such interactions have been linked to pikas having reduced foraging time, limiting the amount of food they can stockpile for winter. Additionally, pikas have been considered pests in regions like the Tibetan plateau, where high densities of burrowing pikas are thought to reduce forage for domestic livestock and damage grasslands.\\n\\n## Conclusion\\nPikas are fascinating creatures with distinct adaptations that allow them to thrive in alpine environments. However, their future is uncertain due to the looming threats of climate change and habitat alteration. Conservation efforts, research, and monitoring are vital to ensure the survival of these unique mammals in a changing world.\\n\\n---\\n\\n**Sources:**\\n- [Wikipedia - Pika](https://en.wikipedia.org/wiki/Pika)\\n- [Treehugger - American Pika](https://www.treehugger.com/surprising-facts-about-american-pika-4864528)\\n- [National Park Service - Pikas at Rocky Mountain National Park](https://www.nps.gov/romo/learn/nature/pikas.htm)\\n- [Wikipedia - American Pika](https://en.wikipedia.org/wiki/American_pika)\\n- [Britannica - Pika](https://www.britannica.com/animal/pika)', name='Researcher')]}}\n", "----\n", "{'supervisor': {'next': 'FINISH'}}\n", "----\n" ] } ], - "source": ["for s in graph.stream(\n {\"messages\": [HumanMessage(content=\"Write a brief research report on pikas.\")]},\n {\"recursion_limit\": 100},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"----\")"] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1d363d2c-e0da-4cce-ba47-ad2aa9df0fef", - "metadata": {}, - "outputs": [], - "source": [""] + "source": [ + "for s in graph.stream(\n", + " {\"messages\": [HumanMessage(content=\"Write a brief research report on pikas.\")]},\n", + " {\"recursion_limit\": 100},\n", + "):\n", + " if \"__end__\" not in s:\n", + " print(s)\n", + " print(\"----\")" + ] } ], "metadata": { @@ -230,7 +339,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/multi_agent/hierarchical_agent_teams.ipynb b/examples/multi_agent/hierarchical_agent_teams.ipynb index e539c91f9..43b889cca 100644 --- a/examples/multi_agent/hierarchical_agent_teams.ipynb +++ b/examples/multi_agent/hierarchical_agent_teams.ipynb @@ -40,7 +40,10 @@ } }, "outputs": [], - "source": ["# %%capture --no-stderr\n# %pip install -U langgraph langchain langchain_openai langchain_experimental"] + "source": [ + "# %%capture --no-stderr\n", + "# %pip install -U langgraph langchain langchain_openai langchain_experimental" + ] }, { "cell_type": "code", @@ -53,7 +56,25 @@ } }, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_if_undefined(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n\n\n_set_if_undefined(\"OPENAI_API_KEY\")\n_set_if_undefined(\"LANGCHAIN_API_KEY\")\n_set_if_undefined(\"TAVILY_API_KEY\")\n\n# Optional, add tracing in LangSmith.\n# This will help you visualize and debug the control flow\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\nos.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\""] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_if_undefined(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"Please provide your {var}\")\n", + "\n", + "\n", + "_set_if_undefined(\"OPENAI_API_KEY\")\n", + "_set_if_undefined(\"LANGCHAIN_API_KEY\")\n", + "_set_if_undefined(\"TAVILY_API_KEY\")\n", + "\n", + "# Optional, add tracing in LangSmith.\n", + "# This will help you visualize and debug the control flow\n", + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_PROJECT\"] = \"Multi-agent Collaboration\"" + ] }, { "cell_type": "markdown", @@ -73,7 +94,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "id": "4024eb89-843d-4cc3-ab3f-e1eb4d031179", "metadata": { "ExecuteTime": { @@ -81,8 +102,37 @@ "start_time": "2024-05-15T08:19:42.397083Z" } }, - "outputs": [], - "source": ["from typing import Annotated, List\n\nfrom langchain_community.document_loaders import WebBaseLoader\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.tools import tool\n\ntavily_tool = TavilySearchResults(max_results=5)\n\n\n@tool\ndef scrape_webpages(urls: List[str]) -> str:\n \"\"\"Use requests and bs4 to scrape the provided web pages for detailed information.\"\"\"\n loader = WebBaseLoader(urls)\n docs = loader.load()\n return \"\\n\\n\".join(\n [\n f'\\n{doc.page_content}\\n'\n for doc in docs\n ]\n )"] + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "USER_AGENT environment variable not set, consider setting it to identify your requests.\n" + ] + } + ], + "source": [ + "from typing import Annotated, List\n", + "\n", + "from langchain_community.document_loaders import WebBaseLoader\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.tools import tool\n", + "\n", + "tavily_tool = TavilySearchResults(max_results=5)\n", + "\n", + "\n", + "@tool\n", + "def scrape_webpages(urls: List[str]) -> str:\n", + " \"\"\"Use requests and bs4 to scrape the provided web pages for detailed information.\"\"\"\n", + " loader = WebBaseLoader(urls)\n", + " docs = loader.load()\n", + " return \"\\n\\n\".join(\n", + " [\n", + " f'\\n{doc.page_content}\\n'\n", + " for doc in docs\n", + " ]\n", + " )" + ] }, { "cell_type": "markdown", @@ -99,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "id": "f20a18ca-2709-4c12-84f3-88678591a9fa", "metadata": { "ExecuteTime": { @@ -108,7 +158,99 @@ } }, "outputs": [], - "source": ["from pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom typing import Dict, Optional\n\nfrom langchain_experimental.utilities import PythonREPL\nfrom typing_extensions import TypedDict\n\n_TEMP_DIRECTORY = TemporaryDirectory()\nWORKING_DIRECTORY = Path(_TEMP_DIRECTORY.name)\n\n\n@tool\ndef create_outline(\n points: Annotated[List[str], \"List of main points or sections.\"],\n file_name: Annotated[str, \"File path to save the outline.\"],\n) -> Annotated[str, \"Path of the saved outline file.\"]:\n \"\"\"Create and save an outline.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n for i, point in enumerate(points):\n file.write(f\"{i + 1}. {point}\\n\")\n return f\"Outline saved to {file_name}\"\n\n\n@tool\ndef read_document(\n file_name: Annotated[str, \"File path to save the document.\"],\n start: Annotated[Optional[int], \"The start line. Default is 0\"] = None,\n end: Annotated[Optional[int], \"The end line. Default is None\"] = None,\n) -> str:\n \"\"\"Read the specified document.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n lines = file.readlines()\n if start is not None:\n start = 0\n return \"\\n\".join(lines[start:end])\n\n\n@tool\ndef write_document(\n content: Annotated[str, \"Text content to be written into the document.\"],\n file_name: Annotated[str, \"File path to save the document.\"],\n) -> Annotated[str, \"Path of the saved document file.\"]:\n \"\"\"Create and save a text document.\"\"\"\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n file.write(content)\n return f\"Document saved to {file_name}\"\n\n\n@tool\ndef edit_document(\n file_name: Annotated[str, \"Path of the document to be edited.\"],\n inserts: Annotated[\n Dict[int, str],\n \"Dictionary where key is the line number (1-indexed) and value is the text to be inserted at that line.\",\n ],\n) -> Annotated[str, \"Path of the edited document file.\"]:\n \"\"\"Edit a document by inserting text at specific line numbers.\"\"\"\n\n with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n lines = file.readlines()\n\n sorted_inserts = sorted(inserts.items())\n\n for line_number, text in sorted_inserts:\n if 1 <= line_number <= len(lines) + 1:\n lines.insert(line_number - 1, text + \"\\n\")\n else:\n return f\"Error: Line number {line_number} is out of range.\"\n\n with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n file.writelines(lines)\n\n return f\"Document edited and saved to {file_name}\"\n\n\n# Warning: This executes code locally, which can be unsafe when not sandboxed\n\nrepl = PythonREPL()\n\n\n@tool\ndef python_repl(\n code: Annotated[str, \"The python code to execute to generate your chart.\"],\n):\n \"\"\"Use this to execute python code. If you want to see the output of a value,\n you should print it out with `print(...)`. This is visible to the user.\"\"\"\n try:\n result = repl.run(code)\n except BaseException as e:\n return f\"Failed to execute. Error: {repr(e)}\"\n return f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\""] + "source": [ + "from pathlib import Path\n", + "from tempfile import TemporaryDirectory\n", + "from typing import Dict, Optional\n", + "\n", + "from langchain_experimental.utilities import PythonREPL\n", + "from typing_extensions import TypedDict\n", + "\n", + "_TEMP_DIRECTORY = TemporaryDirectory()\n", + "WORKING_DIRECTORY = Path(_TEMP_DIRECTORY.name)\n", + "\n", + "\n", + "@tool\n", + "def create_outline(\n", + " points: Annotated[List[str], \"List of main points or sections.\"],\n", + " file_name: Annotated[str, \"File path to save the outline.\"],\n", + ") -> Annotated[str, \"Path of the saved outline file.\"]:\n", + " \"\"\"Create and save an outline.\"\"\"\n", + " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", + " for i, point in enumerate(points):\n", + " file.write(f\"{i + 1}. {point}\\n\")\n", + " return f\"Outline saved to {file_name}\"\n", + "\n", + "\n", + "@tool\n", + "def read_document(\n", + " file_name: Annotated[str, \"File path to save the document.\"],\n", + " start: Annotated[Optional[int], \"The start line. Default is 0\"] = None,\n", + " end: Annotated[Optional[int], \"The end line. Default is None\"] = None,\n", + ") -> str:\n", + " \"\"\"Read the specified document.\"\"\"\n", + " with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n", + " lines = file.readlines()\n", + " if start is not None:\n", + " start = 0\n", + " return \"\\n\".join(lines[start:end])\n", + "\n", + "\n", + "@tool\n", + "def write_document(\n", + " content: Annotated[str, \"Text content to be written into the document.\"],\n", + " file_name: Annotated[str, \"File path to save the document.\"],\n", + ") -> Annotated[str, \"Path of the saved document file.\"]:\n", + " \"\"\"Create and save a text document.\"\"\"\n", + " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", + " file.write(content)\n", + " return f\"Document saved to {file_name}\"\n", + "\n", + "\n", + "@tool\n", + "def edit_document(\n", + " file_name: Annotated[str, \"Path of the document to be edited.\"],\n", + " inserts: Annotated[\n", + " Dict[int, str],\n", + " \"Dictionary where key is the line number (1-indexed) and value is the text to be inserted at that line.\",\n", + " ],\n", + ") -> Annotated[str, \"Path of the edited document file.\"]:\n", + " \"\"\"Edit a document by inserting text at specific line numbers.\"\"\"\n", + "\n", + " with (WORKING_DIRECTORY / file_name).open(\"r\") as file:\n", + " lines = file.readlines()\n", + "\n", + " sorted_inserts = sorted(inserts.items())\n", + "\n", + " for line_number, text in sorted_inserts:\n", + " if 1 <= line_number <= len(lines) + 1:\n", + " lines.insert(line_number - 1, text + \"\\n\")\n", + " else:\n", + " return f\"Error: Line number {line_number} is out of range.\"\n", + "\n", + " with (WORKING_DIRECTORY / file_name).open(\"w\") as file:\n", + " file.writelines(lines)\n", + "\n", + " return f\"Document edited and saved to {file_name}\"\n", + "\n", + "\n", + "# Warning: This executes code locally, which can be unsafe when not sandboxed\n", + "\n", + "repl = PythonREPL()\n", + "\n", + "\n", + "@tool\n", + "def python_repl(\n", + " code: Annotated[str, \"The python code to execute to generate your chart.\"],\n", + "):\n", + " \"\"\"Use this to execute python code. If you want to see the output of a value,\n", + " you should print it out with `print(...)`. This is visible to the user.\"\"\"\n", + " try:\n", + " result = repl.run(code)\n", + " except BaseException as e:\n", + " return f\"Failed to execute. Error: {repr(e)}\"\n", + " return f\"Successfully executed:\\n```python\\n{code}\\n```\\nStdout: {result}\"" + ] }, { "cell_type": "markdown", @@ -127,7 +269,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "id": "e09fb60f-1aac-455b-b67d-8d2e4ccfd747", "metadata": { "ExecuteTime": { @@ -136,7 +278,58 @@ } }, "outputs": [], - "source": ["from typing import List, Optional\n\nfrom langchain.agents import AgentExecutor, create_openai_functions_agent\nfrom langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_openai import ChatOpenAI\n\nfrom langgraph.graph import END, StateGraph, START\n\n\ndef create_agent(\n llm: ChatOpenAI,\n tools: list,\n system_prompt: str,\n) -> str:\n \"\"\"Create a function-calling agent and add it to the graph.\"\"\"\n system_prompt += \"\\nWork autonomously according to your specialty, using the tools available to you.\"\n \" Do not ask for clarification.\"\n \" Your other team members (and other teams) will collaborate with you with their own specialties.\"\n \" You are chosen for a reason! You are one of the following team members: {team_members}.\"\n prompt = ChatPromptTemplate.from_messages(\n [\n (\n \"system\",\n system_prompt,\n ),\n MessagesPlaceholder(variable_name=\"messages\"),\n MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n ]\n )\n agent = create_openai_functions_agent(llm, tools, prompt)\n executor = AgentExecutor(agent=agent, tools=tools)\n return executor\n\n\ndef agent_node(state, agent, name):\n result = agent.invoke(state)\n return {\"messages\": [HumanMessage(content=result[\"output\"], name=name)]}\n\n\ndef create_team_supervisor(llm: ChatOpenAI, system_prompt, members) -> str:\n \"\"\"An LLM-based router.\"\"\"\n options = [\"FINISH\"] + members\n function_def = {\n \"name\": \"route\",\n \"description\": \"Select the next role.\",\n \"parameters\": {\n \"title\": \"routeSchema\",\n \"type\": \"object\",\n \"properties\": {\n \"next\": {\n \"title\": \"Next\",\n \"anyOf\": [\n {\"enum\": options},\n ],\n },\n },\n \"required\": [\"next\"],\n },\n }\n prompt = ChatPromptTemplate.from_messages(\n [\n (\"system\", system_prompt),\n MessagesPlaceholder(variable_name=\"messages\"),\n (\n \"system\",\n \"Given the conversation above, who should act next?\"\n \" Or should we FINISH? Select one of: {options}\",\n ),\n ]\n ).partial(options=str(options), team_members=\", \".join(members))\n return (\n prompt\n | llm.bind_functions(functions=[function_def], function_call=\"route\")\n | JsonOutputFunctionsParser()\n )"] + "source": [ + "from typing import List, Optional\n", + "from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser\n", + "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "from langchain_core.messages import HumanMessage\n", + "\n", + "\n", + "def agent_node(state, agent, name):\n", + " result = agent.invoke(state)\n", + " return {\"messages\": [HumanMessage(content=result[\"messages\"][-1].content, name=name)]}\n", + "\n", + "\n", + "def create_team_supervisor(llm: ChatOpenAI, system_prompt, members) -> str:\n", + " \"\"\"An LLM-based router.\"\"\"\n", + " options = [\"FINISH\"] + members\n", + " function_def = {\n", + " \"name\": \"route\",\n", + " \"description\": \"Select the next role.\",\n", + " \"parameters\": {\n", + " \"title\": \"routeSchema\",\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"next\": {\n", + " \"title\": \"Next\",\n", + " \"anyOf\": [\n", + " {\"enum\": options},\n", + " ],\n", + " },\n", + " },\n", + " \"required\": [\"next\"],\n", + " },\n", + " }\n", + " prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", system_prompt),\n", + " MessagesPlaceholder(variable_name=\"messages\"),\n", + " (\n", + " \"system\",\n", + " \"Given the conversation above, who should act next?\"\n", + " \" Or should we FINISH? Select one of: {options}\",\n", + " ),\n", + " ]\n", + " ).partial(options=str(options), team_members=\", \".join(members))\n", + " return (\n", + " prompt\n", + " | llm.bind_functions(functions=[function_def], function_call=\"route\")\n", + " | JsonOutputFunctionsParser()\n", + " )" + ] }, { "cell_type": "markdown", @@ -154,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "id": "53db0c78-e357-48ba-ae5f-3fc04735a3b7", "metadata": { "ExecuteTime": { @@ -163,7 +356,44 @@ } }, "outputs": [], - "source": ["import functools\nimport operator\n\nfrom langchain_core.messages import BaseMessage, HumanMessage\nfrom langchain_openai.chat_models import ChatOpenAI\n\n\n# ResearchTeam graph state\nclass ResearchTeamState(TypedDict):\n # A message is added after each team member finishes\n messages: Annotated[List[BaseMessage], operator.add]\n # The team members are tracked so they are aware of\n # the others' skill-sets\n team_members: List[str]\n # Used to route work. The supervisor calls a function\n # that will update this every time it makes a decision\n next: str\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsearch_agent = create_agent(\n llm,\n [tavily_tool],\n \"You are a research assistant who can search for up-to-date info using the tavily search engine.\",\n)\nsearch_node = functools.partial(agent_node, agent=search_agent, name=\"Search\")\n\nresearch_agent = create_agent(\n llm,\n [scrape_webpages],\n \"You are a research assistant who can scrape specified urls for more detailed information using the scrape_webpages function.\",\n)\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"WebScraper\")\n\nsupervisor_agent = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: Search, WebScraper. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"Search\", \"WebScraper\"],\n)"] + "source": [ + "import functools\n", + "import operator\n", + "\n", + "from langchain_core.messages import BaseMessage, HumanMessage\n", + "from langchain_openai.chat_models import ChatOpenAI\n", + "from langgraph.prebuilt import create_react_agent\n", + "\n", + "# ResearchTeam graph state\n", + "class ResearchTeamState(TypedDict):\n", + " # A message is added after each team member finishes\n", + " messages: Annotated[List[BaseMessage], operator.add]\n", + " # The team members are tracked so they are aware of\n", + " # the others' skill-sets\n", + " team_members: List[str]\n", + " # Used to route work. The supervisor calls a function\n", + " # that will update this every time it makes a decision\n", + " next: str\n", + "\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\")\n", + "\n", + "search_agent = create_react_agent(llm, tools=[tavily_tool])\n", + "search_node = functools.partial(agent_node, agent=search_agent, name=\"Search\")\n", + "\n", + "research_agent = create_react_agent(llm, tools=[scrape_webpages])\n", + "research_node = functools.partial(agent_node, agent=research_agent, name=\"WebScraper\")\n", + "\n", + "supervisor_agent = create_team_supervisor(\n", + " llm,\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following workers: Search, WebScraper. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\",\n", + " [\"Search\", \"WebScraper\"],\n", + ")" + ] }, { "cell_type": "markdown", @@ -175,7 +405,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "id": "1a7a1260-d9f6-4011-b2b1-13fab5126997", "metadata": { "ExecuteTime": { @@ -184,11 +414,42 @@ } }, "outputs": [], - "source": ["research_graph = StateGraph(ResearchTeamState)\nresearch_graph.add_node(\"Search\", search_node)\nresearch_graph.add_node(\"WebScraper\", research_node)\nresearch_graph.add_node(\"supervisor\", supervisor_agent)\n\n# Define the control flow\nresearch_graph.add_edge(\"Search\", \"supervisor\")\nresearch_graph.add_edge(\"WebScraper\", \"supervisor\")\nresearch_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\"Search\": \"Search\", \"WebScraper\": \"WebScraper\", \"FINISH\": END},\n)\n\n\nresearch_graph.add_edge(START, \"supervisor\")\nchain = research_graph.compile()\n\n\n# The following functions interoperate between the top level graph state\n# and the state of the research sub-graph\n# this makes it so that the states of each graph don't get intermixed\ndef enter_chain(message: str):\n results = {\n \"messages\": [HumanMessage(content=message)],\n }\n return results\n\n\nresearch_chain = enter_chain | chain"] + "source": [ + "research_graph = StateGraph(ResearchTeamState)\n", + "research_graph.add_node(\"Search\", search_node)\n", + "research_graph.add_node(\"WebScraper\", research_node)\n", + "research_graph.add_node(\"supervisor\", supervisor_agent)\n", + "\n", + "# Define the control flow\n", + "research_graph.add_edge(\"Search\", \"supervisor\")\n", + "research_graph.add_edge(\"WebScraper\", \"supervisor\")\n", + "research_graph.add_conditional_edges(\n", + " \"supervisor\",\n", + " lambda x: x[\"next\"],\n", + " {\"Search\": \"Search\", \"WebScraper\": \"WebScraper\", \"FINISH\": END},\n", + ")\n", + "\n", + "\n", + "research_graph.add_edge(START, \"supervisor\")\n", + "chain = research_graph.compile()\n", + "\n", + "\n", + "# The following functions interoperate between the top level graph state\n", + "# and the state of the research sub-graph\n", + "# this makes it so that the states of each graph don't get intermixed\n", + "def enter_chain(message: str):\n", + " results = {\n", + " \"messages\": [HumanMessage(content=message)],\n", + " }\n", + " return results\n", + "\n", + "\n", + "research_chain = enter_chain | chain" + ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "id": "110f59bed6134685", "metadata": { "ExecuteTime": { @@ -199,7 +460,7 @@ "outputs": [ { "data": { - "image/jpeg": "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L7HktlprlFSV1BHOHEU9ypn007NHEeNG8BzddNRqOwgrZrhmMJwdcbYFm8Lv/V5f+dmf5KmWEs3hd/6vL/zsz/JUy9/7G/iXf8f/AKpedl/8H4u7REX0D54REQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQYV6+89f/uJP4SouxRodilnaRqDRQgg/wC7ClqogbVU8sL9dkjSx2nmI0K4mn4RW2kp4oIbreo4YmhjGCt5NaBoB2LnynJ4yq1FvOwmJx7nfkmUU2Mc7ijL/R+4ZegGN/VcP/2r5/o/cMh//AMb+q4f/tUpfcqofli9+u/Yn3KqH5Yvfrv2LzfRlz13zduuWOnuhqYII6aCOGFjYoo2hjGMGga0DQADzL0Wy+5VQ/LF79d+xPuVUPyxe/XfsWfof+7HZLT0ha5S1qKNOi3S1vFjhc++3+93SS4C611JugqOrb1cU7mM5AdugHNS79yqh+WL3679ieh/7sdknpC1ylH+QcIcHyy6y3O9YjZbtcZQ0SVVZQRyyvAAA1c4EnQAD9i17uAPDRwaHYDjhDRo0G2Q8hrroPF85P8AzUofcqofli9+u/Yn3KqH5Yvfrv2LSPsuuNkXvmprtid9PdDl8axSy4bbjb7DaqOzUJeZe5qGBsMe86au2tAGp0HP5l0fC7/1eX/nZn+Spl6/cqofli9+u/Yt3i+J0eJQVkdJLUzmrn7omlqpese5+xrO3zbWNH7F25HkeqVV11V52dGHHnE8fc5spyqi7bzKYbtERdryxERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERBXfoIfATJ+frp/mnqxCrv0EPgJk/P10/wA09WIQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREFd+gh8BMn5+un+aerEKu/QQ+AmT8/XT/NPViEBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEXE3LiWx8rorFQOu5adrquSTqKXX8V+hL/wArGkeTXtWsOaZa7mKOyx/imSZ2n7dB/wBlvoZj8UxHvn6OmnJ7tcYxSklFGvhnl3xayfSmTwzy74tZPpTJoo6o7VtUvcklIo18M8u+LWT6UyeGeXfFrJ9KZNFHVHaape5JKRRr4Z5d8Wsn0pk8M8u+LWT6UyaKOqO01S9ySUqXf0mfAo5xw0pM+tdOH3jGNWVmxvjy0L3c/wAvVvId8zXSFWH8M8u+LWT6UyxbpkGSXq21dvr7fYauhq4X089PL1xZLG5pa5rh5QQSD+VNFHVHaape5P5m/wBH1wVn4q8e7bd5mPbZMTkju9TM3kDO12tNHr5zI3dp5WxvC/sMqx9HrhXWdHHEKyw4+y21grKx9ZPWVjpDK8nkxpIAGjWgAfPuP9oqUvDPLvi1k+lMmijqjtNUvcklIo18M8u+LWT6UyeGeXfFrJ9KZNFHVHaape5JKRRr4Z5d8Wsn0pk8M8u+LWT6UyaKOqO01S9ySUijXwzy74tZPpTJ4Z5d8Wsn0pk0UdUdpql7kkpFGozPLdRrTWXTy6OmWRTcRL1RvBudhhnp/wC1La6ovkb8/VyNbqPyOJ8wPlaLHdVE/HxROS3o25qQkWDZr3RZBQMrKCcTwOJbrtLXNcO1rmuALXDytIBHlCzljMTTOEuXdvERFAIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiIC4DPrq+63IY7E8to2wie4uY7QvDjpHD59rtHl3zBo5h5XfqJmPdLlWVvf8A1nfEM7OYaKeENH/Ln+1bW/uxVXG+I2dsR59rsySiK7u3gymMbG0NaA1oGgAGgAWih4gYvUZA6wxZJaJL41xa62MrojUgjtBi3bteXmWl45X244zwczW62lzo7lR2ipmglZ76NwjPjj52++/YsThJw0xHG8HxaW02e3SSRUUNRFc+52Onle+Mbput03Fz9ziXa89x8i5HvTVOdmw7i13ehvlGKu3VtPcKUvfGJ6WVsrC5jix7dzSRq1zXNI8hBB7Flqp3De8ZbgnDfHskpciZLYZsunts2PuoI9joZ7tNC9/Xf1nWB7y8EEN0ABaeZO7zri/k1hzSpulivl0vmOUF/prTX0veamjtkG+aOGWHukuE7pWl/vmBzQ7RpHamDKL0YYzCygka57mBwL2gEtB5gHs/7Fc7YOJeIZVXmhsmV2S8VrdSaaguMM8g07fFY4nkov4X2G6N6Q/Fisdk1c+jhrqIy2809P1U4fRNMbXO6veBGCA3a4E7Ru3EkmH+EWKXXiFw74aUFpwJ1tltV4ZcZ81qHU8YEMVVI94h2uMry8e56EAefkAVOCJuzswjn3TguoirJjvEnP6HgBBxCuOStu12uIZb6O3SUMEVJBLNWtp4qiRzGB7nNBJIDg066bdfGW3y3NM24WVeS2Svyk5JLNh1yvluuU1vggmo6qmaNRsjaGOjPWNcNzSQW6Eu1UYLaaMMcFhEVeKS58RKrMsGtEnEGRlPldkqLnUPitFKHUUkTYHbafVhG09eB7r1h0aeep1GHZOI+b5geG9nGS96K24XK+Wy6XGjoIHuqe4i9rJGska5rHHYCdBpqTy00CGmjlPnDxWSfI2Ju57gxuoGrjoNSdB/iuZyLijhmI3DuC+5dYrLXbBJ3LcblDTy7T2O2vcDodDz+ZV04jXnKMgtlTit1ySV9xxnO7JSMvFJSQMdVRzvglhe9hYWCSIya+KA0lo1aRqD3vSexampujxkU1w6u83enpqeN13qqWFtRIRURjUljGhvaeTQBz7ERN2ZiZpjclvGM4xzNop5Mdv9rv0dOQ2Z9srY6kRk9gcWOOhOh7fMt0q65dlWRt4oX7E8Etlxs9DZaSkmq5sZtdtllnnnD3M63uqWMCMNboNgJJ3auboNcm05PxLyvLcSxq6XR2D3KrxuruFyjpqOmnkE0NVHExzN/WMZva8OI1eACR2+MGCYu8MFgUVaMK4i56Mb4dZXdsoZdIb5kBsFbaRboYYdnWzwiZr2jeJN0IefG2+MQGjReNTxCz+1cM8n4jnLjVwWG+V0JsE1upmwVFJDWuh6syNYJA/YPFcHdoGoPMlgaaMMcFkqS70NfV1tLTVtPU1VE9sdVBFK1z4HOaHNa9oOrSWuDgDpqCD5VlqBbZkUGLZB0gLtUXZthjpq2jeLi+n6/udxttOGu6r+2dxGjPKSB5Vy1Hm+aXi38RsRv1wvTWHE33ihr7xa6OjrWNJkY9nVxFzCxwaNNzWvGrgRqAUwNLEb45rRRyNlY17HB7HAFrmnUEecL6q1QZZk3DHgJw2pLZc6+/3nJjb6KjkdSUhkoInUgkLIWe5RyFrYyG9a7Ul2ri7TQyDwbumfT3a80WWUV0daY4oZaC43qnoqerfIS4SxOZSSvYWjRhDtGnxnAg6AotTciZiMEkvuT8Tre/cJIgboLhEHaNkg/tSEfhsHjA9pALfKNJYDg4Aggg8wR5VF1TFHUU8sUoBiewtcD2EEc11/DeolrOHmLzzkumltdK97j2kmJpJXX+O1nTvicPhO7swl5eXURExXHF0aIixeWIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICjbLKF1ky99UQe4ru1vuhPisqWNDdp+d8Ybp/unfNrJKxLraqS92+ahroG1FLMNHxu1HYdQQRzBBAII5ggEEELSiqIxirdOxtZuTariqEb1dLDXUs1NUxMnp5mOjkikaHNe0jQtIPaCDpoozxXo+WnD7hQPt+S5ULNb5uvo8fkuzjQQEElrQ0ND3MGvJjnlvzFSrX4rkFieRSxDIKIEBha9sVUxvmcHEMkP4wLf7vlMT530lML4X342XLpq7Hrr1TZxTVtKQXxu10e1w1a9uocNWkjVrh2ghNBXP4cJ+PmXuxes3MJxbOLglY4sGosUFXcDbqS7C8slMkfWmYVhq9pOzTZ1hI00128tdea0186Nlhvhu0Tr7kVFbbhXm6m2UlaxlNBWGQSGdjTGTrvG/a8uZuOu3sXhi/Sw4dZrfqOyWG5VN2u1Y/q4KOkpHvkkOmp0A8gAJJ7AASeQUpd8K/wBHL16p9qavd5JmqzPGHNfcsooeIk+YUV2u9trKtsTa+hpZ2CkrjE0sjdKxzCdQ06atc3kBrqtlw9wWg4a4dbsbtk1TPQ0IeI5KtzXSnc9zzuLWtHa49gHLRbPvhX+jl69U+1O+Ff6OXr1T7U1e7yWi5aicYmHK0HBvHqPhU3h7MKmvsAhdDrUyDr+chkDt7A3RzXkEEAaFo8y1VJwAszafIe+d7v2QXC82mWxyXS61UclRT0cgIdHDtjaxupO7UtJJAJ10Xf8AfCv9HL16p9qd8K/0cvXqn2pq93kjOs84aKDhpbKe/wCLXds9WanHLdNbKRhe3Y+KQRBxkG3Uu9xboQQOZ5HlpGuS8AamPJsDprBcLxQ2mhuN4uNbdqWqhZVUslU1zwG7m6OaZHFumx2je3zqZ++Ff6OXr1T7U74V/o5evVPtTV7vJE1WauMef+ODZ0f8abgtwxp9TdZnV9c26VF5lq91wfWNe1zKjrduge0sZpo3aA3TTTVbfIOFtJlfDSowu8Xi7XGkqWhs1xmlj7sfpKJAS4RhnaAPedg/asvEuIVHndpNzsFuul1t4mkp+6IKXVvWMcWvbzPaCCFuu+Ff6OXr1T7U1e7yTn2ecOQzDg3bsqydmRUt5veMXw0wo562xVTYXVUIJLWStex7XbSTo7QOGvIrZ2/hvb7fldoyEVlwqLhbLO+yRmpnEokhc+N5fI4jc6TWJvjbuep1BJ1W874V/o5evVPtTvhX+jl69U+1NXu8jPs444w42h4JWOgxbGrBHV3B1HYLx36pXukj6x83Wyy7XnZoWbpnDQAHQDn2k8DgfRykrqGvGYXC+MopMirrmMbbXxm3VDTWPlgfIxjS4gjY8sLwNe1uuqnDvhX+jl69U+1O+Ff6OXr1T7U1e7yVmbM4bYcXkHAzHckueV1dVPcWR5PTRQXGkhqdsLpItgiqGDTVszRGwBwOnijUFfixcDrXaciqb5W3u+5Dcau2yWmrku9UyRtRTOcHBjmtjaG7SHabA337tdSdV2/fCv8ARy9eqfauV4g8YrJwqt1PX5bS3Sx0NRIYo6mpondWX6a7S4agEjUgHt0OnYU1e7yWzrOOOMNNT9HmxtwhmK1V6v8AcbdSywT2yWprG90Wl8OvUmmkYxpaWA6Au3HTkdQuuwnC3YbTVUcmQXrIpqh4e6pvdS2V7dBoGtDGMa0fMGjU8zqou/02eEfpIf3DlMGM191zqz266WG0PbbLjTx1VNcbjMyOJ8UjQ5j2taXPOrSDoWt117Rz0avc4xh8YRpLNG3GHreI56+FlroiRXXAmniLTzjaeT5fyMaS78ug7SFLFFSRW+jgpYG7III2xRt8zWjQD/kFpcWxGLHg+omm7vuszds1Y6MM8Xt2MbqdjAee3Unykk810CmqYimKKf8Av7PGym/pqtm6BERZOQREQEREBERAREQEREBERAREQEREBERAREQEREBF5VVXBQ00lRUzR09PG3c+WVwa1o85J5AKFcy6Z3CXD63vfFkrcnvDjtjtmMwuuM0jvwQY9WA/MXBBN6Ktv3a+OHEfxcD4QNxahf7y78QKzucgHz0kXuo/5lP9GviTxC8fiXxpvL6V/v7LhkLbXTgfgGUAvkb/AHgD86CWuIHG/AeFkbnZXl1pssjRu7mqKlpqHD8WJur3fsaVEp6ZE2bExcKeGGV8Qdx0juclP3strvMe6Jh/gWhdvgHRP4T8NZGz2fCrdLXg7jcLk01tSX+VwfMXFpP4uiloAAAAaAeQIK2+CfSU4mc7zl2N8KbXJ20mPUhuNdt/BfLKdjXfjRn9irZ0uegrlct4wutxCtyviPd7pLLQXa532tZU9zO8V0DidoMUXOfUkljdrRqC4bv6TIggLop9EjH+jZjgmIiuuaVsIbcbxpqAORMMGo1bGCBz7XEAnsa1s+oiAiIgIiICIiCu/QQ+AmT8/XT/ADT1YhV36CHwEyfn66f5p6sQgIiICIiAtPl2IWbPcbuFgyC3w3Wz18ZiqKScate3t8nMEEAgjQggEEELcIg/ltxE/o38hx7jpjdrsVPXXzhxernEye4Uz2NqbbTbt04mc4bWlsYeWPIIcQ1um5wabYf6MnErh14/DHjXeYqVnvLLmUTbnTEfgCUjdG3+63X51ZlEFZxxs468Njtzzg/HldAzk+8YBV9eSB5RSSe6Hz9rR/8ATpMM6a3CTL6zvfUZJ4K3hp2yW3J4HW+WN34LnSe56/MHlTouczLhxivESj7lyjHLXf4ANGtuNIyYs/ulwJafnGhQbyirae5UsVTSVEVVTSjdHNC8PY8ecEciF7qt9Z0HsXsVVLXcN8oyjhfXvO/ZY7lI+ke7/wCJBITuH4u4BeQp+k7wx/q6nFeMVrj/ALMze89zeB5iPcRy8p1KCyqKt1P027JjM0dLxOwrKuGFSTtNTc7e+poHHzMqIgd/Py7QFNGEcUsP4lUvdGLZNar/ABgbnCgq2SPYPxmA7mn5iAg6lERAREQEREBERAREQEREBERAREQEREFe730ybPV3iusuAYZlfEi7Uc76WZ1ptz4aKKVji1zZKiUAN0cCNdpHzrC29JriZ2uxXg7bJPI0d+bmwH/+g/4FZHQe+DTLP0xu/wDOCsSgrnS9CTG8gqY63iTleU8UK1p39Verk+KiY7zxwRFoaPm3EKacN4cYrw7ou5MYxy12CAjRzbdSMhL/AO8WgFx+c6ldGiAiIgIiICIiAiIgIiICIiAiIgrv0EPgJk/P10/zT1YhV36CHwEyfn66f5p6sQgIiICIiAiIgIiICIiAiIg856eKqhfDNGyaGQFr45GhzXA9oIPaFDGb9DbhJnFV3c/E4LFdgd8dyx57rfPG/wDCHVENLvnc0qa0QVtHAfjLw58bh9xlmvlEz3lmz+lFa1w8gNUwdaB5NGtCDpD8VuHnicR+C1yrKRnv7zgs7blE4eV3c5IkY0durndiskiDiOEvGTF+NmPVF4xarnqKelqXUdVFVUslPLTzhrXOje14HMB7TqNRz7V26rt0Qvvxx2/WPdP4YlYlAREQEREBERAREQEREBERAREQV26D3waZZ+mN3/nBWJVdug98GmWfpjd/5wViUBERAREQEREBERAREQERYFTfrZRyFlRcaSB4OhbJO1pH7CVMRNWyIGei1fhVZflig9ZZ7U8KrL8sUHrLPar6OvplOEtoq6dLLpY3Pouy2Gc4KMms11a9gr23U0vUzs5mJzeof2tIcDu56O5eLqZ38KrL8sUHrLPaov6SmB45x54O37FJLtbBXyx90W2eSpj9xq2amJ2uvIE6sJ/Be5NHX0yYSp70LOmhWW6447wrocBfdJrzfJpXXGO67TTxTzGSV/VdSdwiYXOPjDUMPYv6TL+e39GjwapMR7/cQcodDbbq577VbaatkbHJGxp93l2uOvNwDAeR8STyFX18KrL8sUHrLPamjr6ZMJbRFq/Cqy/LFB6yz2p4VWX5YoPWWe1NHX0yYS2iLWNyizOIDbvQknyCpZ7VsIpWTRh8b2yMd2OadQf2qs01U74Q/aIiqCIiAiIgIiICIiAiIgrt0Qvvxx2/WPdP4YlYlV26IX3447frHun8MSsSgIiICIiAiIgIiICIiAiIgIiIK7dB74NMs/TG7/zgrEqu3Qe+DTLP0xu/84KxKAiIgIiICIiAiIgLCvN3prFbZ66rc5sMQ1IY3c5xJ0a1o8riSAB5SQFmqO8+qzcMutdsJ1goqd1wkYfLI5xjiP5ABNy85afItLdMVTt3Rt8+/c1tW9JXFLWXWSvyx7pLtNLDROPudqgkLI2t8nWuadZHeca7B5AdNxxocYs9OwMitNDGwaANZTMA5dnkWyUMYjxD4icVNchxSkxqgwp9W+CkdeBUPrK6GOQsfO0xkNja4tdtBDidATpqqzeuTsicI5RufRRTRaiKaYSx4P2v5No/3DfYng/a/k2j/cN9iiyfpGWTGeI+bY9ldwprXSWeejZRyxU00jurlpo5Hvnc0Oaxoe/QPcGN05ak6ldZmfGnDeH1dBSX68GjllgbUgspJ5o2REkCR742OaxpLT4ziByKppLnVK2fRt2un8H7X8m0f7hvsTwftfybR/uG+xcBfuO9psHFWz4fLT1U8Nwtjq9tfSUdRUt3GWNkbR1UTgWEPcTJu2t0aDpuC1tTxBz3N8qyWhwCkx6K1Y7Vd7qitv8A17jWVYY18kUQiI2NYHtaXu3czybyTSV9U9pNdO6Eo+D9r+TaP9w32J4P2v5No/3DfYo2xvpG45V4rZ6/IeusV4rqmqt77THTzVcjKumcWzxN6pji7TTUcgSCOWuoXSw8ZcMnw+XKGX6DvLFOaWSV0b2yNn106kwlvWCXUj3Pbu5jkmkr6pIroni6TwftfybR/uG+xPB+1/JtH+4b7FyUPHXBZccrL6cgjgttFUw0lW+pglhkppJXNbGJY3sD4w4uHjOaBpqddAStth3EnHc+lr4bLXPnqaAsFVTVFLNTTRB4JY4xysa7a4A6O00Oh0KaSvqlMVUzsiW3OPWsgg22jIP/AMBnsXnT2Cntk/dFoc+yVWoPWUGjGu05aPj02PH94H5tNAuM4V8Xo+JN+y6gFKKWO1Vm2hk+OUerohOOfMGaCpAI0G1rfPqc3gpnVfxI4d0d+ucNNBWTVVbA5lI1zYw2GrmhaQHOcdS2NpPPtJ7OxWi9cp3VSrjRXs34pixDK33oy0FwjZBd6dge8RAiKdhOglj1JOmvItJJYSASQWud0qiC61Rs9TbbxGdslDVR73eeF7hHK35/FcToeWrW9mmol9aVxE0xcjj84/7DwspsxarwjdIiIsnIIiICIiAiIgIiIK7dEL78cdv1j3T+GJWJVduiF9+OO36x7p/DErEoCIiAiIgIiICIiAiIgIiICIiCu3Qe+DTLP0xu/wDOCsSq7dB74NMs/TG7/wA4KxKAiIgIiICIiAiIgKNszpzScQqeocDsr7YImnTluhlc4jXzkT6jz6HzKSVpMtxsZJbGxse2Gup5BUUk7gSI5QCOYHa0gua4eZx00OhWtuYiZpndMYefi3sXNHciqXFuaHtLT2EaFV54VZVf+CuH03D66YHk15uNnlkpaCus9EJaOvgMjnRSGcuDIjtcA4PI001+ZT5BX/65JQVcfcdzhGslJIeen4bDy3sPkcOXkOhBAy1z1U1UThVD6LCK8KqZQTccZuc9b0iSbTVvZdqKFlD/AKs4isItQYWxcvdNH6t0brz5dq4vJLbllxtEVgu9uzKS3vxCiprNQY+yWGGWtdA5s7a2RpbsLXdWNsrms27tQTqrVIq4qTaieKt+Ovu2FXThLktbjV/q6OHDHWKtiorbLNU0lVrSu0liA3NaTC8btNNQOehBW1st5u/AvLc6oKvEMhyK03y7y36111goTVhz5mM62CUA+5Fr2ci7RpDtdRop7RExbw3SqVb7fdOFWRcLLtkdqrZrzdbxkN7r7ZaIDWTQPqYi7q2tZqX7Glu7br2O01AXjkGA5LktyreIbscyGkss+WRXN2PUUslJdTSMoe5TVMbG9r2yl+r+rBDi3Xzq01yxW13e+2e81dL1tytBmNFP1j29UZWbJPFBAdq3l4wOnk0W1U4qaDhM7P2Vkv8AhNvu2DXK641jeZNuVZfbLFUOyQ1c9XUwU9ZFJvayd75GxsD5NSQ3TRx7Oa3fFJ+UY5xIzm9Y5ZLlXVlXh9Db7fNSUkkkZrH1lQwHc0EaxiRsjvM0anQFWARQtoo4T52+KuuM8Pcz4S57w7qp30F7skdCcUqBYbVPE+GDZ1kM85dNLqBLHoX6NA65xPvl3XRps9fYeEFuornRVNurGV9ye6nq4nRSBrq+oc0lrgDoWua4Hygg+VSgvGsrqe3wGapmZBECBvkdoNT2D8p8ymImqcIWptxROMed3g1+TU5r7dFQNBMldUwUrQBr76RocfyBu5x+YFTIuFw3HJ624RX24076ZsTSLfSTNLZGbgQ6aRp965zeTW9rWl27m4tZ3S6q/u0Rb5YzPvnDweLld2Llf3d0CIixcIiIgIiICIiAiIgrt0Qvvxx2/WPdP4YlYlV26IX3447frHun8MSsSgIiICIiAiIgIiICIiAiIgIiIK7dB74NMs/TG7/zgrEqu3Qe+DTLP0xu/wDOCsSgIiICIiAiIgIiICIiDW3zG7ZktM2C50MNYxh3MMjfGjd52OHNp+cEFc6/hRa+yG4XinZ5GtuEj9P2vLj/AIrtEWtN2umMInYvTXVT+GcHEfcooPle9eu/Yn3KKD5XvXrv2Lt0V9Pc5/JfTXOqXEfcooPle9eu/Yn3KKD5XvXrv2Lt0TT3OfyNNc6pVr6LdHWcWOFz77f73dJLgLrXUm6Co6tvVxTuYzkB26Ac1Lv3KKD5XvXrv2KLugh8BMn5+un+aerEJp7nP5GmudUuI+5RQfK969d+xPuUUHyvevXfsXbomnuc/kaa51S4kcKLfrzu16cPN3aR/wBgtpZuH9jslWyrhpHVFaz3tVWzPqJGctPFLydvL8HT/FdEirN65MYYqzcrqjCZkREWLMREQEREBERAREQEREFduiF9+OO36x7p/DErEqu3RC+/HHb9Y90/hiViUBERAREQEREBERAREQEREBERBXboPfBpln6Y3f8AnBWJVdug98GmWfpjd/5wViUBERAREQEREBERAREQEREBERAREQV36CHwEyfn66f5p6sQq79BD4CZPz9dP809WIQEREBERAREQEREBERAREQEREBERBXbohffjjt+se6fwxKxKrt0Qvvxx2/WPdP4YlYlAREQEREBERAREQEREBERAREQV26D3waZZ+mN3/nBWJVdug98GmWfpjd/5wViUBERAREQEREBERAREQEREBERARFS7+kz4FHOOGlJn1rpw+8Yxqys2N8eWhe7n+Xq3kO+ZrpCgknoIfATJ+frp/mnqxC/jz/R9cFZ+KvHu23eZj22TE5I7vUzN5AztdrTR6+cyN3aeVsbwv7DICIiAiIgIiICIiAiIgIiICIiAiIgrt0Qvvxx2/WPdP4YlYlV26IX3447frHun8MSsSgIiICIiAiIgIiICIiAiIgIiIK7dB74NMs/TG7/AM4KxKrt0Hvg0yz9Mbv/ADgrEoCIiAudyPOKLH5xSMimuVzc0OFFSAF7Wnsc9xIaxvI83Ea6HQEjRfvNcifjlmElM1klwqpW0tIx/vTK7U6nzhrQ55HaQwgc1xNFRtooi3rJJ5Xu3yzzO3STPPa5x8p5D5gAAAAABrEU0U59UY8o88Hdk2T6bbVuZ0mc5TP40VrtNI09jJaqSZw/KQxo/wCWv7e1fjwzy74tZPpTLAuF4oLQaUV1bTURq5201OKiVsfXTO1LY2akbnHQ6NHM6FZajT8qY7Hp6pZ5PTwzy74tZPpTJ4Z5d8Wsn0pl5omnnpjsTqlnk9PDPLvi1k+lMnhnl3xayfSmXPXfPMZx+6wWy6ZFabbcqjQw0dXXRRTSa8htY5wJ/YFvU089MdiNVszwenhnl3xayfSmTwzy74tZPpTLyfI2PbucG7jtGp01PmX1NPPTHYnVLPJ6eGeXfFrJ9KZPDPLvi1k+lMvNfHyNj27nBu47RqdNT5k089Mdhqlnk9fDPLvi1k+lMsW6ZBkl6ttXb6+32GroauF9PPTy9cWSxuaWua4eUEEg/lWgyTibh+G17KG/5ZY7HWvjEzaa5XGGnkcwkgODXuBIJaRr2cj5lt7HfrZk9rgudnuNJdrbPu6qsoZ2zQybXFrtr2kg6OBB0PaCE089MdiNWsTOGCP+j1wrrOjjiFZYcfZbawVlY+snrKx0hleTyY0kADRrQAPn3H+0VKXhnl3xayfSmXmiaeemOxOqWeT08M8u+LWT6UyeGeXfFrJ9KZeL5GxgF7g0EhoLjpqT2BY77tQx3OO2urKdtxkidOykMrRM+NpAc8M11LQXNBOmgJHnTTz0x2Gq2eTO8M8u+LWT6UyeGeXfFrJ9KZeaJp56Y7DVLPJ6eGeXfFrJ9KZPDPLvi1k+lMsepqYaOnlqKiVkEETDJJLI4NaxoGpcSeQAHPVaPG+IeK5lLJFj+TWe+yxjc9ltr4qhzR5yGOOiaeemOxGq2N2DpPDPLvi1k+lMnhnl3xayfSmXmiaeemOxOqWeT08M8u+LWT6UyDNMtbzNJZX/AIokmbr+3Q/9l5omnnpjsNUs8m2oOJggkEd+t5tLCdBWxS9dSj53v0aYx87m7R5XeftwQRqOYUZEAggjUFZmC3V1kuzcfkce988bpbfuP9SW+/gH4oHjNHkAe0aNDQJjNuxObGEx3+fPt4MoySLdOfRuSEiIsnmCIiCu3RC+/HHb9Y90/hiViVXbohffjjt+se6fwxKxKAiIgIiICIiAiIgIiICIiAiIgrt0Hvg0yz9Mbv8AzgrEqu3Qe+DTLP0xu/8AOCsSgIiII94kPc7KsXiP9V1dZMAf9oBE1v7dr3/4rDXQcSLRPWWukuNJG+aqtc/dHVR83SxFpZK0Dyna7cB5XMaPKucgnjqYY5oZGyxSND2SMOrXNI1BBHaCtLu2iiY4Rh3zP1e9kVUTbw5In6QfOo4XDy+G9v8A5c65O2cVMmxW/wCYy5peqmmuNuprnX0OKyWyOOlraWAOfFJS1QG6Q7AC9pcXAuOrQAu/o+BFlps0gyGW732ujpa2W5UdmrK7rKCkqZA/dLHHt1B90foC4hu46AL903A60nLGX263m+ZG+HuruSgvFW2ampBUNLZhG0MBILHFgDnOAadBoudtNNczjDiLHl2dYvUcNLzkGTQ3+gzOojo6q1soIoWUMs1M+eI072De5rTHsPWF2oOvJaTAuImeDEeFOX3fKRd4cnucNqrrUbdBDE1sjZQ2Vj2NDxIHRtJ57TqdGt5KSsT4A2PFLzZ67vtfbxBY2PZZ7ddawTU1tDm7PcmhgcSGasBkc8tB0GizLdwSsdsw/Dsbiq7g6hxauhuFFI+SMyySR79okOzQt90drtDTyHMIiKK/M+790fcC8HxriLw4yW75XaaC7Xe9Xi5tu81dC18keyokiZFvcNWNZGxm0Ajb2jRazox8QMhu7uH1krrjJU2yXDqyrLJmNL5Xw3COCCUvI3a9SezXQ66ka81IGQ9HOw3y7Xirp75kdho71IZrrarPceopK6Rw0e97NpLS8ABxY5u7yrcXrgzZq+ewVFqrLjilVY6R1upJrHKyMikds1gcHse0s9zYRy1BGoIKEUVRhhw70GXy5ZLxFPDipqMoq7fVx5/dbdFPS0tMTG2MVjInAPiILmRxlg1BBDyXAkAiR7jLmmQ8YLxiluzmqsdFbcfoaxssdtpJny1Eks7HPfvjPIiNurW6filvPXcR9HfHqfCaHG6a5XqmbQXaS9UVzjq2mtpql73uLmyOYQ4e6vb47Xag89TzXPVvA+83ritc62TJMktNo8HaC2su9ur4Y6mtkjknMjZfEJ10ew7w1vNx2ntRGbXG/jhx9jF4L8Ysj4g5ji9Nc5oYqepxm4VFZTU8TRFJWU1xZS9cxxG4NIDyG7tPG8pAK4u+XLJeIp4cVNRlFXb6uPP7rbop6WlpiY2xisZE4B8RBcyOMsGoIIeS4EgETRLwFx+lp8bZYq26YrNYKSSgpKmzzsbI6neWukjk6xjw8OcwOJI3buYIJWNH0d8ep8JocbprleqZtBdpL1RXOOraa2mqXve4ubI5hDh7q9vjtdqDz1PNDMuTGE+dyPsrob7/AKS1XDbMbtOa1UeHULZzfaptKBpVVA6waQvG5x7QGtA8nmUi43frjScaqrFAynt9kp8WpLi210sTBFDVSVVQ2RzXBocQQwDzctdASV0Fi4b0Vjy7wlNwuNfdXWeCzSS1kjHCSKKR7xI7awEyF0jtTroeWgCx8s4VUWU5RT5DFeLxYbtHRm3yz2ioZEamm37xE/cx3IOLiHN2uG46OChpFFUbY5odwbiFnWfycOLaMrda33u2Xipr6yK3075XOp6xkcRjDmbGuDTt5tIIJ1aXaOGTZ+K2aZLDi2GwXenocjr71ebZWZF3Gx5EFukIdJHCfEEsgdGOYLR4529gElYVwNsGBz4vLbqq4ynHaOsoaQVMrHh8dTK2WQyaMBLg5gAII5a66nmsas6P2PVVsNPHXXahrWXmrvtLdqOpbFV0dTUPc6URODNNh3lu1zXAjt101UqRRcw39/u/dw3GvE8lgo+F1HV5zXVVb4YQxtuMdvpY5PHhmMbyzqywuZtcBoA09YdWnRuntnWXXbhjxQr6+qrzkNPbeH9ddGRVNFTMlM8EtO0kTRxteGyElzmg7ATqGjQad9c+C1De8QgsdxyHIa6oprgy6U16mrWmup6lnvXsds2NAGo27Nujjy5rMHCW01F4prncqyvvVRHY57BMLg+N7aqnmex8hlDWDV56sDUaDQnl5oTNFWM4ezi4Kx5LnGH5Lw6dkWTxZJRZgX09TRigigbQz9zOnYYHMAc5g2OYesLjoQdR2Lm8C4iZ4MR4U5fd8pF3hye5w2qutRt0EMTWyNlDZWPY0PEgdG0nntOp0a3kpOxDgPZsSvtqujrxfb6+zQvp7RT3isbNFbmPbscIgGNJOwbNzy47eWqyLdwSsdsw/Dsbiq7g6hxauhuFFI+SMyySR79okOzQt90drtDTyHMKURRX5n3fujC1Zlk9YzOrPm+QVFFfTa7o+HGZrXFDTPgbuEU9JUgazNEem4FxcC7mG6c+OwCzV19xrhNkEGHjBLbidoiuFxzOt7na6pgbQFpDGRuL5I3kh537eTefM6KdI+AlqkvM9yuWQZFfZjS1dHSMuda2VtAypG2bqdGA6kAAF5doAAupoeH1ppOHFNg8rZa2xRWptmc2ocOslpxCIvGc0Dxi0cyAOfZomKItVTO3z59iGeFfE/LpuI9stF2uF3vNivlnqbhRVt6s9Nb3l8LoiHwticXdW5svvZWhw8U6nUrFxW/8Wb30erbmtNkU97yG5UkDzQUlspfcITM3rJoWbQZJ+qDjsc7YSSA3kFI+O8Bbbj+Q2W9yZJkd3uFop5aOldcqyORop5GbTCWtjaNBo124APJY3c4gaLbW/hPRWbhlbMJtl6vVrordFFFT3GjqWR1gDHBw1eGbTrpoRt0I5EImKK8Ns8+Pu/dEd24y3ySx4ZY8VyC5ZXdb9WVzJ7rT2qlhuNKyla10kJppnRRNmBkYDvA0AcQw8l8umdcWLPitLTVhqLPX1OVWy2W+63iio+uqaWodtkE0MEkkYLXajVhYXN002nVSAejljfeGGibcL1HdYbnLeWZFHWBtybWSNDZJesDdvjMAaWbNhAA28luHcIKKqslqt1xvt9u7rfeYL4ytr6pkk8k8Tg5jXHYGiPUDxWNb5dNNdUMy5O+e91WOWyttFohpbjd6i+1bC4vrqmKKJ8gLiQC2JrWjQEN5Dyc9TqvSse6K+YzLH/WtukYb59HMex3/AEucs5frGqB1+zCnlaCaKzF0sjwfFdUvYWsj+ctY9zz5t0Z8vLfJ9ledwiJ8/Hctfqii1OKTERFR80IiIK7dEL78cdv1j3T+GJWJVduiF9+OO36x7p/DErEoCIiAiIgIiICIiAiIgIiICIiCu3Qe+DTLP0xu/wDOCsSq7dB74NMs/TG7/wA4KxKAiIgLhb7glXSVEtZjzoAyRxfLa6g7Ii49rongExk9pBBaTz8UlxPdIr01zT7YaUXKrc51MolfNeKbxanGLrG8dvVNimb+wsef/p+zsX474V/o5evVPtUuor51rjR3y7deucoRF3wr/Ry9eqfanfCv9HL16p9ql1EzrXR3mvXOUIi74V/o5evVPtTvhX+jl69U+1S6iZ1ro7zXrnKERd8K/wBHL16p9qd8K/0cvXqn2qSbxlNnx6qttLc7pR2+puU4paKGpnax9TKRrsjBOrnaeQKPJWZTxmtGd4zerReOG9rbVChtt8t1xi7troWvPWSs0B6lrg0Aa6kteewhM610d5r1zlDm6viVQUOYUeKzW27NyGsp3VcFvFE50joQSDJy5BuoI1JA15Lfd8K/0cvXqn2qS8fsNNjdlt1tpnTTR0NLHSRz1UhlmexjQ0b3nm48tST2nUrZJnWujvNeucoRF3wr/Ry9eqfanfCv9HL16p9ql1EzrXR3mvXOUIIxLiFR53aTc7BbrpdbeJpKc1EFLq3rGOLXt5ntBBC3XfCv9HL16p9q3PAe7d+cEdUeAP3N/wDX6pneTqOp10lI6/b1Uf8AWe/1289e09qkRM610d5r1zlCIu+Ff6OXr1T7U74V/o5evVPtUuomda6O8165yhEXfCv9HL16p9qd8K/0cvXqn2qXUTOtdHea9c5QiCS510Ubnuxy97Wgk6Uep0/IDzXNQ8XbJJYXXqWCvoLU2p7jNXcafuRgm1AEespb4xJAA8p5Kwi0WaYLj3Eawy2XJ7NR3y1SuDnUtbEJGhw7HDX3rhqdHDQjXtTOtdHea9c5Qj2O6VssbXsx68vY4BzXNpQQQewg7l+u+Ff6OXr1T7V01w4Xiq4gY9k1Jkt9tVNaKY0Zx6jq9lsq49rw0yw6c3NLwQ7X+wBotbQ3HiVi9HnNxv8ARWnKqenkdUY5bsfa+Csmi1eepnMrtm8DqwC3t0ceZIAZ1ro7zXrnKGr74V/o5evVPtQV1xcdG43enO8gNM1v+JcAtzHxyxy12TEavLXS4NccmeYaK031vVVAmBAMbtNWtPNvaR74eU6KQw4OJAIJB0OnkTOtdHea9c5QjWhxjIb4/bNCMeoiSHSSPZLVOH4jW7mMPzuLtPwfN39ptNJY7fFRUUIhp4gdG6lxJJ1LnOPNziSSXEkkkkkkrMRVqrxjNiMIct29Xdn70iIizYCIiCu3RC+/HHb9Y90/hiViVXbohffjjt+se6fwxKxKAiIgIiICIiAiIgIiICIiAiIgrt0Hvg0yz9Mbv/OCsSq7dB74NMs/TG7/AM4KxKAiIgIiICIiAiLQXHNbVTXiosNLX0VblTKKSuhsQq2MqZWN0AJaTq1pc5o3Eac/mKDfqNqnik3NL1nWFYZNLTZlYaQDu6522bvfFUyMJjY5/LcQCxxA/svBG/RwGqpcKvHHLC8VruIlvuOE3SguXfR9ist5dsfseTAyoewDdp7m8gEaObqCOwS6AAToNNeZ+dBwNh4UU1bTYfdM7ZbswzjHoHNiv76FsJbI8gueyMEtafFboe0EEjbqQu/REBEXlLVQQu2yTRxu7dHOAKD1RY/fCl+Mw/vAnfCl+Mw/vAg5ThLa82s+Jup8/vFFfL/3XUPFVQMDY+5zITCzQRx82s0B8XtHae1dmoz4D4vivD7BHWrGq26SW019VUF1+gfTVHWSSlz/ABJIojs3E7Tt0I0IJ7VInfCl+Mw/vAgyEWP3wpfjMP7wL3B1CD6iIgIiICIiDFr7VRXUQito6esEMjZohPE1/VyNOrXt1HJwIBBHMLlKThFjtr4j3nPbfDUUuU3ajFHVVJqpXwyNaGBjjC52wOaI2gEActfwiV2qIIdhoeLXC/hRURwVtPxgy+Ct3QGsbFajNSeL4hI8XrAA7RxPMu566Lp6zi7bLLm2MYheKK4UOQX+l6+nEVJJPSNkDXOfCahrdu5oY889OQBOmoXdog11ryO03yetgtt0orhNRSmCqjpahkrqeQEgseGk7XAggg6HkVsVw1VwaxmK35fFYqQ4hX5VGW3K7Y9tpat7/H0la8AgSAyPO/TXVxPbzWqrMZ4jYnj2H2rEr9br+6hqBHea/MDK+pq6Yu5uY6ID3UAnTXQHaNfKgk5FxVHxErZuJl2xWqxK80Vuo6MVkWTysZ3tqBozcwP3ateC/TaRz2PPIBZ3DziZi/FjHhfMRvVNfbX1phNRTE+JIACWOaQC1wDmnQgHQjzoId6IX3447frHun8MSsSq7dEL78cdv1j3T+GJWJQEREBERAREQEREBERAREQEREFdug98GmWfpjd/5wViVXTopf8AlnOeOeDu8XvVlz7rDGf/ANuCujEsbR83iO0/KrFoCIiAiIgL4ToCVhXy7w4/Za+6VLJ5Keip5KmRlNE6WVzWNLiGMaCXO0HIAak8lGOG2um421mB8V6kZNjppaKd9HjdbOIIgZdWieWNnNxLNdu52m1zTtB11DGOZXvpD8Oqmo4bXi5YBKy7dyG7XuyHrJqdn9Y+nik01DidA52hBY4ENPMSRQ4PYqDJ6vJorRRNyStgjpqq6sgaJ5mMGjWl3bp2ctfIO3Qab1EBERAREQFVnpN8QLlj/GXFLCOI8PDex1tmqqueungo3tlmjliaxm6oYQNQ9/IEditMoJ4kYJXXrpF4vkEltiq7DRY/W0k88pjcGTyTQujbsJ3HUMfzA0GnMjVBw9Nk+Q2DKuEtuZnTsyteS11e+ouRpKVjaqnbQPlia0wsDdoewODm6E66EkclzeKdIa93rhjxKZcXC35XZaK9VtnreqZsrKenknjZK1um0uikjDHtI/AcQQ9SVnmF3K6cUOFV0ttC11psVZXyVr2PYwU7JKKSKPRpIJ1e5o0aDprqdBzUc1PR4u+VcA7lj9U3vFl0Vfeam2VQlY7aypqJ/c3OaSOrmhl2uHk3AkbmjQMuu4t5lQ3S9ig/8ZmpOHFLkFNbe52e6173zBz/ABWhx1DG+IDpy5AEr923Mchp+A2aZvQ8UIc0kjsE9XRyQ22liFvq2QPeeTBz0O33OUEjbz11W1x3C8sxfPpshhsbawU+A0NrghfWRRtmr4ZZnugLtSWjxm+PtLefInTRcfduGGZ5rPxKv7MJp8KmveH1VlFmjuEEst1rn6mOeR0ZETdo1YHOO47zroAg22S9JK0VeM8PoMazmzVuSXK9Wekr6akqaeeZ8UsrG1DTHz26gkEgAt8hCuHH/Vt/IFVvOuFldcsF4d0drslObpar3ZautEfVRuiiglY6d24kB20NJ0BJPkBVpI/6tv5Ag/SIiAiIgIiICIiAiIgIiIC4nNODeJ51h1djFbbBQ2qsqBWSttMjqJ5nBDhLuiLSXatadTrroNddF2yIKedE/C8ppuIvEqex5ebdidsz+60txsVRQsqH1zRHGI390OO9jgQ0HTtGpPNTzQ8Scos7c+r8zwx9kx/Hy+e219BWNrprtTDrDq2Bg3RvDWM8UnmZAOWmqjnh8Twl6Xmb4lIersue0bcotgPvW1sfudZGPO53KU+YAKyCDlMX4o4xl+NWK+0N1ihoL4Sy393g0stQ8EgsbHKGuLgWu5Ac9NRqOa6tRL0gKjCKGXh3PmWNz3+WfLKCgs8tMNH0NdIXGKZzt7SIw6MbhqdeWrXdilpAREQEREBERAREQEREBERBXS3f+Tunfd4PeUuY4dDV6/h1VJP1e359IjqrFqufST/8q8b+AOaD3OOO/wA+Ozu8jhXQFjA78hYSFYxARF5zzxUsEk88jIYY2l75JHBrWtA1JJPYAPKg9Fqspya3YXjdzv13nNNa7bTvqqqYMc8sjY0ucdrQSeQ7AFpI+L2HTcMvuhR5BSPw3uQ1nfUOPV9WDoQRpuDw4FhjI3h4LSN3JajGqa75vmVFnFHlzqnh5cLHGKDHm0IjEr5TvdPM543HxdoA0bpqQfLuDHxSkqeJmTYtxOoclvtvxiazEU+KVNOKZj5JTr1847XHbtDWnUAgOa7Rx1k9fANBoOQX1AREQEREBERAWLU2ymrJOsmi3v0013Ecv2FZSINf3hof9h/1u9qd4aH/AGH/AFu9q2C5XK+K2E4HXRUWTZhYMdrJY+ujp7tc4KWR8epG8NkcCW6gjXs1B8yDTcJbhjea4m642XIK3LqIVdRB3xr4RBIHskLXR7WxQjRhBaDs5gdru1dn3hof9h/1u9qg/o7dJLhjmWK08FBUYlgtfVXSopafGqa60rZZ5DMWtkZGBGXOmJDho0kl3a7tVgEGv7w0P+w/63e1Z4GgAHYF9RAREQEREBERAREQEREBERAREQV46ZdtqsfxjFuKdridLdeH13iuUjYx48tBIRFVxD5nMLST5mFT9bLlTXi3UlfRTNqKOqiZPBMw6tkY4BzXD5iCCsfI7BRZXj1zslyiE9uuVLLR1MR/txyMLHD9oJUJ9DO/1sfDW4YFepTJf8AuU2O1DncjLBGdaaUDyMMRa1v9xBJXEivzagnxPwNttFcYpr5TxXw1jgDBbSHddLHq9ur2+LoBu/uldmqqZJ0quDfFzJ8KpbJxn8F57VeGXOZstJV0VPXxRseX0000rYo2McPw3EEgDaSQFYnEeIuJ8QG1TsXyezZI2kLRUG0XCKq6ndrt39W47ddrtNe3Q+ZB0SIiAiIgIiICIiAiLW37IKLG6EVNbIWh7xHFEwbpJpCCQxje1x0BPzAEnQAkTETVOEJiJmcIbJFG1VmGT3NxdSsorHAR4rJ4zVT9v9ohzWNOnkG78qxO+eWekcf1fH7Vro6Y31xHbPyiYdkZHdmMcHJ9Om1VFV0dLzd6Ju65Y5WUd8pT+C+GoYXH9jHPKnSzXWC+2ehuVK7fS1kEdRE7zse0Oaf+RCiHLrTe84xW8Y9dr82a2XWkloqmNtCxrjHIwtdodeR0J0PkK+4pbsgw3GLRYLfkhNBa6SKip+uomPf1cbAxm5xPM6NHPyqcyj1kfq8E6ldTSq/9OziZ9zLo1ZRNDKIq+8tbZaXnpq6cESaHyEQtlIPnAXWd88s9I4/q+P2qOOM3BNnHultFJmN7nrqC2TPqIqOCIQRyPcA3V+xwJ0AIGhGm4+dRmUesj9Xgaldfz36JvSWvHBHJH2OptkuXYJfZRFdcZMQn6wuAb10DHeL1ugALT4sjQGu0IY9n9kcau4yDHLVdG0FZam11JFUiguMPU1NNvYHdXLHqdj267XN1OhBChPhdgtj4IxsjsOF2IQBoY+rtdN1Nft00O58rnmX8he3t8p7ZytF3pL7b4q2hmE9NLrtdtLSCDoWuaQC1wIILSAQQQQCFWqiaYzonGOcednxc9y1Xa/FDMREWbEREQEREBEUfZnfb3Fl8Vsttxjt9OKEVLiaZspc4yOb5TyGgVoiJiZmcIjzwxZ3LlNqiblc4RCQUUXd35X6SR/V8ftTu/K/SSP6vj9qy02T+tjsq/wBXnelMk6+6fBKKqh/SLcC/upcGH5JboN9+xPfWsDW6ulpCB3Qz9gaJB/u3Ae+Uvd35X6SR/V8ftX5lqsonifHJkMUkbwWuY63RkOB7QRrzCabJ/Wx2Vf6npTJOvunwfzq/o4OBbuJPGIZdcaffYsTLKppcPFlrTr1DR59hBk5dhYzX3y/rQq9cJuFp4JYzLYMRubLdbpaqSska6jbI58j9NSXOOp0Aa0eYNC7Tu/K/SSP6vj9qabJ/Wx2Vf6npTJOvunwSiii7u/K/SSP6vj9qd35X6SR/V8ftTTZP62Oyr/U9KZJ190+CUUUR3PIsqs9OyqdfYqhjZ4WOiNCxu5rpGtI1B5cnFS4tfu1UxXRVExtjjww5xHN22L9vKKc+1OMbhERVdAiIgIiICIvhOg1PYg+ouBuXEWquLnMxumgmp/JdK0nqX/PGxpDpG/jataeRaXA6rVvu2WSOJ7/08fP3sVubp/i4n/Fb6KI2V1RE/H6RLroyW7XGMQlJFFffPLPSOP6vj9qd88s9I4/q+P2pmUesj9XgvqV1Kirjff8A9I+mVZ7sPcrFxMthtdWexoudIN0D3HzuhJjaPKdV3XfPLPSOP6vj9q5TiJgtw4n0Vpp73f3E2m5wXehnpqRkcsFTCSWPa4H5yNPKCmZR6yP1eBqV1/NXpl8DH8EuPN2tNBSlljuru+VpZG3UCKRx1iaAP7Dw9gHboGnyr+nnQ/4FN4BcFLTZaqJrcgrf/ELs8Dn17wPc9fNG0NZy5atcR75afN+GcfEe+Y3eMjqKW6XLHanuu11ElEGmnk1addGuAcNWNOjgRqAdNV2PfPLPSOP6vj9qZlHrI/V4GpXUqIor755Z6Rx/V8ftTvnlnpHH9Xx+1Myj1kfq8DUrqVEUV988s9I4/q+P2r6LplYOvhFEfmNvZp/3UZlHrI/V4GpXUpoo3o81yS1vBrqekvdLy3GjYaaoaPKQ1znMf+Tcz9unPu7PeaS/W+OtopetgfqNS0tc0g6FrmkAtcDqCCAQVWqiYjOicY9nnGPi57lmu1+KGaiIs2IoliuRym5zX2QiSJ+6G3jyR02o5j55C0PJ8o2DntCk29db3nrup/ruok2f3tp0/wAVFeLbfBi0bddvccOmvbpsC2/DamqN8zh8Hp5DRE1TVPBhZrn9g4d2uK4ZDcW0FPNKIIQI3yyTSHUhjI2Bz3u0BOjQToCsnE8stWcWGmvNlqXVduqC8RyuhfESWuLHAse1rgQ5pGhA7FzPFTh7cMwmx68WG6QWnJseqn1dvkrITNTSl8To5IpWAg7XNcRuadW9oUNZPxiyziG/D8YoKRtmutZeLparzHRXl1G2SeiY0mKCsbE97Wv37+TA/RhbqOZXI9Sq5NE7fgtEirhXRZjheF1Vlyiru88t6vVPRY5SWbInS14c6Nz3wy174Y3CP3N79xBcGkjUkBcnLkWa0WD5PjVVf7lbLna81tFupq1l0dW1NPBUOpnGM1BYwzNHWO9+3mDtdqAmCs3sN8LdrVVWVWuiyagx+aq2Xevp5qqnp+red8URYJHbgNo0MjORIJ15a6FQ7fLFVTcVbBw3iyjIrbYHWiqvk9Sy7TGur5hNHGIRUuJe1jA4vLWEdo8iw8w4fNqeM/DjHjkeQMhhsV3L6+O4ObXTN66mIY6cDfpzHMEO0aAT26lpuTwhYNedsuRxfJKWraQ2huU0dJWM56dY7xIZQOzduLYyfK1zdddjQo46P15uV0wu40d0uE91qLNfLjZ2V1W7dPPFBUvjjdI7+07aACfLpqea7LMN/g9U9Vr126Pq9Pw+sbt/x0XRk/8AFinhOyfj5x96LkRdtTimdERUfMiIiAiIgKNss+Elv5pZ/OepJUbZZ8JLfzSz+c9RX/Bue76w877R/lLnnjD6iIvmHwAiiHpI5TfrDY8WtdglNLU5DfYLTJUir7kc1jmSP2Nn6uTqnPMbWBwYSNx00OhEcZhbeJnD3hpmE9Zeam12981pFtczIJbpW0kxr4mTEVEsEbjG9jmjY/eOTh2OIWtNvOiJx3uu3k81xE50RjP1wWlWpuGVWu15DaLHVVXVXS7MnfRQdW89a2ENMp3AbW6B7ffEa68tVXvP8svnAu9Z/TWa73S808OHNvdNHeqt9a6nqxUvhMjXPJIZoWuLB4vicgByWfS4IcN468JpHZRe8olrLfdnS1N2rjUMc8QwEyRNPKMO3e9b4ugboOWptFvjM+cFoyeMM6Z2YTh8IxWLREWDiabLvvKf+Jpv58amFQ9l33lP/E038+NTCvoMk/lY/wAqvlS+y+xv5er/AC+kCIi6HvCIiAiIgLgeIlyNyuNPjbCO5nw91XBvPx4i4tjiP4r3Nfu84jLTqHFd8otuu/7oOQb9f6ql2ebZsd//AK3Le3siquN8R9Yj6uvJaIruxEse83elx+z110rpHQ0NFA+pnkbG6QtjY0ucQ1oLnaAHkASfIFr2ZvY5KTHqltxi6jIHMZa3kECqLoXTN28uWsbHO56dmnaQDuZ4I6qCSGZjZIpGlj2OGoc0jQgqkbLZfbhQT2imFS6o4IslqoNCQKyRtZ1lO38YGhgc3TzzD8i43uXK5owwjz/xci35babrkl3sNLV9ddbSyGStgEbwIRMHGPVxG0khpOgJIGmumoX6yHKrXiotxulV3KLhWxW6m9ze/rKiQkMZ4oOmuh5nQDykKptFkOR5A+xVdjdLTu4n3+6XN0ouTrZPJR0sbY6OnFS2KR0e6NnWeK3cQCAW6lb7MMQzShx3H7RldymioqnOLULZJBeH19bSRuDhI01T4Y3OId4zSWlw3aanQKcGemmYnCPP/FqVhXu90GN2irul0q4qC3Ukbpp6md21kbAOZJUV8NJLlifGPKsH7+XLIrHBa6S7U8t2qTU1FFLLJLG6Ayu8ZzXCMPAcSQOxdzxPwSm4m4HeMZq6qSihuEQYKmIAuie17XsdoeR0c1p0PaOXlUN4qmqmZiNr84JxQxviUytdj1dLWdxlgnE1HPTObvBLDpKxpIO06EajkuqVbM8405rhuNZZjF5lt1Nllugt8kGR21pNOaWqqhTGofE/XqpGeMS0kt10I5Lc8YMSrOFXA3ObjZsvymquLqGPq6m43eWd8MgkbrJG7kYydeYaQPMAmDOLuyfZvT0igziPZDjNDj+KUN2zC+ZHf66SeLqchko3TdVDrM6Sfn1EIBDtkTR4xADdNQuBsV+yjIbHhOP3jILvSTw57X2GrqKK6P7omp4YKhzYpKhgYZNNGjftaTtDtA7mBN3CcMFsUVUMxzvKOHlTnGG2W+1typYbxY6GkuV0ri6ooG12/ro3VT2vI02N2ve15Z1wOh0Czcrs3Evh5w44jXCqudRbrM3HpZKZrsmnulZT1rXDSWOd8ET42lhdqNx5tBGnNTgjTb9m5aJeNDcji+Q0lezRtHXTR0dc3noS8hkUv95ry1pP4Ljr71umgwHGHYzY2Ca7XO81lU1k9RU3OrfMXSFgDixpO2NpPPYwBo17FlZtu8FLn1evXdSer07d/wDZ/wCrRb5Ptu008J2T8fPatcpi5bmKk0oiKj5gUSxW04tcprFIAyJhdNbzz0kp9RyHzxlwYR5BsPLcFLS1t+x+iyOiFNWxlwY8SxSsO2SGQAgPY7tadCR84JB1BIOlNUYTRVunzi6bF6bNePBDudcNsd4lUVJS5FQOroqSXr4DHUy07436Fu4Pic1w5Ejt8q19VwVwisw2kxV+O0rbFSSienp4i+N0MoJPWskaQ9smpOrw7cdTqea7uqw/JrY4tpX0V7gA8V88hpZ+3+1tY5jjp5Rt/IsTvZlfo5H9YR+xNBVP4ZifjEfPCXsRfsVbcYca7gnhjsTGNus7nWoVYrwDVzmdtQOyYT7+tD9OW4P105a6LyoeBODWyGoipbEIY6iqpa6YNqp/daimfvhld4/N4dzLjzfy3btAu372ZX6OR/WEfsTvZlfo5H9YR+xNXuc4/NT4p0uT84c/nHDTG+I0VEzILYK19FIZKWojmkgngcRo4sljc17dRpqAeeg17F+LHwvxjG6qz1Nutnc89op6ilon9fK8xxzva+YHc47i5zQS52p1158yuj72ZX6OR/WEfsX0WvKydPB2MfObgzT/ALJq9fOPzU+KdNYxxxhr8cxW14nBWw2ql7lirKye4Tt6x7988zy+V/jE6auJOg5DyALY2y2nJ8kpaVo3UNtmjq6x/PTrG+PDED2btwbIR5A1uoG9pWZR4TkN0eBcKmls1LyLmULzPO4eUb3Na1n5druXmPZ3VotFJYrfFRUMIgpotdrdS4kk6lznEkucSSS4kkkkkklWppizOdM41ezh7cflh9MJ5MoyqnNzLbMREWLxxERAREQFG2WfCS380s/nPUkqP8zsV7ly+K5223MuFOaEUzgalsRa4SOd5e0aFTNM1266I3zHu5c3Fltuq7k9dFEYzPi5TKqTLamWnONXSy26INPXNuttmq3OPLTaWVEW0dvaD+xaLvXxT9JcQ/8Al2q//OXb9wZX6OM+sI/YncGV+jjPrCP2LyYyHKI4R20+L5OMgyyIw0f/AJce7BblmVkuNm4iPsGSWqpDOrp6C2zUm1wJO4ufUSHUHaWlu0tIPPzfKLghhdBjVfYI7TI+2V88NTVMnrqiWSaSJ7HxudK+QyHa6Nmg3actOzULse4Mr9HGfWEfsTuDK/Rxn1hH7E1LKfZ+anxTqOXbopw90xH1aquwaxXS+1V4rLdHVV9VbjaJ3zOc5klIXF5idGTsIJcdTprz0105LkLXwAxXDJ4LniFshteQUMMsNuqq+oq6yGmbIAHt6ozjxNByaCAPJopE7gyv0cZ9YR+xO4Mr9HGfWEfsSMiymN2H5qfFEZDl1MYRTPbHi4kWvil5clxD/wCXar/85e1DbeJTK2ndWZFiktIJGmaOCw1LJHM18YNca1waSNdCQQD5D2LsO4Mr9HGfWEfsTuDK/Rxn1hH7E1LKOUdtPinUcs9XH6Wvy77yn/iab+fGphUR3PHcqvFMyldYo6drp4XuldXMcGtbI1xOgHPk0qXF6dm1VZsRRXhjjM74nhTyx5PofsyxcyezNN2MJx+kCIis9cREQEREBcDxDthttxgyRg/1ZkPctwdz8SIOLo5T+Kxzn7vMJC46BpXfL4RqFeirNnbu4tLdc26oqhGS1tHjdsoK27VcFFEypu0jZa6TTUzubG2Ju7XzMY1unZy+crorlw6qrc4vxupggp/Ja6wHqGc+yN7dXRj8XRzRyDQ0DRat9pyyNxBsFPJz99FcWkH6TQf8FOgmfwVRMe+I+f7vdpyq1XGMzg5a78JsSv2H23Fq6ywzWO2tibRU4e9jqbq27Y3RyNcHtcBy3B2vM8+ZWE/gfhMuLR47LZeutDKzvgIpaud7zUbS3rXSF+9zgDyJcdNAe0DTtO9mV+jkf1hH7E72ZX6OR/WEfsTV6+cfmp8U6axzhxdu4Yw4FZqmn4fx26x11XUNmqqu7Qz3B1QA0jx3GdsjiOWhLyANRpzXlLhmT5TS1NozS447escq4nR1VFQWqppJZB2t0kNW/boQDyGvLtC7nvZlfo5H9YR+xO9mV+jkf1hH7E1evnH5qfE01jqcfjvBXCcVs12tVvx+n7iuzBHXtqnvqX1TQCA2R8rnOcACdAToNTposGh6PmBW6yXW0Q2SQ2+5wMpaqKa4VUpdE125sbXOkLmNB5gNIC77vZlfo5H9YR+xO9mV+jkf1hH7E1e5zj81PiaXJ+cNFmfDywcQYKOO+0Lqo0UpmppoaiWnmheQWkskic17dQdCAdCO3VR3lvRqsFw8GKCx22mtlipL6bvc6NtVPF1v+qyQ7otpJa/UxkkFmu0uJ3dsw97Mr9HI/rCP2J3syv0cj+sI/Ymr3Ocfmp8Sq7Yq3zDkrXwawu0YhccXp8fpnWO4vdJW01Q585qXnTV8j3lz3u5DRxcSNBoRoFjW7gXhNrx292OC0SOt16hbTV7J6+pmkniAIazrHyF7QNztA1w01Oi7bvZlfo5H9YR+xfRa8rJ08HYx85uDNP8Asmr184/NT4mmsc4fuKJsMTI2DRjAGtHmAXnQ205RkNLQs0dR0M0dXXP56AtIfFF/ec8NcR+C066b265lHhWR3SQCvqKSy0vLcKJ5qKhw8oDnNaxn5dr+XmPZ3dns9JYbfHRUUXVQR6kAuLnOJOpc5xJLnE6kkkkntVqaYsznTONXDDh7cflh+08uUZXTmzRb4s1ERYvHEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERB//9k=", + "image/jpeg": "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", "text/plain": [ "" ] @@ -208,7 +469,11 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ndisplay(Image(chain.get_graph(xray=True).draw_mermaid_png()))"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "display(Image(chain.get_graph(xray=True).draw_mermaid_png()))" + ] }, { "cell_type": "markdown", @@ -220,7 +485,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "id": "912b0604-a178-4246-a36f-2dedae606680", "metadata": { "ExecuteTime": { @@ -228,8 +493,28 @@ "start_time": "2024-05-15T08:19:51.937879Z" } }, - "outputs": [], - "source": ["for s in research_chain.stream(\n \"when is Taylor Swift's next tour?\", {\"recursion_limit\": 100}\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'supervisor': {'next': 'Search'}}\n", + "---\n", + "{'Search': {'messages': [HumanMessage(content='Taylor Swift\\'s next tour is called \"The Eras Tour,\" which is scheduled to hit U.S. stadiums beginning in March 2023 and running into August, with international dates set to be revealed later. The tour has already started with some shows, including the kickoff on March 18, 2023, in Glendale, AZ. The U.S. leg is set to wrap up in Los Angeles at SoFi Stadium on August 9, 2023.\\n\\nFor specific dates and locations, you may want to check Taylor Swift\\'s official website or trusted ticketing platforms, as the tour dates and details are subject to change.', name='Search')]}}\n", + "---\n", + "{'supervisor': {'next': 'FINISH'}}\n", + "---\n" + ] + } + ], + "source": [ + "for s in research_chain.stream(\n", + " \"when is Taylor Swift's next tour?\", {\"recursion_limit\": 100}\n", + "):\n", + " if \"__end__\" not in s:\n", + " print(s)\n", + " print(\"---\")" + ] }, { "cell_type": "markdown", @@ -245,7 +530,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 9, "id": "1bcdbf44-9481-430c-8429-fa142ed8a626", "metadata": { "ExecuteTime": { @@ -254,7 +539,76 @@ } }, "outputs": [], - "source": ["import operator\nfrom pathlib import Path\n\n\n# Document writing team graph state\nclass DocWritingState(TypedDict):\n # This tracks the team's conversation internally\n messages: Annotated[List[BaseMessage], operator.add]\n # This provides each worker with context on the others' skill sets\n team_members: str\n # This is how the supervisor tells langgraph who to work next\n next: str\n # This tracks the shared directory state\n current_files: str\n\n\n# This will be run before each worker agent begins work\n# It makes it so they are more aware of the current state\n# of the working directory.\ndef prelude(state):\n written_files = []\n if not WORKING_DIRECTORY.exists():\n WORKING_DIRECTORY.mkdir()\n try:\n written_files = [\n f.relative_to(WORKING_DIRECTORY) for f in WORKING_DIRECTORY.rglob(\"*\")\n ]\n except Exception:\n pass\n if not written_files:\n return {**state, \"current_files\": \"No files written.\"}\n return {\n **state,\n \"current_files\": \"\\nBelow are files your team has written to the directory:\\n\"\n + \"\\n\".join([f\" - {f}\" for f in written_files]),\n }\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\ndoc_writer_agent = create_agent(\n llm,\n [write_document, edit_document, read_document],\n \"You are an expert writing a research document.\\n\"\n # The {current_files} value is populated automatically by the graph state\n \"Below are files currently in your directory:\\n{current_files}\",\n)\n# Injects current directory working state before each call\ncontext_aware_doc_writer_agent = prelude | doc_writer_agent\ndoc_writing_node = functools.partial(\n agent_node, agent=context_aware_doc_writer_agent, name=\"DocWriter\"\n)\n\nnote_taking_agent = create_agent(\n llm,\n [create_outline, read_document],\n \"You are an expert senior researcher tasked with writing a paper outline and\"\n \" taking notes to craft a perfect paper.{current_files}\",\n)\ncontext_aware_note_taking_agent = prelude | note_taking_agent\nnote_taking_node = functools.partial(\n agent_node, agent=context_aware_note_taking_agent, name=\"NoteTaker\"\n)\n\nchart_generating_agent = create_agent(\n llm,\n [read_document, python_repl],\n \"You are a data viz expert tasked with generating charts for a research project.\"\n \"{current_files}\",\n)\ncontext_aware_chart_generating_agent = prelude | chart_generating_agent\nchart_generating_node = functools.partial(\n agent_node, agent=context_aware_note_taking_agent, name=\"ChartGenerator\"\n)\n\ndoc_writing_supervisor = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following workers: {team_members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"DocWriter\", \"NoteTaker\", \"ChartGenerator\"],\n)"] + "source": [ + "import operator\n", + "from pathlib import Path\n", + "\n", + "\n", + "# Document writing team graph state\n", + "class DocWritingState(TypedDict):\n", + " # This tracks the team's conversation internally\n", + " messages: Annotated[List[BaseMessage], operator.add]\n", + " # This provides each worker with context on the others' skill sets\n", + " team_members: str\n", + " # This is how the supervisor tells langgraph who to work next\n", + " next: str\n", + " # This tracks the shared directory state\n", + " current_files: str\n", + "\n", + "\n", + "# This will be run before each worker agent begins work\n", + "# It makes it so they are more aware of the current state\n", + "# of the working directory.\n", + "def prelude(state):\n", + " written_files = []\n", + " if not WORKING_DIRECTORY.exists():\n", + " WORKING_DIRECTORY.mkdir()\n", + " try:\n", + " written_files = [\n", + " f.relative_to(WORKING_DIRECTORY) for f in WORKING_DIRECTORY.rglob(\"*\")\n", + " ]\n", + " except Exception:\n", + " pass\n", + " if not written_files:\n", + " return {**state, \"current_files\": \"No files written.\"}\n", + " return {\n", + " **state,\n", + " \"current_files\": \"\\nBelow are files your team has written to the directory:\\n\"\n", + " + \"\\n\".join([f\" - {f}\" for f in written_files]),\n", + " }\n", + "\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\")\n", + "\n", + "doc_writer_agent = create_react_agent(llm, tools=[write_document, edit_document, read_document])\n", + "# Injects current directory working state before each call\n", + "context_aware_doc_writer_agent = prelude | doc_writer_agent\n", + "doc_writing_node = functools.partial(\n", + " agent_node, agent=context_aware_doc_writer_agent, name=\"DocWriter\"\n", + ")\n", + "\n", + "note_taking_agent = create_react_agent(llm,tools=[create_outline, read_document])\n", + "context_aware_note_taking_agent = prelude | note_taking_agent\n", + "note_taking_node = functools.partial(\n", + " agent_node, agent=context_aware_note_taking_agent, name=\"NoteTaker\"\n", + ")\n", + "\n", + "chart_generating_agent = create_react_agent(llm, tools=[read_document, python_repl])\n", + "context_aware_chart_generating_agent = prelude | chart_generating_agent\n", + "chart_generating_node = functools.partial(\n", + " agent_node, agent=context_aware_note_taking_agent, name=\"ChartGenerator\"\n", + ")\n", + "\n", + "doc_writing_supervisor = create_team_supervisor(\n", + " llm,\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following workers: {team_members}. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\",\n", + " [\"DocWriter\", \"NoteTaker\", \"ChartGenerator\"],\n", + ")" + ] }, { "cell_type": "markdown", @@ -266,7 +620,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "id": "9c5c644f-8966-4d2e-98d2-80d73520e9fe", "metadata": { "ExecuteTime": { @@ -275,11 +629,57 @@ } }, "outputs": [], - "source": ["# Create the graph here:\n# Note that we have unrolled the loop for the sake of this doc\nauthoring_graph = StateGraph(DocWritingState)\nauthoring_graph.add_node(\"DocWriter\", doc_writing_node)\nauthoring_graph.add_node(\"NoteTaker\", note_taking_node)\nauthoring_graph.add_node(\"ChartGenerator\", chart_generating_node)\nauthoring_graph.add_node(\"supervisor\", doc_writing_supervisor)\n\n# Add the edges that always occur\nauthoring_graph.add_edge(\"DocWriter\", \"supervisor\")\nauthoring_graph.add_edge(\"NoteTaker\", \"supervisor\")\nauthoring_graph.add_edge(\"ChartGenerator\", \"supervisor\")\n\n# Add the edges where routing applies\nauthoring_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\n \"DocWriter\": \"DocWriter\",\n \"NoteTaker\": \"NoteTaker\",\n \"ChartGenerator\": \"ChartGenerator\",\n \"FINISH\": END,\n },\n)\n\nauthoring_graph.add_edge(START, \"supervisor\")\nchain = authoring_graph.compile()\n\n\n# The following functions interoperate between the top level graph state\n# and the state of the research sub-graph\n# this makes it so that the states of each graph don't get intermixed\ndef enter_chain(message: str, members: List[str]):\n results = {\n \"messages\": [HumanMessage(content=message)],\n \"team_members\": \", \".join(members),\n }\n return results\n\n\n# We reuse the enter/exit functions to wrap the graph\nauthoring_chain = (\n functools.partial(enter_chain, members=authoring_graph.nodes)\n | authoring_graph.compile()\n)"] + "source": [ + "# Create the graph here:\n", + "# Note that we have unrolled the loop for the sake of this doc\n", + "authoring_graph = StateGraph(DocWritingState)\n", + "authoring_graph.add_node(\"DocWriter\", doc_writing_node)\n", + "authoring_graph.add_node(\"NoteTaker\", note_taking_node)\n", + "authoring_graph.add_node(\"ChartGenerator\", chart_generating_node)\n", + "authoring_graph.add_node(\"supervisor\", doc_writing_supervisor)\n", + "\n", + "# Add the edges that always occur\n", + "authoring_graph.add_edge(\"DocWriter\", \"supervisor\")\n", + "authoring_graph.add_edge(\"NoteTaker\", \"supervisor\")\n", + "authoring_graph.add_edge(\"ChartGenerator\", \"supervisor\")\n", + "\n", + "# Add the edges where routing applies\n", + "authoring_graph.add_conditional_edges(\n", + " \"supervisor\",\n", + " lambda x: x[\"next\"],\n", + " {\n", + " \"DocWriter\": \"DocWriter\",\n", + " \"NoteTaker\": \"NoteTaker\",\n", + " \"ChartGenerator\": \"ChartGenerator\",\n", + " \"FINISH\": END,\n", + " },\n", + ")\n", + "\n", + "authoring_graph.add_edge(START, \"supervisor\")\n", + "chain = authoring_graph.compile()\n", + "\n", + "\n", + "# The following functions interoperate between the top level graph state\n", + "# and the state of the research sub-graph\n", + "# this makes it so that the states of each graph don't get intermixed\n", + "def enter_chain(message: str, members: List[str]):\n", + " results = {\n", + " \"messages\": [HumanMessage(content=message)],\n", + " \"team_members\": \", \".join(members),\n", + " }\n", + " return results\n", + "\n", + "\n", + "# We reuse the enter/exit functions to wrap the graph\n", + "authoring_chain = (\n", + " functools.partial(enter_chain, members=authoring_graph.nodes)\n", + " | authoring_graph.compile()\n", + ")" + ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 11, "id": "58e7d1e48a9c39a5", "metadata": { "ExecuteTime": { @@ -290,7 +690,7 @@ "outputs": [ { "data": { - "image/jpeg": "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", + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCADuAiYDASIAAhEBAxEB/8QAHQABAAIDAQEBAQAAAAAAAAAAAAYHBAUIAwIJAf/EAFgQAAEDBAADAgcIDgUKBAcBAAECAwQABQYRBxIhEzEIFBUiQVHUFjJVVmF0k5UjMzZCUmJxgZGUsrPR0wkXU1RzJCU1NzhDcnWSoTSCg8EYJmSForHSw//EABsBAQEAAwEBAQAAAAAAAAAAAAABAgMEBQYH/8QANREBAAECAgkCAwgBBQAAAAAAAAECEQMSBBQxUVJhkaHRIUETcbEFFSMyM4HB8OEiQ1PC8f/aAAwDAQACEQMRAD8A/VOlKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClYV4uzFkgLlPhawCEoaaTzOOrJ0lCB6VE9B/7CtIMXeyIdvkTzjjaxtNoZd5YzQ9SynRdV6+YlHqSO87aaImM1U2j+7Fs3Mi/2uI4W37lEZWOhS4+lJH5ia8vdVZfhiB+so/jXlGwvH4bQbj2K2MNj7xuG2kfoAr19ytl+B4H6sj+FZ/g8+y+h7qrL8MQP1lH8ae6qy/DED9ZR/GnuVsvwPA/Vkfwp7lbL8DwP1ZH8Kfg8+x6HuqsvwxA/WUfxp7qrL8MQP1lH8ae5Wy/A8D9WR/CnuVsvwPA/Vkfwp+Dz7Hoe6qy/DED9ZR/GvpvJbQ6sJRdYS1HuSmQgk/96+fcrZfgeB+rI/hXy5iNieQUOWW3LSe9KorZB/7U/B59k9G1SoKSCCCD1BHpr+1GV4RHtqi/j7pscnZV2TI3FdJ9DjPdrfpRyq/G762VivRurbzUhjxO4xVdnJilXMEK9CkK0OdtQ6pVobHQhKgpKcaqItmom8dy25tKUpWlClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUEYf1d+IDMZYCmbPDTMCTv7c+pxtCvVtKG3h/6lSeozFT4lxHuIUFctwtjC2jroSy44HOv5Hmuny1Jq3422mI2Wj/AD3uslKUrQit4HhC4JeZN8jWq7vXSTZ48mTITEt8pxC0sHld7JYaKXilRCSGio7IGq0nDfwnMZzPhBHzy6pl2GMllhU1l23y1JZcdOkNtKLIMjZIAU0FAkj11COFbF6s/FN/H8PsmWWjh9KZuL1ztuUW4sRLbKU4FNqgPHqtDq1uKLaVLSAeYcpOhHsYvOb2TwaMXxC347mGP3fHX4FsyN2Jal+NeJBa0SF29RBD6tISeZvmISvY69wXxF8IHAJmB3PMm8hQMdtb6Y0+S5GeQ5EdUpCQh1lSA4g7cR75I6KB7utRPNPCwxbGnMRchR7ndIF8u67auUizzx2SEMF1TrSRHJfB22E8mwoKUpJUEK1Rl1wW8XHCOOsOBiuZPQr7KsEu2IyBiRKmTmkOstvKJWVrJHZKJQshaUcpKUjuv7wi4VxjP8N8jgWa4XyJjuTNzp8W0x1SJKY6oshkuIaT5y+VTqNhIJ1s66UFuwZjVxhR5TPP2L7aXUdo2ptXKobG0qAUk6PcQCPSK96wrLdE3u0QrgiPJiIlMoeEeaypl9sKAPKtCuqVDeiD3Gs2gVGMh1acpsFzb0nxp02yT3+ehSFrb/KUuJAHqDivXoyeoxmI8buWMwU7LjlyS+dD3qGm1rKj6hzBCfyqFdGB+e3tafosbUnpSlc6FKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoNPkdmduTcWVCU21dIDhfird2EKJSUqbXrryKSSD36PKrRKRXixcLVmdun2ifGbWpxlUe4WealKlBtYKVJWg7CkKBI2NpUO4kVvq1l6xq25Alvx6KHXG99m+hSm3W/XyOJIUn8xFbqaqZjLX7e+5fmhbfg38KWXErRw4xdC0kKSpNpYBBHcQeWv4PBs4UJII4b4sCOoItDH/81IPcMtsFLOSX5lHoT42lzX51oUf+9PcTI+NV++mZ/lVl8PD4+0lo3pRSov7iZHxqv30zP8qsG+4rMttkuEtrKb6XWI7jqOZ1kjaUkjf2L5KfDw+PtJaN6bUqi/Bsm33ixwPxPLb3lF2RdbpHW6+IqmkNAh1aRypLZI6JHpqzPcTI+NV++mZ/lU+Hh8faS0b2rvXAXhvkd1lXO64HjtxuMpZcflyrYy466o96lKKdk/Kawz4NnCc9/DfFj/8AaGP/AOakHuJkfGq/fTM/yqe4d5XReT35afSPGG0/90tg/wDenw8Pj7SWjeyIUPHOGmOR7fAiRLFaGCpMaBBYCE8ylFZS00gbUpSio8qQSST0JNf2x26RKuT18uLPYS3W+wjRidmKxsKKVEHXaKUApWunmoSObk5le1pxG12eUZbTK5E4ggzJjy5DwB7wFrJKQenmp0Og6dBW5qTVTTExR7+/9/v8vkUpStCFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFanLPuVvPzJ79g1tq1OWfcrefmT37BoKi8B//AGVOHnzJz9+5V51RngP/AOypw8+ZOfv3KvOgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgVqcs+5W8/Mnv2DW2rU5Z9yt5+ZPfsGgqLwH/APZU4efMnP37lXnVGeA//sqcPPmTn79yrzoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFK1OQ5AixMMhLKpc2Svso0VB0XFaJOyeiUgAkqPcB0BJAMfN+zA9RbrIjf3vjrytfJvshv9AroowK8SM0bOc2WybUqEeXcw/uFj/W3v5dPLuYf3Cx/rb38utmq1746wWTevz/AP6U/gs/dLZYOJ8Btbvk5tNouYHUNsqcUthzXoAccWkn0lxFdk+Xcw/uFj/W3v5daPObTfeImH3jGbzarI/a7rFXFfQJbwUEqGuZJ7LooHRB9BANNVr3x1gs/PD+jZ4F/wBYnF5eZXGPz2TE+V9vnTtLs1W+xA9fJoubHUFLfrr9YqoLwf8AhZePB84awsRs8azTEtOuSJM5191Dkp5Z6rUA3oaSEpH4qE9T31Y3l3MP7hY/1t7+XTVa98dYLJvSoR5dzD+4WP8AW3v5dPLuYf3Cx/rb38umq1746wWTelQkXzL9jcCyAenUt7+XW4x7JXblJdgXCKmBdGkB0tNul1p1s9OdtZSkkA9CCAQddNFJOFej10Rm9J+UwWb6lKVzIUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgUpSgheXH/53xgejsJp/Ppn+JrY1rcu+7jGPm83/APxqB+EBeMxseI2+TiHjjWrk0m6ybZCbmzY8HlX2jjDCwQ4sK7PppR5SohJIr1NmFR8v+0sp2Qs6lUdhHEyffOIPD23QctRlWP3XHblOfnohNseNvMyI6ELUkJBbUgLWhSByje9pBGhEI/EzOsmyWyWONlTlp8oZ5kFkclNQIzi0Q4rT62W0hbZG09mNKIJPerm6g680MXUFK5ZybjlnGBDJsLXKOTZNGyK2WW3XpmEwl5bU1hT4UpgrbZU8hLTiR5yEKKmyQOoPnkvEXi/hHDXiHcJzd3YZgWpqXab5f4VtalNyu2CHGi3GccaWgpIUFFCSPOB30NM8DqqsS4XiBaVREzpsaGqW+mNHEh1LZedIJDaNnzlEJUQkdeh9VU/kl2zHhNeMMu1/zBd8x6fdDbr2h2FGjsxDIbCYy2ylAWltD6eXz1qOnxzE8oNQpzLssyGLwqy6fdgq15Jm6vErPItsVaWrc40+qIoLU0XEuBtgLC0qCv8AKFgnQACah1BSuYbRxmyzC/d1Nzm9yUZHaLbc7lGw5+1NMRJDLKipl6JLSOZ5vkCQvalKBWSQnl67y0ZXn2D5Dw5cyTK2cnhZmHI8mEm3Mx02+R4qqQhUdTYClNjkUghwqOiDsd1M0DoKtSydcS7IB6bVP39LEqsPBguOZZlw0xzMctyxy8SLtbkuG3NwY7DDZKvNc2hAWVlI6+cEecdJGhVnNf6zLH/yq4fvYdbaJvEzyn6SsJ1SlK8pClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClK+XHEMtqWtQQhIKlKUdAAd5JoPqlQbOuNuE8N7Na7tf7+xGt10lCFCkMIXIQ+8SQEJLYV6j1PQaOz0rIGfzl8T1YmnEb34giJ4yvJFNIFv5iOjSVc3MpXeCNdPz7oJjSqhUxxly/hnmEWQ7YcGzB1xScflwFmWhtscpSX+dChtWlJJSk8oXsJ2nr+b928OXiBJvOKx84tAmXLDZW+yRJlW95+S2pH/AIxtK+VxaVtdQUDvWNdTQfpxkWSWi48ULNaYl0hSrpAiylS4TMhC3o4WGuQuIB2jm0dbA3o6pnOGrza2R4jeQXrG3WJAkImWOSll4kJUnlVzoWlSCFHaVJI2Ae8CuWeCvhW47xPz2HfnuG3uQym/SfFGZsMmUq/lDaUOo7TskFBaHYEBRKTzEFST39VnMWEHSrVfUq9IFllK1+cNkfoNerRTOJhURT62j+Zn+WVrxFkGZ8HDHrbasaj2e7XyxT7CZXi93hSkGW6JK+eSHi42tC+0XpR2noQOXlqG3/waXbXdsCgYxcr8za4uQ3O83K8i4NeOxFSIroCkrWnawXFJT1SskKPNsbNXV7s43wZfvqSX/Kp7s43wZfvqSX/Kq6vXwyZZ3IS14NuJHDLrYJj11ub9znIukq+y5hNyVMRy9k+l5IHIpvlSEhICQBrWid+8rgPCu2CZFi16yvKL/GviG2n5lymtuPtJQdgNANBtHy+Zs+nfSpf7s43wZfvqSX/Kr4ezmFHZW67b7400hJUta7LLCUgdSSez6Cr8CvhkyzufWf4NauJeGXfF740p613NgsPBtXKsDYIUk6OlJICgdHqBWLknDe0ZMjFG3S9CYxq5M3OCzDKUI52mnGkIUCk+Zyuq6DR6Dr68BvjTij2LHJm5ctzHA0p83hFvfVE7MEhS+2COTlBB2d66Vs7fxCtl1gRpsKJeJkKS2l5iQxZ5S23W1AFK0qDeikgggjoQafBr4ZMs7kVheD5Y/dGu73q837LSI0uJGh36Yl9iK1JAD6GwEJUQpICfPUrSeg1Xzh/g82PEr5arm5esgv6rNHci2eNeZqXmbY2tPIoMgISSeTzOZZWoJ6bqZ+7ON8GX76kl/wAqnuzjfBl++pJf8qnwK+GTLO58cPMGgcNMIs2LWx2Q/b7VHTGYclqSp1SR3FRSlIJ/IBUfz/ihjvCrPMQnZLMcgw7i1LtjDrcdx77O45GKEkISogHlI3rWyPXUkTmMZSgBbL7snXWySwP3dQXi/wAWl8Gsbc4kXPE7tdLVDT5ObhsNoS8kPFKlPvBX2poKZbbG9kqX1SBolNM4VMzXFvSe8WLTG1cbeYWF3JXcdRereq/tNB9y1CUjxpLZGwstb5uU+vWq3FfmXdf6TCxqy5WU2vgva4+T9gY3luTcQqStsgeYopYSojoOhUeg6arqHwdeGs+9eDC27bply4eXnMP88odiTlTzb0LcC2CyXVK0lbKW1KSTvbqwdHoPIYuk6VW1zs/E+0IwGLYr5Y7zHhltjKJl9jrbkTm/sQW9HSz5qHNB1XKo8u1JGzrrtLdl2VOcQb/abjhTsHFYUYSIGRtT23zOVyo5mvFkjtEKBU5rfeEfLQTWlVpZvCExKdw8Xml4NwwyzNSxCeGTw1QnmnSUgBSDvoSoDYJHf16GrAh3aDcW4zkWYxIRKZEhhTTgUHWiAQtOu9JBHUdOooMulKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKUClKw7xebfj1rlXK6zo1tt0VBdfly3UtNNIHepS1EBI+UmgzKVXmQ8dsXsdoxm6RjPyKBkcgRre/YIa5qXDvRUSjuSOpJ+Q+qs6Pl+Uv8AFSVjysKdaxJiGHhlSp7fI6+QkhlMfXP6V7VsjaR66Ca15PSWY3J2zqGu0WG0c6gOZR7kjfeT6qq61YPxIyThzfbNmmax7bf50sLi3bD2DHVCjgtns0dqCSTyrBURsBw9egNbWZwMxW+TMHuWQMSsjvmHtJRbbvcJS/GC4A3t53kKUuLJaSolSSNk9OpoNizxbxSbkWQY/Bu7VxyCwxlSp9qiArkNIA33dxJ2ABv0j11FhxcyvM+FzOTYHw/uD11kTPF27RlixaXUsgkGQQebafekAdSD8mqs+NaoUOVJlR4bDEmUoKfebaSlbpAABWQNqOgB19VR/PeKuH8LoHjmWZJbbAyRzIE2QlC3P+BHvln5EgmgwLjZ89ncRceuUTILfa8OjxT5UshhB6RKkFKxpLx1yISVNkEdSUHYIOqw7LwPtEBzOk3W7XzKoeYLX49b77OMiPHZUXPsEdOgWmwHSkAHegnr0FV3/wDFRe+IR7LhHwzvmYMr6Iv94T5JtWvw0uOjncA7ykJBqE8S4ecRrZbbhxi4rzrHa7tPFtjY1wttzoLr55iWjK5VOnohfMkgDzFa60Fy5DxD4QeDfjlvstxu1kxiFbgVQrQ2e1fb5ioktsJ5nOpWrqB9939arnLPC0ymfZXbniGAmw4+khPut4jyk2e3gn3pQyT2rwPo5dE+qpBwx8H+Fwv4qy2bDw8x234k3C50ZTJlLlXuTLVykkKcCihIPPsbG9pIPQpqcYhwRt1qwdWN5ZdZ3Ettyd5QdkZZySiXRy8oSkp0lCSnaU9dbPo6UFETOG3EDiFxMsuNcScoy3JbHcoRnyHcLbRasditkL5WXHwovSCooHQ8p0tB6jeqVj/0Y+Q3TjnIaujlttPDdSjLTJssha19nzkJipQ8pTiXSkBRUSpACuilEclfpM00hhtDbaEttoASlCRoJA7gBX3QaTEcMsmCY9a7HYbc1b7XbI/isRhG1FpvoSOZRKjsgEkkknqdmt3SlApSlArlD+kX45/1W8F143bpHZX7LOeEjlPnNxAB4wv84Ulv/wBQke9rq+uc/Cd8DvDePt0Vl2Q3W/w7ha7SqKwxbZDKGClCnXQVJWys8xLhBII6AerdBzt/RmcYImQ2PIuDWSBqZBfZel2+PJ6oeZWOWVG0ehBB5wn08zprtTg3kU67Wy+2qXhisJi49dXrPboiEER5ENoJDT7B5Ejs1DYASNDl1XKHgV+Bjh8rDuHXFxu/ZLDycEzizHkxxFJS6tBbKSwV8i0ApUOfZClaIrq3IWrnj3Fm1ZHPzaJa8OkwRZRYJ5Q2mRcXHgWVtKJG3FAcnL5xIB0OpNBYdKUoFY9wt8W7QJEKbHalw5LamXo76AttxChpSVJPQggkEGsilB+ZHHP+jcvcPjBZGsCbdewjILglp50AOLsSTtTinApaedtKErUk8wJIDZPOpBX05H8HHihwfYbTwj4pPSbRHSEs4tnDXjkRKAOjbb6AHGkAdAlIH5a6bpQc0p8LDJeGpDHGThfeMVYR0XkVhHlO1kfhrUjzmh+KeZVXRw+4uYZxWgeN4jk1tvzQTzLREfBdbH47Z0tH5FAVLSAQQRsH0GqZ4g+CDww4gT/KhsJxrIEq5273jTpt8tC/w9t+apXyqSo0FuXO1Qr1DXEuEOPPiL9+xJaS4hX5UqBBqNX3hFiGS5Zj2S3Gxsv3vHhq1ygtaPFR16JSlQSR17iCP0VTfuC8IPhH52K5nbeK1kb7rTlzfi1wCfwUS0dFq/Gd0PkrKtvhoWTH5zNr4p4pfuFN1cVyJcu0YyLe6r1NymgUqHykAfLQWXbuF8+wXbM7rbM1v7sy/trVFjXZ9MuFaXyFcq47Gk6SCUkoJO+Xv6mtLOTxfwzhbbm4KrLxFzluWRMdlAWxh6MS4QUBOwFgdkOvTqo+gVY2O5PZ8vtbVysV1hXm3O+8lwJCH2lfkUkkVs6CE3DiJOtvE6z4icQvkuJcIhkLySKwFW6K4A4S06skFJ+xjXQ7LiB69eOKccsHzRzLUWy/MkYnIXGvTsptyM1DUkrBKnHEpQU/Y1nmSSNDe+tTysC82G25FbJluukCNcYE1osyY0ppLjbyD96pJGiPkNB6Wu7Qb5b2Z9tmR7hBfHM1JiupdbcG9bSpJIPUHurLquMl4A4lkWO45YmWZlgtePyRKt8exy1w0tq2SUkJ70nauh9ZrZpw3JEcU15GM1le5lcPsDiphNFntQBp4Pe/B7yQO/p16dQmlKqy3X3izjGCZLcsjx+w5XkMWTu02nF5LkfxqMSkfZHJHRLgBUToaPJ02SKy7lxsiYvHwNvJLDeLVdctU2y3CYiqlJgvq7MdlIcQNIPM6kbPTor1GgsilR638QsZuuY3LE4l9gSMmtrSX5dpQ+kyWWyEELUjv5dON9e7z0+ut+haXUJWhQWhQ2FJOwR6xQfVKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKUoFKVoMiz7HsTsd4u91u8aNbrOnnuDwV2niw/HSnah392qDf0qs7vx1t6LVhd0xuwX7NbZlLyUxp1jhFbMZklO35HNyqbQAonqn70jpW2gT8+f4p3SJLtdnjcPmoY8TuCJC1XB+SQ2TtGuRKB9lHXqdJO+8UE2rXXDIrVaZ0KFOucOHMmr7OLHkSENuPq/BQknaj8g3VcR+DmRZPwwuuKcQeINxyCXcJgkG7WOOmzvMsgtkR09mVAp2hQJPVQcI79GpSOE2JuP4xKmWSLdLhjTCI9rn3BAfkRgkJAUFq683mJPN3760Gut/G3HL/keY47YjLvOQ4swXJ0BmK42S5olLKFrASpZ0B0JHnDrWmuWWcVMt4XQLpiOJ23FsulS+Ry25i+pbcaN547VRjnfMdIIT6NkEdKthKQnegBs7Oh3mv7QQifhGQ3DifZsmGZzYVggw1MvYrHYR4tJfUHAXVunzzrnRpOuhbB9JFeOOcD8Qxo5d2VvduDeVvqeuzN1kuS2n9lZ5OzcJSlADihygAa0DvQqe1hXi927Hbc9cLrPi2yAyNuSpjyWmkD1qUogD89B9Wm0QLDbmLfbIUe3QI6eRmLEaS002n1JSkAAfkFZdUBd/DMxKdcHrTw9tF84qXps8qmsZhqXFaV6O1kq02lP4wKhWH5H8Init1uN2sXBuyud8W1oF2uvL6UqdVplJ9SkdRQXjleaWDBLWq5ZHeoFigJ75FwkoZQT6gVEbPyDrVJyPDCh5c+5C4T4TkPE+UlRR49EYMG1oUOmly3gAPzJIOjo1qLnwJ4N8HrJc88zNNz4l3O1uIZlXO/uuXqUHlKSENBkAoSoqcRoFI1zJ2QOtWuL3mdyvuDuYpY7VEwOXETKujl1LkedHQpvbbLTCRpCxzJJ5ug5VJ83oaCrr1i3GjMLVKuvETiVZ+EeKsoLsmHigHjDbXp7We/0aUPwkDVYWLcLOGuEzMLvmE8PJvFiTk0gKczGdJROMVkKSFyXXpCtJVpRKUoSkq5FDYPfcmOcHYVpm5m9eL1dswiZQ/wA8i2ZA8mTDjM7XysMtFOktgL0Qd70N1OocKPbojMWIw1FjMpCG2WUBCEJHcEpHQD5BQQdjDMquuT5cMjyWPOwu5xTBt9ihwuwdjIUgBxxcgK5ys7WNDoBojR2K3fD/AIeY/wALsTg43jNvFus0LmLEftFuFJUoqUoqWSoklSiST3k1I6UClKUClKUClKUClKUCtTlv3KXr5k9+7VW2rU5b9yl6+ZPfu1UFReA//sqcPPmTn79yrF4pYZYc0xJ5rIbI7kEW3OJurEKOtSHlSGNrb7MpUnz9jQ84A70ehNV14D/+ypw8+ZOfv3KvOgi/DDPonFLh/YsrgxpMKLdYyZCY0tHK60eoKVD5CCNjoe8dDUoqE4c3mcPNMwRk0+2ysekSml44iOOSShnsh2yHU8o3pfcrmUT1J5RoVNqBSlKBSlKBSlKBWNcrZDvEF6FPiMTobyeV2PJbDjax6lJIII/LWTSgoDIvAtwdd0dvWDy7twtyBXXx3E5ao7Sz6AuP1bKfxUhO61on+Ebwi6S4Vj41WJv/AH8Mi03cJ9ZQdsr0PQnaj666RpQUXiPhlcO75dE2XIZE7h3knQKtGYRVQHAe7o4r7GQT3ecCfVV4R5DUtht5hxDzLiQpDjagpKge4gjvFafLsHx7PrWq25JY7ffYCt/5PcIyHkg+sBQOj8o61SL/AIHzeFvuTeEWd3/hnIKivyah03C1LV3nmivE9T6wrpvoKDoqlc5Ditxv4U+ZnfDljPLQ376+4C6VSAn1qhO6WpWup5CEipzw38J/hrxTlCDZ8mjx70Fci7NdAYc1C/SjsnNFRHp5eYfLQWpSlKDV+5ez+VpF0Frhoukhkxnp6GEpkONHXmFwDmKeg6b9A9VQBnwdccx3hrPwrCZ134e2+VM8fEnH5y0yGntpJ5Vuc5CVciQU9xGwNbq06UEDuON5zGv2IeRsph+56A0li8xblC7WVPAAHapeB81Z11GtbJPyV/IOV5vGvmYJvGHMosFtZVIs823Tw/IuWgT2RY5QULOhrZ1tQHXrU9pQVQvwkcXsPDO25vmkS8cPrdNmm3iLkUBbclt/awAptvnISezUQo9NaJ1urE909n8rsWo3SGi6vsiS1AW+lMhbR3pYbJ5inzVddeg+qs6TGZmx3GJDSH2HByrbdSFJUPUQehqP3Hhti92za15hLscN/KLY0piHdlN/5Q02pK0lAUO9OnHOh2BznXfQSWvkuIB0VJ3+Wq2tnBNvFomeHG8ovttuWVqcf8ZlyjMbtshRcJdjNL6I2p0kjejyp9QqI8ULhfuH/BDKJvlo3DJrLYJDpvJjIQX5TUdRL/ZdUJ5lp5uTqkb11FBe3ao/DT+mnao/DT+muQeFWR3PJIibtH45N5m9Etqpc2xxodt8xSmVa5yy2Fp5VkHvGynR6EitFC8IrI8btHBW7Xkm62q/WCTOyN9DCErZCPFf8rASkaS2XVFSUjXIpR15o0Hbfao/DT+mnao/DT+muN+J/F/J7GxxjdtN4S21Ym7Eu0rQwy4lkSVJ7UglJ5woH77evRqttcsvybI+Ot9xNGdM4K3bREctdsXbmHl3lpbYW84FvdVAK5m9NEFPLs0HWXao/DT+mnao/DT+muWYXHe043xk4jWDMcvtVmt9vNuNqjXKQxGIDkbne5SdKX5xB6k63oarf+DxnU7iLgUm8zri3dea8XFiPKaSgIVHblOIZ5eQAEcgTpXee8k99B0VSlKBSlKBSlKBSlKBVM+Dzj+KwYmd2+ycORg8aLkEi2vNOoUpu6JZ1yykFSQChXOoADYGu/0Vc1QrHoWaxuJ2Wv3e5wZWFSmYhscJCQmTFcSgiTzaQCpKlFJBKla1oACgmgAAAA0B6K/tKUClKUClKrLjXx1tXB+DCioiPZFmF2UWbLjNv86VOd9euvI2O9ThGgAe89KDbcXOMOOcFsWVesgkL24sMQrfFT2kqe+fessNjqtZOvkHeSBVQ4pwLvXHe9s5zxxgtLYRtVjwArK4drbP+8kjuekKHfvoNka7kokPCPgTdV5SOJXFSWxf+IjyCIkVrrBsDJ/3EVJ2ObR853vPXR71KvSgwrRZrfj9vZgWuDGtsFkcrUWGylppA9SUpAA/NWYQCCCNg+g1/aUFPDBZHAdm2RuFOCQJlru9+7fIGBOLLrLbvQvtBfmkIJSSnfRKOUJ67TbEK5RLl2/ikpmV2DqmHexcC+zcT75CtHooekHqKyaqfIcDlcJLVleRcJ8Pt1xym+T2Z9wt0mcqM1L0dOqRvaEOFJUd+aCpRUebXKQtila6236FcpcqA3MiLusJLZmwWZCXHIpWnmSFgdRsbIJA2BsVsaBSlKBSlKBSlKBSlKBSlVtxp4+YxwQtcdd2ceuF7nq7K14/bUdtOuDpOgltsddb6FR6Du6kgEJvkeSWrD7HMvN7uEe1WqG2XZEuU4ENtp9ZJ/QB6SQBXNL2Q5x4YjrkPGHZ+AcG1EtyMgUgtXO/o7lIipI2yyrqCsjZHr85Azsb4GZZx6vkPMOOPIxbI7gkWjh1Fc5oUT8FcxQ+3u6+9PmjqO4lA6XaaRHaQ00hLbaEhKUIGgkDuAHoFBpsJwuz8O8UtmN2CGIFntrIYjRwtS+VI69VKJJJJJJJ2STW8pSgh+UcKMcy/NcYyy4RXvL2OrcVAlx5C2SErTpbawkgLQeh5T6R6ioHWYRn2QN266L4kWm24U+zeFW+A6LkhyPcG1kdgptR0eZQUE8pAJUknQ3yiw6j2dcPsc4m487Y8ps0W+WpxSXDGlo5gFjuUk96VDZ6gg9T66CQ0qt5F9yvCcyym65NJsMPhPBtaZcSW2HETIa20jtUuJ0QpGgtQKevvQATuptjuR2vLrHDvNkuEe6WqY2HY8yK4FtuJ9YI+XYPqIINBsqUpQKUpQKUpQKUpQKUrX3+/wBuxayzbvd5rFutkJpT0iXJWENtIA2SSaD1u12hWK2SrjcZbMGBEaU8/JkLCG2kJG1KUo9AAB3muRZmER/DtziDkE6xM2vhDZHyYlweiJbuWSOJ2klLhHaNRR1GgQVfl+176Dabz4aV3Yu17Yl2LgfDeDtvtDgUzJyZaTtL746FEbY2lHervPo11DDhsW+IxFisNxozCEtNMsoCENoA0lKUjoAAAABQIcNi3Q2IsVlEeMw2lppptPKlCEjSUgegAACvalKBSlKBSlKBSlKBVMcdbBPyvhlntltTHjVzuNrnRIrHOlHaOraWlCeZRAGyQNkgVc9YDlkhuuKWpslSiVE8x7zQVJY8VFq4fMW9u3sRrl5JTGcQ0lCSXA0ElJUOh87071VV8OuE+RWl7gui7WhCY1hxedbLuhx5pxLLzqIwS2QFHnCuzcG08w6dT1G+rfIEL+yP/Wf408gQv7I/9Z/jQcVXrwbckx7FOLdksaVXmDe/JBsTTshCVtMsPEqjFS1DQaSNJKj1RygEkECZeEFjuX8TYdyxCBw+iS47nZG2ZdJujKBb3fNJfDeu2QtCt65N82h1AJFXhesiTaeKWN4i3jM6XDu0STJdvbal+Lwy0AQ2vzSNr302od3pqZ+QIX9kf+s/xoOfcH4czIPFniPeb3bWZMC6eSxBmPhpZf7KL2bp5dko8/1gb9GxWbwCw66YPhc+3XaEID673cpbTKXELHYuynFtKHISBtCknXeO4gVevkCF/ZH/AKz/ABp5Ahf2R/6z/Gg2NKUoFKUoFKUoFKUoFUfxKyLhXw8494vlOW5qzj2Wu2l20xIMiUhthyKtwuFx/afsaedCglalISVAjziNC8K/Frww/Bkm+DlxGdaitOv4fdFKftM1W1cqd+cwtX4aNgfjJKVekgB+t3CnjVhfG6zS7rhN8bvcKI/4s+sMOsKbc5QrRQ6lKtEKGjrR0QDsHU3rmb+ju4enBPBmssp5otTcgkPXd0KHXlUQ2119RbbQof8AEa6ZoFKVRHFPjferzlb/AA04TMsXbOOUeU7w8OeBjrR6do+eoU938rI2djZGhohteM/HpzDLtFwrC7YnLeJtzRzRLOhWmobfplTFj7W0ne+pBV0A1vY9OCnANHDybNyvJ7kct4lXdP8AnPIJCejaf7vGT/umU9AAAN6BOhpKdxwX4H2XgzaJSYzz95yK5r8YvGRXA88y4vnvUtR2QkEnlQDpI9ZJJsagUpSgUpSgUpSgrzKuHTVjm5TnGD4/aTxMn2zxRqVOUttqUUHmQl3lPXqEjm6E8qQVAAa32I5U7dIFqh35uFZcwftzc6ZYG5qH3Y4PmqI1oqQF7Tzga302aktcteGJxr4feD3c7VmDllg3ri+mE7GsTRJC2WXNpU9J5SD2IPOEg+colaUFP2RaA6UhZHabnd7laYd0hS7pbOy8egsSELfidonmb7VAPMjnSCU8wGwNjdbGvx78Efwj73ZvCwh5Pld4euK8seNsu8ySoed2pSGla6JQlC0tAAABCElKQB0r9hKBSlKBSlKBStXk2UWjDLFMvV9uMe02qGjtH5cpwIbbT8pPpJ6Ad5JAHU1zI9lmfeGM+5Cw5ydw94PFZbk5OtBaud8QDpSIiT1aaPdznqf+puglXEzwlLjd8qk8O+DdtZzDOUHs51xcJ8lWMb0VyHR0UsdfsafSCOpHIZBwW8G23cNrpIyzIrk9m3Em4Dc7Jrina0bGi1HR3MtgdAE9ddOg0kTnhnwtxjg/isfHsTtTNqtrPnKCBtx5etFxxZ6rWdd5+QdwAqWUClKUClKUClKUHy60h5tbbiEuNrBSpChsKB7wRUAueC5Ha8owtWF3m3Y5h1r7WPc8bFuR2MhhSdpLRTotrSpIA1oaWoneuVVg0oIZwz4tY9xahXeRYXJXNabg9bJsebGXHeZebPUKQsA6I0ofIobAOwJU5cojVxYgLlMonPtOPtRVOAOuNoKEuLSneylJcbBI6ArTv3wqtfCTEe2cIb7fHMuueCps3LdlXSzqaDzq2h5jJS6Qh3tDyoDalDnUUJ3rofyxf8MDJrl4UULi/IDrPi8htlNqRIU4hu3gcioySrQ2pBWokBKS4tSwlO9AP2hpWvsF9g5RYrdebZITLttwjty4z6O5xpaQpCh+UEGthQKUpQKUrRZvm9j4c4tcMiyO4s2qzwW+0fkvHoB3AAd6lE6ASNkkgAEmg98ryu0YNjs+/X64MWu0QWi9IlyFaQhI/wD2SdAAdSSAASa5ysGMXvwwb7DyrMoUmy8IYTokWLFZHmO3tQO0TJqf7L0oa7j3nY6r9sVwi++FVkUDOeIdvetHDqC6JON4TKGlTD95Nnp7iSOqGjsAHrsElzpwAJAAAAHQAUHy00hhpDbaEttoASlCBoJA7gB6BX3SlApSlApSlApSlApSlApSlArGuVyiWa3Sp8+UzBgRWlvyJUlwNtMtpBUpa1EgJSACST0AFZNa7I8fg5Zj10slza8YttziuwpTW9c7TiChad+jaVEUFMXrwhMcXxSxt+28XOH7eDNxJIu8Zy/w/GHHyB2BQeYnQO9+cPz1b2K5rj2d25y4Y1frZkMBt0sLlWqY3KaS4ACUFTZICgFJOu/Sh66/DzidwVvvDvjRcuHRjOzLq3cEw4KUp86WlxQ7BaR3eelSDr0E69Ffs7wF4SwuCHCfHsPh8i1wY4MuQgaD8lXnOudeuionW+5ISPRQWBSlKBSlKBSlKD4ddQw0txxQShCSpSj3ADvNQdF+yTIGm51tk261W95IWw3KhrkvLQRtK1FLqAnY68oB1sbO9ipPlJ1jF3+ZvfsGo/jZ3jtr+atfsCu/ApiKJrteb29fVlHpF3jz5j8O2j6nc9ppz5j8O2j6nc9pqC2vwm+Gl3yBNmZyXspypi7egy4MmMwuQlRQWkvONpbUrmBAAUdnu3Vo1uiuJ2RHSPBdqOfMfh20fU7ntNQrjBwjm8ccGl4rlNztb9ufUlxDrFpWh6O6n3rjajIPKobI7iCCQQQSKsytJbc0s13yC+2SLNC7pY+x8oMKbWjsA6grbPMoAKBSCdpJA0QdEEVc/KOkeC7BxqxZJiWOWqx2282hq3WyI1CjNm0OEpabQEIH/ifUkVsufMfh20fU7ntNat3inizON49fzdkLtOQPxo1rkNsuL8ZckfaUhKUlQKvlA5dHm1o1t8oye24Zj0++XiSYlrgtF6Q+G1uciB3nlQCo/kANTPyjpHgu0+U2bNslx6fa2syjWRctoteULZaiiSyD3ltSn1BKtbG9bG9jR0RpuF3C2bwexVqwYzPs8SIlRdeectLi35Tp98684ZO1rV6SfkA0AAJs3f7a9dxakTmFXMxhN8TCx2oYKuUOFPeElQIBPpB9RrPq5+UdI8F2o58x+HbR9Tue01k2/I7ra7jEi3xcOXHmOBhmZDZUx2bpGwlaFLXsK0QFA9DoEddjOrQ5cSlFl0SP87wh0/xk1lERizkqiPXlEfQibzZYNKUrx2JSlKBX8JCQSToDvJr+1h3f/RM3/AX+yasRebCIt5DkGRsIn2mTb7bbXkhcbxqIuS662dFLh5XUBIUOoT1OiNkHaRH8zwCVxFt6IOUIxbIIiCVNtXHHS8Gye8p5pB5T07xo1vsLJVh1hJOyYDGyf8NNeEPPsfuGP3a9xrk2/bLS7KZnPNpUSwuMpSX0qTrm2koV011GiNggn16rUVTTTEWjlE/WGUzZzNln9HHhGSzTKiTEY24Vc5TaIzoRv5EuvuAfkGhXTVuj5nAt8aKrJbdMUy0lsyJFoWXHSABzrIkAFR1s6AGz3CtlZLzDyKzQLrb3vGIE6O3KjvcpTztrSFIVpQBGwQdEA1/JV8t8G6QbbImsM3CeHDFircAcfDYBcKE95CQRsju2PWKxz8o6R4LsbnzH4dtH1O57TTnzH4dtH1O57TW3qrcw8Jrh3gV9fs98u8+FPZeTHUhNknuoU4obShLiGFIWTv70mpNcRtiOkeC6d8+Y/Dto+p3Paac+Y/Dto+p3PaaxsIzuzcRLJ5WsT0h+D2qmeeTCfiL5k635jyEK11HXWqyLbl1pu+RXmxRJfa3Wzhgzo/ZrT2IeSVNecQEq2Ek+aTrXXVXPyjpHguq7iP4PM3i5mFqvWXZSi9262EORsaft6k2vtR/vFspeBcV/xqI7xrR1VnsN5bFYbZZvNlaZbSEIbRZlpSlIGgABJ6ACt1SmflHSPBdqHMhyDHGHJ92k2+521lJXJ8ViLjOtIGypwczqwoJHUp6HQOiTpJnKVBQBBBB6gj01BcyJTiF8IOiIL+iP8NVS6y/6GgfN2/2RXPpFMTRFcRabzH0Sdl2bSlK4UKUpQKh9xyO63S4y4tjXDiMQ3Cy9MmMqf53QNlKEJWjQTsbUT1OwB02ZhVf4mSRetnf+d5g6/wCKquzR6YmKq5i9lje9ufMfh20fU7ntNOfMfh20fU7ntNaLiBxkw7he/Dj5JeUwpcwKVHiMsOyZDiR3qDTSVL5R+FrXy1lWvijjF5kY0zDufbO5Gw/KtafF3U+MNshJdPVI5OXnT0XonfTejXTnjdHSPC3a7iBw6lcVceVY8tVjt+tZcDwjy7I4eRwAgLQoSQpCgFKHMkg6URvRNc9ZB/RrYLenSqJPVYkn7y3tPKA+mkOH/vXYFKuflHSPBdWvCDhfkPBjA4GI2nLmrlbIJX4su6WwuutpUoq5ApLyfNBJ0COm9b0ABM+fMfh20fU7ntNZMi+W+Ld4lremsN3KW247HiKcAddQjl51JT3kJ5k7Po5h66iWe8c8H4ZXJm3ZFfkRLk6126YUeO9KfDe9c6m2ULUlPf5xAHQ9elTPEe0dI8F0k58x+HbR9Tue0058x+HbR9Tue017Y7kNuyyxwbzaJbc+2Tmkvx5LW+VxBGwRvr+mtjVz8o6R4LtRz5j8O2j6nc9pqvs44KXLiRmlhv8AkmSRrtGsh7WFYX7YrycmR6JC2g9txwDoOdRA9AGzVsUpn5R0jwXajnzH4dtH1O57TTnzH4dtH1O57TW3rBvN8t+PQfHLpNYt8XtENdtIcCE861BCE7PpUpSUgekkCmflHSPBdjc+Y/Dto+p3Paac+Y/Dto+p3Paa29KZ+UdI8F2qRPy6Ce2cl2q7IR1VEagrjLcHqS4XlgHv1tOiddR31LbRdI97tcS4RFFcaU0l5tRGjyqGxv5a09ePC4lWAWUk7PYnqf8AiNacamKsPPa0xMR6em2/g2wlNKUrz2JSlKBSlVdxoy16ImLj0N0tOy0F+YtB0oMA8oQD6OdW+v4KFj0g106No9Wk4sYVPur0yfjXGhPuRbDDF2dQSlUtxzs4wO9EJIBK9fIAPxqiK+L+ZLUSHbK0nfRIgOnQ+Ul/r+gVE0pCEhKQEpA0ABoAV/a+7wvszRcKm2S8759Uzbkq/rczL+82f6vc/n0/rczL+82f6vc/n1FaVu1HRf8AjjomaWlyKwOZRxfx3iVPRa15PYo640ZaYSw0tJ5uVTiO185SOdZSdjRVvrpOrE/rczL+82f6vc/n1Fa8LhPYtcCTNlL7KNGaU86vRPKhIJUdDqegPdTUtFj/AG46GaUx/rczL+82f6vc/n1/RxdzJOz29mUfQDb3dfv6hVquca92uHcYTvbQ5bKJDDnKU86FpCknRAI2COhG6yqRoWizF4w46GaVl2Ljk426lvILallonRnW8laEfKts+cB8qSs/IO+rWjSWZsZqRHdQ+w6gONutKCkrSRsKBHQgjruuXqnPB/KnbRf0Y+84VW+fzripJ32L4BWpKfUlaQpWvQpJPeo14f2j9lYdOHONgRa22OSxN120pSvkRq8q+5i8fM3v2DUexr7nLV80a/YFSHKvuYvHzN79g1Hsa+5y1fNGv2BXo4P6M/P+F9nKyZFqj+BJxMN3DamFzb+3HS513KVOfEfl/G7Ut616QK21qi5JxBlZ8nIczv2MTMMtkBiKxa56oqGnjb0SHZUhI6PczilDS9pCUEa3s1ato8GXhpZMiF8j4yly4JlrnoMuZIktNyFKKlOoadcU2lXMSQUpGj3araZtwMwfiJePKl/sSZs4spjuuNyXmBIaSSUtvJbWkPJBJ81wKHU1hllFL8O8lv8A4RGR2eBf8hvGMxI2HWu9Kh2KYqA7PkyuftHy4jS+zR2YAQDra+u+6sfwgIdz4ZZVbW7JImTpWd48MJTMfV2jxnB5AjyXVAAFQaflKKun2v8ANV65lwWwvPnba7ebIhci3NFiJIhSHYbrLX9kFsLQrk6e83y/JW4awLH2bdj0BNsa8Ux9xD1raJURFWhpTSCnZ66QtQG99++8A1cs2sOXuGWPyYnFyy8JlofVZ+GUq5Xtlx/ag6w8hPk7zvSpAlyB+VmsEWu9J8CB3PpOc5fIytdhTMTOTf5TYQoLHIAhCwNgaBUdqV12Tuuu2sZtbF+n3puG2i6T4zMSTJG+ZxpouFtJ9HQuude/r17hrSK4T4qrhr7gDat4l4p4j5O8Yd+0/g9pz9p+fm38tTKKsfwuJdPDLkTHbheGXW8Shz0tRrrIZaUtExxHIptKwlTWkpJbI5CVKJG1EmvcPPF/i3aZGa2Kd4pdV3aQiP2+VvMw4qGZSm/FnbamIpsjkRykqWVq5ufmGwB0tlXCnFs0v1rvd2tinrtbByxZjEp6O4lHMFcii0tPOjmSDyL2nforVu8AsCezBWT+QEt3hctE9a2ZT7bLklJBS8thKw0pwEA85QTsb3urlkQ3hTa7hk/FviZc7lkl8fiWTIxGt1qTcXURGQYTClbbB0tJLmwhW0pIKgASSbUy/wC12X/nEL9+mvexYjacanXqZbYni0i8y/Hpy+0Wvtn+zQ3z6USE+a2gaTodO7ZNeGX/AGuy/wDOIX79NdGBFq4ZU7YWFSlK8hiUpSgVh3j/AETN/wABf7JrMrDvH+iZv+Av9k1lT+aFjah2FfcbYfmEf92muO4qLhw4wLixmUFL8zHrxe8jtmQQW9rLCzIfRHnIT6OUqDbgHegpVr7HXYmFfcbYfmEf92mvOzYJYbBaLpa4VubRb7nJky5kZ1SnUPOSFKW+VBZPRRUrae7roACvTxoviVfOSdrnjhvDuPE+62HFJmS3vHLJY8Ess2LHsU9UJ2U9IbUFvrcR5yko7JKQg+bskkHdRnF25HGPJOAd2yG9XdVwlQMghuzrZc34JkiK6lDbyeyWnlU6lPMop1zjQOwlIHReR8BcEyuFZ4tysfO3aYYt0NceY/HdRGAA7FTjbiVrb0keaskH095rLyPgxhmVWKy2efYmk2+ykeTW4TrkRUQBPJptbKkqSCnoQDojv3WjLKKEuS+JfF3OOIyrFMfhHH7su0W5DGVu2tEPkZbUh5yKiK6mQFqWV7cUQR5oCeXZnXHBN1RivBtN8Uwu9jMbEJyou+yMjzu0KNgHl5ubXQdNVMsm8HzAMvvHlW64+H56mW47zrUt9nxltA0hL6W3Eh4AdPsgV06VK8kw6z5ei1pu0PxtNrns3OIO1WjspDRJbX5pG9bPQ7B9INXLPqOYPCIy3IBNz/JMJm5Iy7haWhLmLyAxbay+ltt0tNwg2oSdoWnn7Tl6r0lVa3iDmV5tPG/iDEjmfZMYuUjHUZDlFvdCHLZFUypICFAhSC4taUl0fa0cyu/WuiMp4AYBmt5uN0vWPNzpVxbDc1KpDyWZPKnkSpxlKw2paU6CVlJUnQ0Roa2jHCbFI8e/MeSQ81fYTNvuSZD7r3jLDTRabQrnUe5CiOYaJ3sknrUmmbih8lcz3iXxdzuxWSRLZgYr4nDhsRstetC2+1jJd8YcSiM8ZBUpRALiuXTeuUnZOytthzDK+MdjxjMssukVyNgrEq6R8cuT0RmVNEtbZeCkcik7HU8oST0B80aNm37wd+H+TLt7lxsS3n4MJFubkInyW3nIyRpLTziHAp5I9ThV6fWalUPB7JAyVu/x4IauzduRaUPpcXpMVKytLYRvl6KJO9b9G9Vcsj0zIaw++D/6B/8AdqqX2X/Q0D5u3+yKiOZ/cfffmD/7tVS6y/6GgfN2/wBkVcf9Gn5z9IX2ZtKUrz0KUpQKr7Eve3v/AJvM/fKqwar7Eve3v/m8z98qu/R/yV/svsqvhoqOvwpOMXlApN6TGtKbeHffC3+L7V2W/vO3LnNr77W/RWPxqsDeacduEdvRd7hbGHIl97STZ5XYPkJRF2gOp85HUaJSQroRsdasTiBwWwzijLhzMksiZs6GktsTGJDsWQhB70dqytCynv8ANJ11PTrWTZuE+JY67jjlssrMA46zIYtaWFrSmOh/lL3mhWllZSCSoE72d7J3bTsRz9Gy26TcLODv3XKL7kCM0uVitLlvu/iEuWxGSpzcqZoqCENq2pSRzqKEdDs1p4OSZtcsDs2PTsmutruMPiknG3Z0a5F+V4n2SllpcjkR22ucjmWjrypJGxXRV04G4TeYEmJKsyi2/dXb2pxmY+08ia4NOOodQsLbKh0IQoDXTVfNp4D4JYm2G7fYEQ2mbozekNNSXggTWm+zQ/y8+irl99v356q5j1rHLIqrLeFdtZ8IvhlbvLWTFpNjuykvKyGYX1FD8dYBc7TmIPaqChvzkpQDsITrbcD3ltZTx1fbjx5ueN5I/qNLd7JbkUR2/J6FL5VFDRTsBWjrajo61VpZ5wsxjiYLf7oraZrlvWtyI+zJdjPMlY5VhLjS0qAUAAU70dDYOq1WccBMD4jXZi6X6wpk3RlnxcTo8p+K+pv0IWtlaFLT8iiRVy2m8Cj8i40ZdxRs/DO1We3Jxx7JLjd4s6LDvq4ZJgKUgMtzUsKUjnIUvzWwohvlBGya9r9auJeIY9ZbTfMimWuJc81tkOC7Bvjk+cxEeStL7LklbDZcBV5yedKiN9SeUGrxncD8EuGEQcQcxqGjHoDgeiRI/MyY7oJPaNuIIWhe1KJWFBR5js9TX3buC+HWqzW61R7QpMO33Ru9MByW+4sTEe9eU4pZWsj8ckHQ2DTLI5+zvOcn4To4p45ZL7cZceLNsDUCdebgp923JnrU2+fGHQ4oJHZ7SpQXyFe9HWj/AHM7DxU4bcOeIl0fusq22NGNSFNpXlki7TGZyVJLbzLy47S2hy9oCAojfKQBqujrhwxxe7SskkTrOxNXkcdmLdUySpxEppkLDSShRKU8vOrqkA9dnqBrQ2vwesBs+P3yyR7I4bde4whT0P3CU8t1gb5Ww4twrQkcytBJGtnVMsiBZ7Z1Yti2M46xes0v+V5TNSWhFyFyGuQ43HWt4qeOxGYCQVlLKQSQkAHrVXZAu9ZX4PF9teVXK4rmY3xCiWpDqLs446GhMigJckJDZeKA+rS1JB2lCtBSQR1dmfDbHOIFthQb5b1SmYLofiuMyHY70dYSUhSHWlJWk8pIOlDYOjutRD4E4Hb8XvmOR8dYbsd7Wly4QO1cLbziUpSHNFXmr8xBK06UVJCiSrrSaZEAvlhl3rjJZ+GgyfI7XjVtxlV4DkO7vInT5CpRa07KKi6pLadHXN1Lid7AAqAWHJMmzufwvx2Tl15ajDJshssm6wJRYeusSI292S1qRoEkIA5wNggqSQrRq+Ll4P8Agt3sdptUu0SHo9qLphP+U5YlM9oduASA72pSo96Ssg6HToK3Fu4V4paPcuINmZhJxgO+SG46loRF7VstuaSDpRUlStlQPUk9/WmWRILRbUWa1Q4DT0iQ3FZQyl6W+p95YSAApbiyVLUdbKiSSepr04W/6v7L/gn9o1714cLf9X9l/wAE/tGs8X9CfnH0lfZKqUpXmoUpSgVz7xRUtXEq68+/NjRko3+DpR/aKq6CqoeN+NuNTIeSsoKmUtCFO194jmKmnD8gUpaT/iJPQA17f2PiU4elRFXvFv3/AL6LyVvSvCemSuE+mE40zLKFBlx9suNpXroVJCklQ33gEb9YqJm38Q/RfsZ+pJHtdfdVVTTsiZ6NaZ1zdhJ4j8QbRCy63yuxmSZinNvZA4IrbaHylUdUIRygaSko9/zb87m9FXFHgZ8l9sv3zG1shQ50t2aQlRTvqATLOjr06P5K/jfCLEmcjN9atCWriZHjZLb7qWi9/alkK7Mr315uXe65MXDrxpiYvERzt+/vs/bbtVUV+m3hjEuJGVt5HeUXDH7++iAwmasRkNIW0ezU171aSFqGlb0Na1W3yhibxBncUVTL9dLbGx1lUSHbbdKLCCDFDpdeA+2c5UQArppJ6emrTk8O8el2W92l2389vvUhcqez2zg7Z1fLzK2FbTvlT0SQOlYeTcI8TzC6uXK62kPzXWfF3nWpDrPbN9wS4G1pCwN9OYHVaZ0bEta9+V55+Y6D34Wf6scQ/wCTw/3CKlFQ33P5XaW2oFgudhgWWK0hiJGlWuQ+602hISlKnBKTza136H/vQ2/iFoavuMg667ssj2uuymqqimKcs+ny8omVZFnUtGUY4pskOC6RgND0FYCv/wASqtRY2rozASm8SYcubzHbsGOthsj0DlUtZ3/5qnvCfHHL9l7VzUk+T7QVK5/QuSpBSlHy8qVqUfUSisNJxacLArrr3T/4yp2r4pSlfmSvKTHRLjOsOjmadQUKHrBGjUDYbvuMRmbcbDMvbMdAaamwHWB2iANJK0uuoIVrv1sE9djehYNK34WNOHeLXjn/AIst0A8uXn4mXv6WF7TTy5efiZe/pYXtNT+lb9ajgjv5L8kA8uXn4mXv6WF7TTy5efiZe/pYXtNT+lNajgjv5L8kA8uXn4mXv6WF7TTy5efiZe/pYXtNT+lNajgjv5L8kA8uXn4mXv6WF7TTy5efiZe/pYXtNT+lNajgjv5L8kA8uXn4mXv6WF7TWRDtdzyS4wXZ1tds1vhvJk9nJcbW8+4keYAG1qSlIJ2SSSSANempvSpOlTb/AE0xE/v/ADJcpSlcSFKUoFfLjaXW1IWApCgUkH0ivqlBX0aLesTiM2xNkl3uNFQGo8uE8yFLbSAEhxLriCFgdCRsHW9jfKPry5efiZe/pYXtNT+ld2tTPrVREz+/lb8kA8uXn4mXv6WF7TTy5efiZe/pYXtNT+lNajgjv5L8kA8uXn4mXv6WF7TTy5efiZe/pYXtNT+lNajgjv5L8kA8uXn4mXv6WF7TTy5efiZe/pYXtNT+lNajgjv5L8kA8uXn4mXv6WF7TTy5efiZe/pYXtNT+lNajgjv5L8lfSI16yuI9bFWSXZI0pBakS5rzJUhtQIV2aWnFkrI6AnQG97OuUz9ttLLaW0JCUJASlI9AFfVK0YuNOJaLWiN3+blylKVoQpSlAqETLXc8buM52DbXbzAmPKk9nGcbQ8w4oeeNOLSlSSRsEEEEkaPfU3pW7DxZwpm0XusSgHly8/Ey9/SwvaaeXLz8TL39LC9pqf0ro1qOCO/kvyQDy5efiZe/pYXtNPLl5+Jl7+lhe01P6U1qOCO/kvyQDy5efiZe/pYXtNPLl5+Jl7+lhe01P6U1qOCO/kvyQDy5efiZe/pYXtNPLl5+Jl7+lhe01P6U1qOCO/kvyQDy5efiZe/pYXtNPLl5+Jl7+lhe01P6U1qOCO/kvyQDy5efiZe/pYXtNPLl5+Jl7+lhe01P6U1qOCO/kvyQDy5efiZe/pYXtNPLl5+Jl7+lhe01P6U1qOCO/kvyQNE7ILgexj4xMt7q+gk3J6P2Lf4xDTq1K136AG9a2ne6llgs7WP2SDbWVqcbispaDi/fL0Oqj8pOyfy1sKVpxMacSMtoiOV/wCZkuUpSudClKUCvh1pD7S23EJcbWClSFDYUD3gj0ivulBT2TcFJcV1T2NPsLjei3TVqTyfIh0A9PUlQ/8AMBURcwXMGlFJxWYvX3zcqKUn8m3gf0iuj6V7uF9s6ThU5ZtV8737TCubvcTl3xTn/rET+dT3E5d8U5/6xE/nV0jSt/37pHBT38npuc3e4nLvinP/AFiJ/Op7icu+Kc/9Yifzq6RpT790jgp7+T03ObvcTl3xTn/rET+dX0nB8vXsDFJwPo5pMUD99XR9KffukcFPfyem5SNh4M3u5upXeX2bRD35zMZfbSFj1c2uVH5Rz/m76uO02mJYrdHgQGExojCeVtpHoHyk9SSdkk9SSSeprLpXk6VpuNpc/iT6R7RsClKVwo//2Q==", "text/plain": [ "" ] @@ -299,11 +699,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ndisplay(Image(chain.get_graph().draw_mermaid_png()))"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "display(Image(chain.get_graph().draw_mermaid_png()))" + ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "id": "9860fd46-c24d-40a5-a6ba-e8fddcd43369", "metadata": { "ExecuteTime": { @@ -311,8 +715,29 @@ "start_time": "2024-05-15T08:19:53.709307Z" } }, - "outputs": [], - "source": ["for s in authoring_chain.stream(\n \"Write an outline for poem and then write the poem to disk.\",\n {\"recursion_limit\": 100},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'supervisor': {'next': 'NoteTaker'}}\n", + "---\n", + "{'NoteTaker': {'messages': [HumanMessage(content='The poem has been written and saved to \"poem.txt\".', name='NoteTaker')]}}\n", + "---\n", + "{'supervisor': {'next': 'FINISH'}}\n", + "---\n" + ] + } + ], + "source": [ + "for s in authoring_chain.stream(\n", + " \"Write an outline for poem and then write the poem to disk.\",\n", + " {\"recursion_limit\": 100},\n", + "):\n", + " if \"__end__\" not in s:\n", + " print(s)\n", + " print(\"---\")" + ] }, { "cell_type": "markdown", @@ -328,7 +753,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "id": "95ae7e52-92ed-41a3-88c4-21b6d7c8b041", "metadata": { "ExecuteTime": { @@ -337,11 +762,26 @@ } }, "outputs": [], - "source": ["from langchain_core.messages import BaseMessage\nfrom langchain_openai.chat_models import ChatOpenAI\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\nsupervisor_node = create_team_supervisor(\n llm,\n \"You are a supervisor tasked with managing a conversation between the\"\n \" following teams: {team_members}. Given the following user request,\"\n \" respond with the worker to act next. Each worker will perform a\"\n \" task and respond with their results and status. When finished,\"\n \" respond with FINISH.\",\n [\"ResearchTeam\", \"PaperWritingTeam\"],\n)"] + "source": [ + "from langchain_core.messages import BaseMessage\n", + "from langchain_openai.chat_models import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o\")\n", + "\n", + "supervisor_node = create_team_supervisor(\n", + " llm,\n", + " \"You are a supervisor tasked with managing a conversation between the\"\n", + " \" following teams: {team_members}. Given the following user request,\"\n", + " \" respond with the worker to act next. Each worker will perform a\"\n", + " \" task and respond with their results and status. When finished,\"\n", + " \" respond with FINISH.\",\n", + " [\"ResearchTeam\", \"PaperWritingTeam\"],\n", + ")" + ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "id": "4880e573-612f-4d24-97c1-2079382a4a2f", "metadata": { "ExecuteTime": { @@ -350,11 +790,50 @@ } }, "outputs": [], - "source": ["# Top-level graph state\nclass State(TypedDict):\n messages: Annotated[List[BaseMessage], operator.add]\n next: str\n\n\ndef get_last_message(state: State) -> str:\n return state[\"messages\"][-1].content\n\n\ndef join_graph(response: dict):\n return {\"messages\": [response[\"messages\"][-1]]}\n\n\n# Define the graph.\nsuper_graph = StateGraph(State)\n# First add the nodes, which will do the work\nsuper_graph.add_node(\"ResearchTeam\", get_last_message | research_chain | join_graph)\nsuper_graph.add_node(\n \"PaperWritingTeam\", get_last_message | authoring_chain | join_graph\n)\nsuper_graph.add_node(\"supervisor\", supervisor_node)\n\n# Define the graph connections, which controls how the logic\n# propagates through the program\nsuper_graph.add_edge(\"ResearchTeam\", \"supervisor\")\nsuper_graph.add_edge(\"PaperWritingTeam\", \"supervisor\")\nsuper_graph.add_conditional_edges(\n \"supervisor\",\n lambda x: x[\"next\"],\n {\n \"PaperWritingTeam\": \"PaperWritingTeam\",\n \"ResearchTeam\": \"ResearchTeam\",\n \"FINISH\": END,\n },\n)\nsuper_graph.add_edge(START, \"supervisor\")\nsuper_graph = super_graph.compile()"] + "source": [ + "# Top-level graph state\n", + "class State(TypedDict):\n", + " messages: Annotated[List[BaseMessage], operator.add]\n", + " next: str\n", + "\n", + "\n", + "def get_last_message(state: State) -> str:\n", + " return state[\"messages\"][-1].content\n", + "\n", + "\n", + "def join_graph(response: dict):\n", + " return {\"messages\": [response[\"messages\"][-1]]}\n", + "\n", + "\n", + "# Define the graph.\n", + "super_graph = StateGraph(State)\n", + "# First add the nodes, which will do the work\n", + "super_graph.add_node(\"ResearchTeam\", get_last_message | research_chain | join_graph)\n", + "super_graph.add_node(\n", + " \"PaperWritingTeam\", get_last_message | authoring_chain | join_graph\n", + ")\n", + "super_graph.add_node(\"supervisor\", supervisor_node)\n", + "\n", + "# Define the graph connections, which controls how the logic\n", + "# propagates through the program\n", + "super_graph.add_edge(\"ResearchTeam\", \"supervisor\")\n", + "super_graph.add_edge(\"PaperWritingTeam\", \"supervisor\")\n", + "super_graph.add_conditional_edges(\n", + " \"supervisor\",\n", + " lambda x: x[\"next\"],\n", + " {\n", + " \"PaperWritingTeam\": \"PaperWritingTeam\",\n", + " \"ResearchTeam\": \"ResearchTeam\",\n", + " \"FINISH\": END,\n", + " },\n", + ")\n", + "super_graph.add_edge(START, \"supervisor\")\n", + "super_graph = super_graph.compile()" + ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 15, "id": "270ff3ae26cd42ff", "metadata": { "ExecuteTime": { @@ -365,7 +844,7 @@ "outputs": [ { "data": { - "image/jpeg": "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a3Q3CIi1l7Pw7/XdlH8DRf1ysFV9w7/XdlH8DRf1ysFe9t/wrf8AbT/jDy2VfxqhERZtUREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERBTWD/rTtn8F/7laCo4DcN6qeSabA8dlmkcXvkfbIS5zidkk8vUkqy4OENrpImxU9zvEELd8sbKzTWj2Dov7/AAVUP2xe/wBN/sXLvezpuXq7tF3DOmZ2T0y7sZbZwiJhV7+APDSR7nvwHHHPcdlzrZCST7fqqaWy2Ullt1NQUFNFRUVNG2KCngYGRxsA0GtaOgAHqC3n4KqH7Yvf6b/Yn4KqH7Yvf6b/AGLXn2VVVtvR3SmMusxspa1FsvwVUP2xe/03+xVF5OtLW8Svwnd9Xu6P7gzi52Gi7Go5NUsHZdmHdPSd6Z2fWseZ/wDtjullzha3SstRfJ+FuHZrcGV2QYvaL1WsjELaivoo5ntYCSGhzgTrbnHX3lTr8FVD9sXv9N/sT8FVD9sXv9N/sUx7JmmcYux3SicvtTqmJVd/e/8ADPWvoBjevZ3XD/8AqpLi+F2DCKOWkx6y0FkpZZO1khoKdsLHv0BzENA2dADf3KWfgqofti9/pv8AYn4KqH7Yvf6b/Ysp9lV1RhN76sYy2zGuKXk4d/ruyj+Bov65WCtBjGGUWKTVs1NPV1M1XyCWSrm7R2mb5QOnT6xW/XbimKKaaInHCIjuiIci9XFy5NcdIiIikREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBc7+Rv/lx/wBqV8/qF0Qud/I3/wAuP+1K+f1CDohERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBc7+Rv/lx/2pXz+oXRC538jf8Ay4/7Ur5/UIOiEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQERfCdDZ8EH1FB6/iYJ5DHYbebswHRrZZexpT97H6cZB97Wlp9TvZrTmmWuOxSWVn70yTO1/Hof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", + "image/jpeg": "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AREQEREBERAREQEREBERAREQEREBERBrvmn/ABy8Of8AlS5frGrYha75p/xy8Of+VLl+satiEBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERARFDp8vutzkkfYqCjmoWPdG2rrqh8fbOadEsY1jvM2CA4kb1sAtIcduHhVYn+K2umKKEeXcw/ILH9bm+7Ty7mH5BY/rc33a36LXvjrBZN1G+I+BWzihgl8xS8M57ddqV9NKQAXMJHmvbv8Jrg1w+doWM8u5h+QWP63N92nl3MPyCx/W5vu00WvfHWCz8Pcw4bX3C+Itwwitonvv9HXG3+LRNLjNIXcrOQd7g/bS31hw9a/bXweuFn7ivBfFMMdN4xPbKU+MSA7aZ5Hulm5f5PaSP182lVOR+DzLkvhC2Pi3U0FmF4tlP2bqMTyGGomaC2Gd5MW+djTofzYz05etx+Xcw/ILH9bm+7TRa98dYLJuihHl3MPyCx/W5vu08u5h+QWP63N92mi1746wWTdFCPLuYfkFj+tzfdrl7rr9ao3VV2tdE+gjBdM+3VEkksbR3uEboxzgDZIB5tDoHHQTRcT2t1gsmqLhFKyeJksT2yRvaHNew7Dge4g+kLmuNBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAVc8ODzcP8AHHHvNvgJ/OYwrGVc8Nv932Nf1dT/AKtq9D0/6Vf1j7VL7JGi1vsnEbMbflWZUOWZPU2a+wwXSotOOzWiFlHUU8Qc6CekquUmYtZyue1ziQSdsACzdFxLySXE/B7rX3LdTlT6QXh/YRf6UH2yWd/Tl0zcjWu8zl7td3RM0IvVFqnYeJPES1cHce4o1+ZG9U8l2ZSV9gmtlNHFLTyXA0g7N8bGyCVoc1wOy066tXys4scWs3uWV3XDLfe5Ka03aqtltt1Nb7ZJb6o08hjPjMs1QyoBe5rtmMN5A4aDyNmZ4G1qKi8duud59xkzq1HK58ZsVhNqfFb6SipZpuealbLLE6WSN3mb31Hnbd0cANHD4VxTzHNKrDcIN28UzC311fHl1dDTQkiGi97BDHMLG+MPmpngtA80v5daVzDYG2XigvUEk1uraavhjlfA+SllbI1sjHFr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", "text/plain": [ "" ] @@ -374,11 +853,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ndisplay(Image(super_graph.get_graph().draw_mermaid_png()))"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "display(Image(super_graph.get_graph().draw_mermaid_png()))" + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "6b8badbf-d728-44bd-a2a7-5b4e587c92fe", "metadata": { "ExecuteTime": { @@ -386,8 +869,215 @@ "start_time": "2024-05-15T08:19:55.796497Z" } }, - "outputs": [], - "source": ["for s in super_graph.stream(\n {\n \"messages\": [\n HumanMessage(\n content=\"Write a brief research report on the North American sturgeon. Include a chart.\"\n )\n ],\n },\n {\"recursion_limit\": 150},\n):\n if \"__end__\" not in s:\n print(s)\n print(\"---\")"] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content=\"Unfortunately, the information obtained from the U.S. Fish & Wildlife Service web pages does not provide additional detailed information on the conservation status, size, or lifespan of the North American sturgeon species beyond what was already included in the initial research report. These pages primarily contain placeholders for the species' profiles without specific information on the topics of interest.\\n\\nBased on the information available, the research report provided earlier remains the most comprehensive summary of the North American sturgeon, including an overview of the species, their conservation status, size, lifespan, conservation efforts, and a chart summarizing key data for each species. Further details would require access to additional sources or in-depth research reports that are not currently available in the provided documents.\", name='WebScraper')]}}\n", + "---\n", + "{'supervisor': {'next': 'PaperWritingTeam'}}\n", + "---\n", + "{'PaperWritingTeam': {'messages': [HumanMessage(content=\"It appears that the information you were seeking from the U.S. Fish & Wildlife Service web pages was not as detailed as you needed for the North American sturgeon species. If you're looking for more comprehensive data on their conservation status, size, lifespan, and conservation efforts, you might need to consider exploring scientific journals, research papers, or contacting experts in the field.\\n\\nIf you have any specific questions or require assistance with creating an outline or reading a document related to the North American sturgeon, please let me know how I can assist you further.\", name='NoteTaker')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='I\\'ve found several resources that could provide the comprehensive data you\\'re looking for on the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. A paper titled \"Reconnecting Fragmented Sturgeon Populations in North American Rivers\" by Jager et al., which may contain information on distribution, range contraction, and conservation efforts ([Read the paper](https://web.ornl.gov/~zij/mypubs/sturgeon/Jager at al_2016_Reconnecting Fragmented Sturgeon Populations in North American Rivers_Fisheries.pdf)).\\n\\n2. The North American Sturgeon and Paddlefish Society (NASPS) website, which lists experts and provides details on the society\\'s mission to foster the conservation and restoration of sturgeon species in North America ([Visit NASPS](https://nasps-sturgeon.org/about/)).\\n\\n3. A press release from the U.S. Fish and Wildlife Service indicating that lake sturgeon do not require listing under the Endangered Species Act due to successful ongoing management efforts ([Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n\\n4. Information on the Conservation Genetics of Atlantic Sturgeon by the USGS, discussing genetic studies and management strategies for Atlantic Sturgeon populations ([Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon)).\\n\\n5. A story on restoring lake sturgeon along the Ontonagon River in Michigan and St. Louis River in Minnesota, detailing efforts by the Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office ([Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon)).\\n\\nThese resources should provide a strong foundation for understanding the current state of North American sturgeon species. If you require more detailed summaries or have any other questions, feel free to ask.', name='Search')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.', name='Search')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n1. **Reconnecting Fragmented Sturgeon Populations in North American Rivers (Jager et al.)**:\\n - This paper discusses the fragmentation of large North American rivers by dams that interrupt the migrations of wide-ranging fishes like sturgeons. Efforts to reconnect habitats are viewed as crucial for protecting sturgeon species in U.S. rivers, as these species have lost between 5% and 60% of their historical ranges. [Learn more from the paper](https://www.semanticscholar.org/paper/Reconnecting-Fragmented-Sturgeon-Populations-in-Jager-Parsley/45414d7c86cd2d4f04b9490c7143f36b5158e729/figure/0).\\n\\n2. **North American Sturgeon and Paddlefish Society (NASPS)**:\\n - NASPS is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing and advancing research pertaining to their biology, management, and utilization. [Visit NASPS](https://nasps-sturgeon.org/about/).\\n\\n3. **U.S. Fish and Wildlife Service on Lake Sturgeon and the Endangered Species Act**:\\n - The U.S. Fish and Wildlife Service determined that lake sturgeon do not require listing under the Endangered Species Act, thanks to ongoing management efforts such as fish stocking that have contributed to their population stability. [Read the press release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list).\\n\\n4. **Conservation Genetics of Atlantic Sturgeon (USGS)**:\\n - The USGS is conducting research on the conservation genetics of Atlantic sturgeon, with a focus on genetic assignment testing and population genetic studies. This is in response to the rediscovery of populations that were previously thought to be extirpated, necessitating updated management strategies. [Learn more from USGS](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon).\\n\\n5. **Restoration Efforts Along the Ontonagon River and St. Louis River**:\\n - The Iron River National Fish Hatchery and Ashland Fish and Wildlife Conservation Office are working with partners to restore lake sturgeon in Michigan and Minnesota. Efforts include collecting larval sturgeon and milt for breeding and stocking programs. [Read the story](https://www.fws.gov/story/2024-04/restoring-reverence-along-lake-sturgeon).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n**Conservation Status of North American Sturgeon:**\\n- Lake sturgeon has origins dating back at least 150 million years and is one of the largest freshwater fish in North America. They are not currently listed under the Endangered Species Act, thanks to conservation efforts such as fish stocking ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- The North American Sturgeon and Paddlefish Society (NASPS) is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing research on their biology, management, and utilization ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- All 26 remaining sturgeon species are now threatened with extinction according to the IUCN ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The USGS is conducting research on the conservation genetics of Atlantic sturgeon to ensure appropriate management strategies can be developed ([USGS Conservation Genetics](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon)).\\n\\n**Size and Lifespan of North American Sturgeon Species:**\\n- The white sturgeon (Acipenser transmontanus), also known as the Pacific sturgeon, can grow up to 20 feet long and weigh up to 1,800 pounds. It is the largest freshwater fish in North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The shortnose sturgeon (Acipenser brevirostrum) can grow up to 4 feet long and weigh up to 50 pounds. It inhabits the eastern coast of North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The green sturgeon (Acipenser medirostris) can reach up to 7 feet long and weigh up to 350 pounds ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n\\n**Conservation Efforts for North American Sturgeon Species:**\\n- Long-term conservation efforts in North America have helped to stabilize and increase some sturgeon populations, such as the white sturgeon in the Fraser River in the U.S. ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The NASPS works to foster the conservation of sturgeon species and restoration of sturgeon stocks in North America ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- The collaborative conservation efforts, including fish stocking, have contributed to the conservation and resiliency of lake sturgeon ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- Work in the Chesapeake Bay includes identifying and protecting habitat used by Atlantic sturgeon for spawning, seeking to minimize vessel strikes, and educating students about these fish ([NOAA Fisheries](https://www.fisheries.noaa.gov/feature-story/supporting-endangered-atlantic-sturgeon-chesapeake-bay)).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.', name='Search')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n**Conservation Status of North American Sturgeon:**\\n- Lake sturgeon has origins dating back at least 150 million years and is one of the largest freshwater fish in North America. They are not currently listed under the Endangered Species Act, thanks to conservation efforts such as fish stocking ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- The North American Sturgeon and Paddlefish Society (NASPS) is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing research on their biology, management, and utilization ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- All 26 remaining sturgeon species are now threatened with extinction according to the IUCN ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The USGS is conducting research on the conservation genetics of Atlantic sturgeon to ensure appropriate management strategies can be developed ([USGS Conservation Genetics](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon)).\\n\\n**Size and Lifespan of North American Sturgeon Species:**\\n- The white sturgeon (Acipenser transmontanus), also known as the Pacific sturgeon, can grow up to 20 feet long and weigh up to 1,800 pounds. It is the largest freshwater fish in North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The shortnose sturgeon (Acipenser brevirostrum) can grow up to 4 feet long and weigh up to 50 pounds. It inhabits the eastern coast of North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The green sturgeon (Acipenser medirostris) can reach up to 7 feet long and weigh up to 350 pounds ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n\\n**Conservation Efforts for North American Sturgeon Species:**\\n- Long-term conservation efforts in North America have helped to stabilize and increase some sturgeon populations, such as the white sturgeon in the Fraser River in the U.S. ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The NASPS works to foster the conservation of sturgeon species and restoration of sturgeon stocks in North America ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- The collaborative conservation efforts, including fish stocking, have contributed to the conservation and resiliency of lake sturgeon ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- Work in the Chesapeake Bay includes identifying and protecting habitat used by Atlantic sturgeon for spawning, seeking to minimize vessel strikes, and educating students about these fish ([NOAA Fisheries](https://www.fisheries.noaa.gov/feature-story/supporting-endangered-atlantic-sturgeon-chesapeake-bay)).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='Based on the search results obtained, here is a summary of the information available regarding the conservation status, size, lifespan, and conservation efforts for North American sturgeon species:\\n\\n**Conservation Status of North American Sturgeon:**\\n- Lake sturgeon has origins dating back at least 150 million years and is one of the largest freshwater fish in North America. They are not currently listed under the Endangered Species Act, thanks to conservation efforts such as fish stocking ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- The North American Sturgeon and Paddlefish Society (NASPS) is dedicated to promoting the conservation and restoration of sturgeon species in North America by developing research on their biology, management, and utilization ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- All 26 remaining sturgeon species are now threatened with extinction according to the IUCN ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The USGS is conducting research on the conservation genetics of Atlantic sturgeon to ensure appropriate management strategies can be developed ([USGS Conservation Genetics](https://www.usgs.gov/centers/eesc/science/conservation-genetics-atlantic-sturgeon)).\\n\\n**Size and Lifespan of North American Sturgeon Species:**\\n- The white sturgeon (Acipenser transmontanus), also known as the Pacific sturgeon, can grow up to 20 feet long and weigh up to 1,800 pounds. It is the largest freshwater fish in North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The shortnose sturgeon (Acipenser brevirostrum) can grow up to 4 feet long and weigh up to 50 pounds. It inhabits the eastern coast of North America ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n- The green sturgeon (Acipenser medirostris) can reach up to 7 feet long and weigh up to 350 pounds ([American Oceans](https://www.americanoceans.org/facts/types-of-sturgeon/)).\\n\\n**Conservation Efforts for North American Sturgeon Species:**\\n- Long-term conservation efforts in North America have helped to stabilize and increase some sturgeon populations, such as the white sturgeon in the Fraser River in the U.S. ([WWF News](https://wwf.panda.org/wwf_news/?6080466/sturgeon-slipping-towards-extinction)).\\n- The NASPS works to foster the conservation of sturgeon species and restoration of sturgeon stocks in North America ([NASPS About](https://nasps-sturgeon.org/about/)).\\n- The collaborative conservation efforts, including fish stocking, have contributed to the conservation and resiliency of lake sturgeon ([U.S. Fish and Wildlife Service Press Release](https://www.fws.gov/press-release/2024-04/collaborative-conservation-keeps-lake-sturgeon-endangered-list)).\\n- Work in the Chesapeake Bay includes identifying and protecting habitat used by Atlantic sturgeon for spawning, seeking to minimize vessel strikes, and educating students about these fish ([NOAA Fisheries](https://www.fisheries.noaa.gov/feature-story/supporting-endangered-atlantic-sturgeon-chesapeake-bay)).\\n\\nThese resources provide comprehensive data on the state of North American sturgeon species and the various conservation efforts being undertaken to preserve and enhance their populations. If you require further information or assistance, feel free to ask.')]}}\n", + "---\n", + "{'supervisor': {'next': 'PaperWritingTeam'}}\n", + "---\n", + "{'PaperWritingTeam': {'messages': [HumanMessage(content='The outline for North American sturgeon species, including their conservation status, size, lifespan, and conservation efforts, has been successfully created and saved to a file named \"North_American_Sturgeon_Overview\". If you need to review the document or require additional information, please let me know.', name='NoteTaker')]}}\n", + "---\n", + "{'supervisor': {'next': 'PaperWritingTeam'}}\n", + "---\n", + "{'PaperWritingTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" contains the following outline:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need more detailed information on any of these topics or have any other requests related to the document, feel free to let me know!', name='DocWriter')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" contains the following outline:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need more detailed information on any of these topics or have any other requests related to the document, feel free to let me know!')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" contains the following outline:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need more detailed information on any of these topics or have any other requests related to the document, feel free to let me know!')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" contains the following outline:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need more detailed information on any of these topics or have any other requests related to the document, feel free to let me know!')]}}\n", + "---\n", + "{'supervisor': {'next': 'PaperWritingTeam'}}\n", + "---\n", + "{'PaperWritingTeam': {'messages': [HumanMessage(content='It seems that the document \"North_American_Sturgeon_Overview\" contains exactly the outline provided earlier, with three main topics:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you would like to delve into any of these topics or have another request regarding the document, please let me know how I can assist you further!', name='NoteTaker')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='It seems that the document \"North_American_Sturgeon_Overview\" contains exactly the outline provided earlier, with three main topics:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you would like to delve into any of these topics or have another request regarding the document, please let me know how I can assist you further!')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='It seems that the document \"North_American_Sturgeon_Overview\" contains exactly the outline provided earlier, with three main topics:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you would like to delve into any of these topics or have another request regarding the document, please let me know how I can assist you further!')]}}\n", + "---\n", + "{'supervisor': {'next': 'PaperWritingTeam'}}\n", + "---\n", + "{'PaperWritingTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" indeed contains the three main topics outlined earlier:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need detailed information on any of these topics or have another specific request related to the document, please let me know, and I can provide the information or take further action as needed.', name='NoteTaker')]}}\n", + "---\n", + "{'supervisor': {'next': 'ResearchTeam'}}\n", + "---\n", + "{'ResearchTeam': {'messages': [HumanMessage(content='The document \"North_American_Sturgeon_Overview\" indeed contains the three main topics outlined earlier:\\n\\n1. Conservation Status of North American Sturgeon\\n2. Size and Lifespan of North American Sturgeon Species\\n3. Conservation Efforts for North American Sturgeon Species\\n\\nIf you need detailed information on any of these topics or have another specific request related to the document, please let me know, and I can provide the information or take further action as needed.')]}}\n", + "---\n", + "{'supervisor': {'next': 'FINISH'}}\n", + "---\n" + ] + } + ], + "source": [ + "for s in super_graph.stream(\n", + " {\n", + " \"messages\": [\n", + " HumanMessage(\n", + " content=\"Write a brief research report on the North American sturgeon. Include a chart.\"\n", + " )\n", + " ],\n", + " },\n", + " {\"recursion_limit\": 150},\n", + "):\n", + " if \"__end__\" not in s:\n", + " print(s)\n", + " print(\"---\")" + ] } ], "metadata": { @@ -406,7 +1096,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/multi_agent/multi-agent-collaboration.ipynb b/examples/multi_agent/multi-agent-collaboration.ipynb index 5c2c89284..c660fd296 100644 --- a/examples/multi_agent/multi-agent-collaboration.ipynb +++ b/examples/multi_agent/multi-agent-collaboration.ipynb @@ -118,7 +118,7 @@ "id": "71b790ca-9cef-4b22-b469-4b1d5d8424d6", "metadata": {}, "outputs": [], - "source": ["import functools\n\nfrom langchain_core.messages import AIMessage\n\n\n# Helper function to create a node for a given agent\ndef agent_node(state, agent, name):\n result = agent.invoke(state)\n # We convert the agent output into a format that is suitable to append to the global state\n if isinstance(result, ToolMessage):\n pass\n else:\n result = AIMessage(**result.dict(exclude={\"type\", \"name\"}), name=name)\n return {\n \"messages\": [result],\n # Since we have a strict workflow, we can\n # track the sender so we know who to pass to next.\n \"sender\": name,\n }\n\n\nllm = ChatOpenAI(model=\"gpt-4-1106-preview\")\n\n# Research agent and node\nresearch_agent = create_agent(\n llm,\n [tavily_tool],\n system_message=\"You should provide accurate data for the chart_generator to use.\",\n)\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n\n# chart_generator\nchart_agent = create_agent(\n llm,\n [python_repl],\n system_message=\"Any charts you display will be visible by the user.\",\n)\nchart_node = functools.partial(agent_node, agent=chart_agent, name=\"chart_generator\")"] + "source": ["import functools\n\nfrom langchain_core.messages import AIMessage\n\n\n# Helper function to create a node for a given agent\ndef agent_node(state, agent, name):\n result = agent.invoke(state)\n # We convert the agent output into a format that is suitable to append to the global state\n if isinstance(result, ToolMessage):\n pass\n else:\n result = AIMessage(**result.dict(exclude={\"type\", \"name\"}), name=name)\n return {\n \"messages\": [result],\n # Since we have a strict workflow, we can\n # track the sender so we know who to pass to next.\n \"sender\": name,\n }\n\n\nllm = ChatOpenAI(model=\"gpt-4o\")\n\n# Research agent and node\nresearch_agent = create_agent(\n llm,\n [tavily_tool],\n system_message=\"You should provide accurate data for the chart_generator to use.\",\n)\nresearch_node = functools.partial(agent_node, agent=research_agent, name=\"Researcher\")\n\n# chart_generator\nchart_agent = create_agent(\n llm,\n [python_repl],\n system_message=\"Any charts you display will be visible by the user.\",\n)\nchart_node = functools.partial(agent_node, agent=chart_agent, name=\"chart_generator\")"] }, { "cell_type": "markdown", @@ -213,45 +213,45 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'Researcher': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_3zDlnDMUkWEJxnHASo59doCL', 'function': {'arguments': '{\"query\":\"UK GDP 2018 to 2023\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 221, 'total_tokens': 247}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-ac6640c6-2bb4-478f-b3c4-eabf98cf4900-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2018 to 2023'}, 'id': 'call_3zDlnDMUkWEJxnHASo59doCL'}])], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_3zDlnDMUkWEJxnHASo59doCL', 'function': {'arguments': '{\"query\":\"UK GDP 2018 to 2023\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 221, 'total_tokens': 247}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-ac6640c6-2bb4-478f-b3c4-eabf98cf4900-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2018 to 2023'}, 'id': 'call_3zDlnDMUkWEJxnHASo59doCL'}])], 'sender': 'Researcher'}}\n", "----\n", "{'call_tool': {'messages': [ToolMessage(content='[{\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp/timeseries/ihyp/pn2\", \"content\": \"Preliminary estimate of GDP time series (PGDP), released on 27 April 2018\\\\nPublications that use this data\\\\nContact details for this data\\\\nFooter links\\\\nHelp\\\\nAbout ONS\\\\nConnect with us\\\\nAll content is available under the Open Government Licence v3.0, except where otherwise stated Year on Year growth: CVM SA %\\\\nDownload full time series as:\\\\nDownload filtered time series as:\\\\nTable\\\\nNotes\\\\nFollowing a quality review it has been identified that the methodology used to estimate elements of purchased software within gross fixed capital formation (GFCF) has led to some double counting from 1997 onwards. GDP quarterly national accounts time series (QNA), released on 22 December 2023\\\\nIHYP: UK Economic Accounts time series (UKEA), released on 22 December 2023\\\\nIHYP: GDP first quarterly estimate time series\\\\n(PN2), released on 10 November 2023\\\\nIHYP: Year on Year growth: CVM SA %\\\\nSource dataset: GDP first quarterly estimate time series (PN2)\\\\nContact: Niamh McAuley\\\\nRelease date: 10 November 2023\\\\nView previous versions\\\\n %\\\\nFilters\\\\nCustom time period\\\\nChart\\\\nDownload this time seriesGross Domestic Product:\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp\", \"content\": \"Quarter on Quarter growth: CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: q-on-q4 growth rate CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product at market prices: Current price: Seasonally adjusted \\\\u00a3m\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: quarter on quarter growth rate: CP SA %\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: q-on-q4 growth quarter growth: CP SA %\\\\nCurrent Prices (CP)\\\\nDatasets related to Gross Domestic Product (GDP)\\\\n A roundup of the latest data and trends on the economy, business and jobs\\\\nTime series related to Gross Domestic Product (GDP)\\\\nGross Domestic Product: chained volume measures: Seasonally adjusted \\\\u00a3m\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: Hide\\\\nData and analysis from Census 2021\\\\nGross Domestic Product (GDP)\\\\nGross domestic product (GDP) estimates as the main measure of UK economic growth based on the value of goods and services produced during a given period. Contains current and constant price data on the value of goods and services to indicate the economic performance of the UK.\\\\nEstimates of short-term indicators of investment in non-financial assets; business investment and asset and sector breakdowns of total gross fixed capital formation.\\\\n Monthly gross domestic product by gross value added\\\\nThe gross value added (GVA) tables showing the monthly and annual growths and indices as published within the monthly gross domestic product (GDP) statistical bulletin.\\\\n\"}, {\"url\": \"https://www.macrotrends.net/global-metrics/countries/GBR/united-kingdom/gdp-gross-domestic-product\", \"content\": \"U.K. gdp for 2021 was $3,141.51B, a 16.45% increase from 2020. U.K. gdp for 2020 was $2,697.81B, a 5.39% decline from 2019. U.K. gdp for 2019 was $2,851.41B, a 0.69% decline from 2018. GDP at purchaser\\'s prices is the sum of gross value added by all resident producers in the economy plus any product taxes and minus any subsidies not included in ...\"}, {\"url\": \"https://www.statista.com/statistics/281744/gdp-of-the-united-kingdom/\", \"content\": \"Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nGDP of the UK 1948-2022\\\\nUK economy expected to shrink in 2023\\\\nHow big is the UK economy compared to others?\\\\nGross domestic product of the United Kingdom from 1948 to 2022\\\\n(in million GBP)\\\\nAdditional Information\\\\nShow sources information\\\\nShow publisher information\\\\nUse Ask Statista Research Service\\\\nDecember 2023\\\\nUnited Kingdom\\\\n1948 to 2022\\\\n*GDP is displayed in real terms (seasonally adjusted chained volume measure with 2019 as the reference year)\\\\n Statistics on\\\\n\\\\\"\\\\nEconomy of the UK\\\\n\\\\\"\\\\nOther statistics that may interest you Economy of the UK\\\\nGross domestic product\\\\nLabor Market\\\\nInflation\\\\nGovernment finances\\\\nBusiness Enterprise\\\\nFurther related statistics\\\\nFurther Content: You might find this interesting as well\\\\nStatistics\\\\nTopics Other statistics on the topicThe UK economy\\\\nEconomy\\\\nRPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nCPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nAverage annual earnings for full-time employees in the UK 1999-2023\\\\nEconomy\\\\nInflation rate in the UK 1989-2023\\\\nYou only have access to basic statistics.\\\\n Customized Research & Analysis projects:\\\\nGet quick analyses with our professional research service\\\\nThe best of the best: the portal for top lists & rankings:\\\\n\"}, {\"url\": \"https://www.statista.com/topics/3795/gdp-of-the-uk/\", \"content\": \"Monthly growth of gross domestic product in the United Kingdom from January 2019 to November 2023\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nContribution to gross domestic product growth in the United Kingdom in January 2023, by sector\\\\nGDP growth rate in the UK 1999-2021, by country\\\\nAnnual growth rates of gross domestic product in the United Kingdom from 1999 to 2021, by country\\\\nGDP growth rate in the UK 2021, by region\\\\nAnnual growth rates of gross domestic product in the United Kingdom in 2021, by region\\\\nGDP growth of Scotland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Scotland in 2021, by local (ITL 3) area\\\\nGDP growth of Wales 2021, by local area\\\\nAnnual growth rates of gross domestic product in Wales in 2021, by local (ITL 3) area\\\\nGDP growth of Northern Ireland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Northern Ireland in 2021, by local (ITL 3) area\\\\nGDP per capita\\\\nGDP per capita\\\\nGDP per capita in the UK 1955-2022\\\\nGross domestic product per capita in the United Kingdom from 1955 to 2022 (in GBP)\\\\nAnnual GDP per capita growth in the UK 1956-2022\\\\nAnnual GDP per capita growth in the United Kingdom from 1956 to 2022\\\\nQuarterly GDP per capita in the UK 2019-2023\\\\nQuarterly GDP per capita in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nQuarterly GDP per capita growth in the UK 2019-2023\\\\nQuarterly GDP per capita growth in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nGDP per capita of the UK 1999-2021, by country\\\\nGross domestic product per capita of the United Kingdom from 1999 to 2021, by country (in GBP)\\\\nGDP per capita of the UK 2021, by region\\\\nGross domestic product per capita of the United Kingdom in 2021, by region (in GBP)\\\\nGlobal Comparisons\\\\nGlobal Comparisons\\\\nCountries with the largest gross domestic product (GDP) 2022\\\\n Monthly GDP of the UK 2019-2023\\\\nMonthly index of gross domestic product in the United Kingdom from January 2019 to November 2023 (2019=100)\\\\nGVA of the UK 2022, by sector\\\\nGross value added of the United Kingdom in 2022, by industry sector (in million GBP)\\\\nGDP of the UK 2021, by country\\\\nGross domestic product of the United Kingdom in 2021, by country (in million GBP)\\\\nGDP of the UK 2021, by region\\\\nGross domestic product of the United Kingdom in 2021, by region (in million GBP)\\\\nGDP of Scotland 2021, by local area\\\\nGross domestic product of Scotland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Wales 2021, by local area\\\\nGross domestic product of Wales in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Northern Ireland 2021, by local area\\\\nGross domestic product of Northern Ireland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP growth\\\\nGDP growth\\\\nGDP growth forecast for the UK 2000-2028\\\\nForecasted annual growth of gross domestic product in the United Kingdom from 2000 to 2028\\\\nAnnual GDP growth in the UK 1949-2022\\\\nAnnual growth of gross domestic product in the United Kingdom from 1949 to 2022\\\\nQuarterly GDP growth of the UK 2019-2023\\\\nQuarterly growth of gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023\\\\nMonthly GDP growth of the UK 2019-2023\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nUK GDP - Statistics & Facts\\\\nUK economy expected to shrink in 2023\\\\nCharacteristics of UK GDP\\\\nKey insights\\\\nDetailed statistics\\\\nGDP of the UK 1948-2022\\\\nDetailed statistics\\\\nAnnual GDP growth in the UK 1949-2022\\\\nDetailed statistics\\\\nGDP per capita in the UK 1955-2022\\\\nEditor\\\\u2019s Picks\\\\nCurrent statistics on this topic\\\\nCurrent statistics on this topic\\\\nKey Economic Indicators\\\\nMonthly GDP growth of the UK 2019-2023\\\\nKey Economic Indicators\\\\nMonthly GDP of the UK 2019-2023\\\\nKey Economic Indicators\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nRelated topics\\\\nRecommended\\\\nRecommended statistics\\\\nGDP\\\\nGDP\\\\nGDP of the UK 1948-2022\\\\nGross domestic product of the United Kingdom from 1948 to 2022 (in million GBP)\\\\nQuarterly GDP of the UK 2019-2023\\\\nQuarterly gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in million GBP)\\\\n The 20 countries with the largest gross domestic product (GDP) in 2022 (in billion U.S. dollars)\\\\nGDP of European countries in 2022\\\\nGross domestic product at current market prices of selected European countries in 2022 (in million euros)\\\\nReal GDP growth rates in Europe 2023\\\\nAnnual real gross domestic product (GDP) growth rate in European countries in 2023\\\\nGross domestic product (GDP) of Europe\\'s largest economies 1980-2028\\\\nGross domestic product (GDP) at current prices of Europe\\'s largest economies from 1980 to 2028 (in billion U.S dollars)\\\\nUnited Kingdom\\'s share of global gross domestic product (GDP) 2028\\\\nUnited Kingdom (UK): Share of global gross domestic product (GDP) adjusted for Purchasing Power Parity (PPP) from 2018 to 2028\\\\nRelated topics\\\\nRecommended\\\\nReport on the topic\\\\nKey figures\\\\nThe most important key figures provide you with a compact summary of the topic of \\\\\"UK GDP\\\\\" and take you straight to the corresponding statistics.\\\\n Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n\"}]', name='tavily_search_results_json', tool_call_id='call_3zDlnDMUkWEJxnHASo59doCL')]}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"The search results provide some information about the UK's GDP over the past years, but most of the relevant data is either not in a structured format that can be easily extracted or it is behind a source that requires further access for detailed statistics. To proceed with generating a line graph, we need specific GDP values for each year from 2018 to 2023.\\n\\nHowever, one of the search results from macrotrends.net does provide specific GDP values for the years 2018 to 2021:\\n\\n- U.K. GDP for 2021 was $3,141.51 billion, a 16.45% increase from 2020.\\n- U.K. GDP for 2020 was $2,697.81 billion, a 5.39% decline from 2019.\\n- U.K. GDP for 2019 was $2,851.41 billion, a 0.69% decline from 2018.\\n\\nWe still need the GDP values for 2022 and 2023 to complete the dataset for the past five years. I will now conduct a further search to find the missing GDP data for 2022 and 2023.\", additional_kwargs={'tool_calls': [{'id': 'call_nvB1wQyQuNeTrOXQZnEtgNDZ', 'function': {'arguments': '{\"query\":\"UK GDP 2022 2023\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 263, 'prompt_tokens': 3199, 'total_tokens': 3462}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-25901401-0d62-485f-b7d5-37e3c159effe-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2022 2023'}, 'id': 'call_nvB1wQyQuNeTrOXQZnEtgNDZ'}])], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"The search results provide some information about the UK's GDP over the past years, but most of the relevant data is either not in a structured format that can be easily extracted or it is behind a source that requires further access for detailed statistics. To proceed with generating a line graph, we need specific GDP values for each year from 2018 to 2023.\\n\\nHowever, one of the search results from macrotrends.net does provide specific GDP values for the years 2018 to 2021:\\n\\n- U.K. GDP for 2021 was $3,141.51 billion, a 16.45% increase from 2020.\\n- U.K. GDP for 2020 was $2,697.81 billion, a 5.39% decline from 2019.\\n- U.K. GDP for 2019 was $2,851.41 billion, a 0.69% decline from 2018.\\n\\nWe still need the GDP values for 2022 and 2023 to complete the dataset for the past five years. I will now conduct a further search to find the missing GDP data for 2022 and 2023.\", additional_kwargs={'tool_calls': [{'id': 'call_nvB1wQyQuNeTrOXQZnEtgNDZ', 'function': {'arguments': '{\"query\":\"UK GDP 2022 2023\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 263, 'prompt_tokens': 3199, 'total_tokens': 3462}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-25901401-0d62-485f-b7d5-37e3c159effe-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2022 2023'}, 'id': 'call_nvB1wQyQuNeTrOXQZnEtgNDZ'}])], 'sender': 'Researcher'}}\n", "----\n", "{'call_tool': {'messages': [ToolMessage(content='[{\"url\": \"https://www.statista.com/statistics/281744/gdp-of-the-united-kingdom/\", \"content\": \"Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nGDP of the UK 1948-2022\\\\nUK economy expected to shrink in 2023\\\\nHow big is the UK economy compared to others?\\\\nGross domestic product of the United Kingdom from 1948 to 2022\\\\n(in million GBP)\\\\nAdditional Information\\\\nShow sources information\\\\nShow publisher information\\\\nUse Ask Statista Research Service\\\\nDecember 2023\\\\nUnited Kingdom\\\\n1948 to 2022\\\\n*GDP is displayed in real terms (seasonally adjusted chained volume measure with 2019 as the reference year)\\\\n Statistics on\\\\n\\\\\"\\\\nEconomy of the UK\\\\n\\\\\"\\\\nOther statistics that may interest you Economy of the UK\\\\nGross domestic product\\\\nLabor Market\\\\nInflation\\\\nGovernment finances\\\\nBusiness Enterprise\\\\nFurther related statistics\\\\nFurther Content: You might find this interesting as well\\\\nStatistics\\\\nTopics Other statistics on the topicThe UK economy\\\\nEconomy\\\\nRPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nCPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nAverage annual earnings for full-time employees in the UK 1999-2023\\\\nEconomy\\\\nInflation rate in the UK 1989-2023\\\\nYou only have access to basic statistics.\\\\n Customized Research & Analysis projects:\\\\nGet quick analyses with our professional research service\\\\nThe best of the best: the portal for top lists & rankings:\\\\n\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp\", \"content\": \"Quarter on Quarter growth: CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: q-on-q4 growth rate CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product at market prices: Current price: Seasonally adjusted \\\\u00a3m\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: quarter on quarter growth rate: CP SA %\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: q-on-q4 growth quarter growth: CP SA %\\\\nCurrent Prices (CP)\\\\nDatasets related to Gross Domestic Product (GDP)\\\\n A roundup of the latest data and trends on the economy, business and jobs\\\\nTime series related to Gross Domestic Product (GDP)\\\\nGross Domestic Product: chained volume measures: Seasonally adjusted \\\\u00a3m\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: Hide\\\\nData and analysis from Census 2021\\\\nGross Domestic Product (GDP)\\\\nGross domestic product (GDP) estimates as the main measure of UK economic growth based on the value of goods and services produced during a given period. Contains current and constant price data on the value of goods and services to indicate the economic performance of the UK.\\\\nEstimates of short-term indicators of investment in non-financial assets; business investment and asset and sector breakdowns of total gross fixed capital formation.\\\\n Monthly gross domestic product by gross value added\\\\nThe gross value added (GVA) tables showing the monthly and annual growths and indices as published within the monthly gross domestic product (GDP) statistical bulletin.\\\\n\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp/bulletins/gdpfirstquarterlyestimateuk/octobertodecember2023\", \"content\": \"This review covered:\\\\nprocesses and quality assurance in making revisions to GDP\\\\npotential improvements to early estimates of GDP enabled through enhanced access to data\\\\ncommunication of revisions to GDP, the story behind the most recent set of revisions in particular, and uncertainty in early estimates of GDP\\\\nWe have already started work looking into the recommendations of this review and have set out our plans on how we will improve the way we communicate uncertainty.\\\\n Source: GDP first quarterly estimate from the Office for National Statistics\\\\nNotes\\\\nOffice for Statistics Regulation Revisions of estimates of UK GDP review\\\\nThe Office for Statistics Regulation (OSR) have completed a review of the practices around the preparation and release of information about revisions to estimates of GDP in our Impact of Blue Book 2023 article released on 1 September 2023, as announced on 6 September 2023 on the OSR website. Across 2023, the services sector sees revisions for the following reasons, with only Quarter 1 2023 seeing growth revised from our previous publication, including:\\\\nupdated input data for the deflator used for telecommunications\\\\nupdated seasonal adjustment which now uses a complete year of data for 2023\\\\nProduction\\\\nThe production sector is estimated to have decreased by 1.0% in the latest quarter after growth of 0.1% in Quarter 3 2023 (unrevised from our previous publication). Important quality information\\\\nThere are common pitfalls in interpreting data series, and these include:\\\\nexpectations of accuracy and reliability in early estimates are often too high\\\\nrevisions are an inevitable consequence of the trade-off between timeliness and accuracy\\\\nearly estimates are often based on incomplete data\\\\nVery few statistical revisions arise as a result of \\\\u201cerrors\\\\u201d in the popular sense of the word. Construction output in Great Britain: December 2023, new orders and Construction Output Price Indices, October to December 2023\\\\nBulletin | Released 15 February 2024\\\\nShort-term measures of output by the construction industry, contracts awarded for new construction work in Great Britain and a summary of the Construction Output Price Indices (OPIs) in the UK for Quarter 4 (October to December) 2023.\\\\n\"}, {\"url\": \"https://www.statista.com/topics/3795/gdp-of-the-uk/\", \"content\": \"Monthly growth of gross domestic product in the United Kingdom from January 2019 to November 2023\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nContribution to gross domestic product growth in the United Kingdom in January 2023, by sector\\\\nGDP growth rate in the UK 1999-2021, by country\\\\nAnnual growth rates of gross domestic product in the United Kingdom from 1999 to 2021, by country\\\\nGDP growth rate in the UK 2021, by region\\\\nAnnual growth rates of gross domestic product in the United Kingdom in 2021, by region\\\\nGDP growth of Scotland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Scotland in 2021, by local (ITL 3) area\\\\nGDP growth of Wales 2021, by local area\\\\nAnnual growth rates of gross domestic product in Wales in 2021, by local (ITL 3) area\\\\nGDP growth of Northern Ireland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Northern Ireland in 2021, by local (ITL 3) area\\\\nGDP per capita\\\\nGDP per capita\\\\nGDP per capita in the UK 1955-2022\\\\nGross domestic product per capita in the United Kingdom from 1955 to 2022 (in GBP)\\\\nAnnual GDP per capita growth in the UK 1956-2022\\\\nAnnual GDP per capita growth in the United Kingdom from 1956 to 2022\\\\nQuarterly GDP per capita in the UK 2019-2023\\\\nQuarterly GDP per capita in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nQuarterly GDP per capita growth in the UK 2019-2023\\\\nQuarterly GDP per capita growth in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nGDP per capita of the UK 1999-2021, by country\\\\nGross domestic product per capita of the United Kingdom from 1999 to 2021, by country (in GBP)\\\\nGDP per capita of the UK 2021, by region\\\\nGross domestic product per capita of the United Kingdom in 2021, by region (in GBP)\\\\nGlobal Comparisons\\\\nGlobal Comparisons\\\\nCountries with the largest gross domestic product (GDP) 2022\\\\n Monthly GDP of the UK 2019-2023\\\\nMonthly index of gross domestic product in the United Kingdom from January 2019 to November 2023 (2019=100)\\\\nGVA of the UK 2022, by sector\\\\nGross value added of the United Kingdom in 2022, by industry sector (in million GBP)\\\\nGDP of the UK 2021, by country\\\\nGross domestic product of the United Kingdom in 2021, by country (in million GBP)\\\\nGDP of the UK 2021, by region\\\\nGross domestic product of the United Kingdom in 2021, by region (in million GBP)\\\\nGDP of Scotland 2021, by local area\\\\nGross domestic product of Scotland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Wales 2021, by local area\\\\nGross domestic product of Wales in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Northern Ireland 2021, by local area\\\\nGross domestic product of Northern Ireland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP growth\\\\nGDP growth\\\\nGDP growth forecast for the UK 2000-2028\\\\nForecasted annual growth of gross domestic product in the United Kingdom from 2000 to 2028\\\\nAnnual GDP growth in the UK 1949-2022\\\\nAnnual growth of gross domestic product in the United Kingdom from 1949 to 2022\\\\nQuarterly GDP growth of the UK 2019-2023\\\\nQuarterly growth of gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023\\\\nMonthly GDP growth of the UK 2019-2023\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nUK GDP - Statistics & Facts\\\\nUK economy expected to shrink in 2023\\\\nCharacteristics of UK GDP\\\\nKey insights\\\\nDetailed statistics\\\\nGDP of the UK 1948-2022\\\\nDetailed statistics\\\\nAnnual GDP growth in the UK 1949-2022\\\\nDetailed statistics\\\\nGDP per capita in the UK 1955-2022\\\\nEditor\\\\u2019s Picks\\\\nCurrent statistics on this topic\\\\nCurrent statistics on this topic\\\\nKey Economic Indicators\\\\nMonthly GDP growth of the UK 2019-2023\\\\nKey Economic Indicators\\\\nMonthly GDP of the UK 2019-2023\\\\nKey Economic Indicators\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nRelated topics\\\\nRecommended\\\\nRecommended statistics\\\\nGDP\\\\nGDP\\\\nGDP of the UK 1948-2022\\\\nGross domestic product of the United Kingdom from 1948 to 2022 (in million GBP)\\\\nQuarterly GDP of the UK 2019-2023\\\\nQuarterly gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in million GBP)\\\\n The 20 countries with the largest gross domestic product (GDP) in 2022 (in billion U.S. dollars)\\\\nGDP of European countries in 2022\\\\nGross domestic product at current market prices of selected European countries in 2022 (in million euros)\\\\nReal GDP growth rates in Europe 2023\\\\nAnnual real gross domestic product (GDP) growth rate in European countries in 2023\\\\nGross domestic product (GDP) of Europe\\'s largest economies 1980-2028\\\\nGross domestic product (GDP) at current prices of Europe\\'s largest economies from 1980 to 2028 (in billion U.S dollars)\\\\nUnited Kingdom\\'s share of global gross domestic product (GDP) 2028\\\\nUnited Kingdom (UK): Share of global gross domestic product (GDP) adjusted for Purchasing Power Parity (PPP) from 2018 to 2028\\\\nRelated topics\\\\nRecommended\\\\nReport on the topic\\\\nKey figures\\\\nThe most important key figures provide you with a compact summary of the topic of \\\\\"UK GDP\\\\\" and take you straight to the corresponding statistics.\\\\n Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp/bulletins/quarterlynationalaccounts/latest\", \"content\": \"Looking at the quarters open to revision, real GDP growth is unrevised in five of the seven quarters compared with the first quarterly estimate; however, it is important to note that the typical absolute average revision between the initial quarterly GDP estimate and the estimate three years later is 0.2 percentage points, as there is potential for revision to GDP when the annual supply and use balance occurs as more comprehensive annual data sources are available at a detailed industry and product level; all the GDP growth vintages for these quarters are shown in Table 4.\\\\n Overall the revisions to production reflect:\\\\nrevised volume data from the\\\\u00a0Department for Energy Security and Net Zero (DESNZ) for electricity, gas, steam and air conditioning supply\\\\nnew Value Added Tax (VAT) turnover data for Quarter 2 2023\\\\nnew and revised Monthly Business Survey data\\\\nseasonal adjustment models\\\\nFigure 7: Revisions to production output across 2022 and 2023 are mainly driven by manufacturing; and the electricity, gas and steam subsectors\\\\nConstruction\\\\nConstruction output rose by 0.4% in Quarter 3 2023, revised up from a first estimate increase of 0.1%. Professional, scientific and technical activities: the upward revision in Quarter 4 (Oct to Dec) 2022 and Quarter 1 2023 are driven by new and revised survey data within the advertising and market research industry; in Quarter 3 2023, six of the eight industries in this section are revised down, with the largest contribution coming from architecture and engineering activities; technical testing and analysis, because of revised survey data since our last publication and the new VAT data for Quarter 2 2023.\\\\n This review covered:\\\\nprocesses and quality assurance in making revisions to GDP\\\\npotential improvements to early estimates of GDP enabled through enhanced access to data\\\\ncommunication of revisions to GDP, the story behind the most recent set of revisions in particular, and uncertainty in early estimates of GDP\\\\nWe have already started work looking into the recommendations of this review and will set out plans more fully during January 2024.\\\\n Important quality information\\\\nThere are common pitfalls in interpreting data series, and these include:\\\\nexpectations of accuracy and reliability in early estimates are often too high\\\\nrevisions are an inevitable consequence of the trade-off between timeliness and accuracy\\\\nearly estimates are based on incomplete data\\\\nVery few statistical revisions arise as a result of \\\\\"errors\\\\\" in the popular sense of the word.\"}]', name='tavily_search_results_json', tool_call_id='call_nvB1wQyQuNeTrOXQZnEtgNDZ')]}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"The search results did not provide exact figures for the UK's GDP in 2022 and 2023. While there are several references to GDP data, growth rates, and quarterly figures, we do not have the specific annual GDP values in a consistent currency format (such as USD or GBP) that would allow us to compile a complete dataset for the past five years.\\n\\nTo proceed, we will need to find another source or use a different method to obtain the missing GDP data for 2022 and 2023. If this data is not available, we may not be able to draw an accurate line graph of the UK's GDP over the past five years.\", response_metadata={'token_usage': {'completion_tokens': 134, 'prompt_tokens': 6996, 'total_tokens': 7130}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-aa7d307d-cfdd-4c83-ad09-b6b0efbffe6e-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"The search results did not provide exact figures for the UK's GDP in 2022 and 2023. While there are several references to GDP data, growth rates, and quarterly figures, we do not have the specific annual GDP values in a consistent currency format (such as USD or GBP) that would allow us to compile a complete dataset for the past five years.\\n\\nTo proceed, we will need to find another source or use a different method to obtain the missing GDP data for 2022 and 2023. If this data is not available, we may not be able to draw an accurate line graph of the UK's GDP over the past five years.\", response_metadata={'token_usage': {'completion_tokens': 134, 'prompt_tokens': 6996, 'total_tokens': 7130}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-aa7d307d-cfdd-4c83-ad09-b6b0efbffe6e-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"It seems we have hit a roadblock in finding the exact GDP figures for the UK for the years 2022 and 2023. The information provided by the search results does not include the specific data we need. Therefore, we currently do not have the complete dataset to generate a line graph of the UK's GDP over the past five years.\\n\\nTo proceed, we might need to look for an official statistical release or a comprehensive economic report that includes the GDP figures for 2022 and 2023. If such data can be obtained, we can then use it to create the desired line graph. Without this data, we cannot fulfill the request as specified.\", response_metadata={'token_usage': {'completion_tokens': 134, 'prompt_tokens': 7150, 'total_tokens': 7284}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-a667e647-45b2-414e-b301-81f846fa59ad-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"It seems we have hit a roadblock in finding the exact GDP figures for the UK for the years 2022 and 2023. The information provided by the search results does not include the specific data we need. Therefore, we currently do not have the complete dataset to generate a line graph of the UK's GDP over the past five years.\\n\\nTo proceed, we might need to look for an official statistical release or a comprehensive economic report that includes the GDP figures for 2022 and 2023. If such data can be obtained, we can then use it to create the desired line graph. Without this data, we cannot fulfill the request as specified.\", response_metadata={'token_usage': {'completion_tokens': 134, 'prompt_tokens': 7150, 'total_tokens': 7284}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-a667e647-45b2-414e-b301-81f846fa59ad-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"I am unable to proceed with generating the line graph as we lack the specific GDP data for the UK for the years 2022 and 2023. To complete this task, we would need to acquire the missing data points. If the data becomes available, or if there's an alternative source that can provide the figures, we can revisit this task and generate the line graph accordingly. For now, we must conclude this attempt.\", response_metadata={'token_usage': {'completion_tokens': 87, 'prompt_tokens': 7276, 'total_tokens': 7363}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-1731ab9f-7ee9-4ff9-a920-7b998a41fe4d-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"I am unable to proceed with generating the line graph as we lack the specific GDP data for the UK for the years 2022 and 2023. To complete this task, we would need to acquire the missing data points. If the data becomes available, or if there's an alternative source that can provide the figures, we can revisit this task and generate the line graph accordingly. For now, we must conclude this attempt.\", response_metadata={'token_usage': {'completion_tokens': 87, 'prompt_tokens': 7276, 'total_tokens': 7363}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-1731ab9f-7ee9-4ff9-a920-7b998a41fe4d-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"As of my last attempt, I have not been able to find the exact GDP figures for the UK for the years 2022 and 2023. Without these figures, it's not possible to generate a complete and accurate line graph of the UK's GDP over the past five years. Should the data become available or if an alternative source can provide the figures, we can proceed with creating the graph. For the time being, this task cannot be completed.\", response_metadata={'token_usage': {'completion_tokens': 93, 'prompt_tokens': 7383, 'total_tokens': 7476}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-6e12dccd-b1b2-4c7d-befd-4a338d027756-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"As of my last attempt, I have not been able to find the exact GDP figures for the UK for the years 2022 and 2023. Without these figures, it's not possible to generate a complete and accurate line graph of the UK's GDP over the past five years. Should the data become available or if an alternative source can provide the figures, we can proceed with creating the graph. For the time being, this task cannot be completed.\", response_metadata={'token_usage': {'completion_tokens': 93, 'prompt_tokens': 7383, 'total_tokens': 7476}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-6e12dccd-b1b2-4c7d-befd-4a338d027756-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"I have attempted to gather the UK's GDP data for the past five years to draw a line graph, but I was only able to obtain confirmed figures for the years 2018 through 2021. The GDP data for 2022 and 2023 was not available in the search results. Without complete data, it is not possible to generate the line graph as requested. If the missing data for 2022 and 2023 becomes available or an alternative verified source can provide these figures, we can revisit this task to create the graph.\", response_metadata={'token_usage': {'completion_tokens': 111, 'prompt_tokens': 7468, 'total_tokens': 7579}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-9d93e637-cedd-4b17-945a-cc97e490432c-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"I have attempted to gather the UK's GDP data for the past five years to draw a line graph, but I was only able to obtain confirmed figures for the years 2018 through 2021. The GDP data for 2022 and 2023 was not available in the search results. Without complete data, it is not possible to generate the line graph as requested. If the missing data for 2022 and 2023 becomes available or an alternative verified source can provide these figures, we can revisit this task to create the graph.\", response_metadata={'token_usage': {'completion_tokens': 111, 'prompt_tokens': 7468, 'total_tokens': 7579}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-9d93e637-cedd-4b17-945a-cc97e490432c-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"I've exhausted the available tools to find the UK's GDP for the past five years, but I was able to obtain data up to the year 2021. Unfortunately, without the GDP data for 2022 and 2023, I cannot create a complete line graph for the UK's GDP over the past five years.\\n\\nTo generate the line graph, we would need to have all the necessary data points for each year from 2018 to 2023. If you can provide the GDP data for 2022 and 2023 or direct me to a specific source where these figures can be found, I will be able to create the line graph for you.\", response_metadata={'token_usage': {'completion_tokens': 136, 'prompt_tokens': 7599, 'total_tokens': 7735}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-82063d76-6655-473e-9f6a-a5c005d92bd3-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"I've exhausted the available tools to find the UK's GDP for the past five years, but I was able to obtain data up to the year 2021. Unfortunately, without the GDP data for 2022 and 2023, I cannot create a complete line graph for the UK's GDP over the past five years.\\n\\nTo generate the line graph, we would need to have all the necessary data points for each year from 2018 to 2023. If you can provide the GDP data for 2022 and 2023 or direct me to a specific source where these figures can be found, I will be able to create the line graph for you.\", response_metadata={'token_usage': {'completion_tokens': 136, 'prompt_tokens': 7599, 'total_tokens': 7735}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-82063d76-6655-473e-9f6a-a5c005d92bd3-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"I have exhausted the available tools to find the UK's GDP for the past five years, but I was able to obtain data up to the year 2021. Unfortunately, without the GDP data for 2022 and 2023, I cannot create a complete line graph for the UK's GDP over the past five years.\\n\\nTo generate the line graph, we would need to have all the necessary data points for each year from 2018 to 2023. If you can provide the GDP data for 2022 and 2023 or direct me to a specific source where these figures can be found, I will be able to create the line graph for you.\", response_metadata={'token_usage': {'completion_tokens': 136, 'prompt_tokens': 7727, 'total_tokens': 7863}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-954e7bb2-ae3e-4610-9927-4b16d733414e-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"I have exhausted the available tools to find the UK's GDP for the past five years, but I was able to obtain data up to the year 2021. Unfortunately, without the GDP data for 2022 and 2023, I cannot create a complete line graph for the UK's GDP over the past five years.\\n\\nTo generate the line graph, we would need to have all the necessary data points for each year from 2018 to 2023. If you can provide the GDP data for 2022 and 2023 or direct me to a specific source where these figures can be found, I will be able to create the line graph for you.\", response_metadata={'token_usage': {'completion_tokens': 136, 'prompt_tokens': 7727, 'total_tokens': 7863}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-954e7bb2-ae3e-4610-9927-4b16d733414e-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"Having attempted to find the UK's GDP figures for the years 2022 and 2023 through available data sources and coming up short, it appears we are currently unable to produce a complete line graph of the UK's GDP over the past five years as initially requested. To create a graph, we would need precise GDP data for each year from 2018 to 2023.\\n\\nIf the missing GDP data for 2022 and 2023 becomes available or if an alternative source can provide these figures, we can then proceed to generate the line graph. As of now, this task must be paused until the necessary data can be obtained.\", response_metadata={'token_usage': {'completion_tokens': 130, 'prompt_tokens': 7883, 'total_tokens': 8013}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-8d1382e2-a77c-4d2f-b06a-2597be59542b-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"Having attempted to find the UK's GDP figures for the years 2022 and 2023 through available data sources and coming up short, it appears we are currently unable to produce a complete line graph of the UK's GDP over the past five years as initially requested. To create a graph, we would need precise GDP data for each year from 2018 to 2023.\\n\\nIf the missing GDP data for 2022 and 2023 becomes available or if an alternative source can provide these figures, we can then proceed to generate the line graph. As of now, this task must be paused until the necessary data can be obtained.\", response_metadata={'token_usage': {'completion_tokens': 130, 'prompt_tokens': 7883, 'total_tokens': 8013}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-8d1382e2-a77c-4d2f-b06a-2597be59542b-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"The search results do not provide the exact GDP figures for the UK for 2022 and 2023. Without this information, it is not possible to generate a line graph of the UK's GDP over the past five years. We would require the GDP values for those two years to complete the dataset and create the graph. As of now, I must conclude this task until the necessary data becomes available.\", response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 8005, 'total_tokens': 8087}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-246b9b29-ffc7-4da9-a09a-0dcfbbb3bd7a-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"The search results do not provide the exact GDP figures for the UK for 2022 and 2023. Without this information, it is not possible to generate a line graph of the UK's GDP over the past five years. We would require the GDP values for those two years to complete the dataset and create the graph. As of now, I must conclude this task until the necessary data becomes available.\", response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 8005, 'total_tokens': 8087}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-246b9b29-ffc7-4da9-a09a-0dcfbbb3bd7a-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"I have attempted to find the UK's GDP for the past five years to create a line graph, but I could only obtain confirmed figures for the years 2018 through 2021. The GDP data for 2022 and 2023 was not available in the search results. Without complete data, it is not possible to generate the line graph as requested. If the missing data for 2022 and 2023 becomes available or an alternative verified source can provide these figures, we can revisit this task to create the graph.\", response_metadata={'token_usage': {'completion_tokens': 108, 'prompt_tokens': 8107, 'total_tokens': 8215}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-f2847a80-610d-49c5-924a-ccffccb7cd5a-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"I have attempted to find the UK's GDP for the past five years to create a line graph, but I could only obtain confirmed figures for the years 2018 through 2021. The GDP data for 2022 and 2023 was not available in the search results. Without complete data, it is not possible to generate the line graph as requested. If the missing data for 2022 and 2023 becomes available or an alternative verified source can provide these figures, we can revisit this task to create the graph.\", response_metadata={'token_usage': {'completion_tokens': 108, 'prompt_tokens': 8107, 'total_tokens': 8215}, 'model_name': 'gpt-4o', 'system_fingerprint': 'fp_85bf4c41a2', 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-f2847a80-610d-49c5-924a-ccffccb7cd5a-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"As of now, I was unable to obtain the complete data for the UK's GDP over the past five years due to lack of specific information for the years 2022 and 2023. Therefore, it's not possible to draw a line graph of the UK's GDP for this period without the complete dataset. Further action to acquire the missing data would be required to proceed.\", response_metadata={'token_usage': {'completion_tokens': 77, 'prompt_tokens': 8207, 'total_tokens': 8284}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-28e09000-8787-4ac0-a7d8-0aba888c2520-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"As of now, I was unable to obtain the complete data for the UK's GDP over the past five years due to lack of specific information for the years 2022 and 2023. Therefore, it's not possible to draw a line graph of the UK's GDP for this period without the complete dataset. Further action to acquire the missing data would be required to proceed.\", response_metadata={'token_usage': {'completion_tokens': 77, 'prompt_tokens': 8207, 'total_tokens': 8284}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-28e09000-8787-4ac0-a7d8-0aba888c2520-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"It appears we have encountered a limitation in obtaining the complete GDP data for the UK for 2022 and 2023. Without these figures, we cannot create the line graph of the UK's GDP over the past five years as requested. If the data becomes available, or if there's an alternative source that can provide the figures, we can revisit this task and generate the line graph accordingly. For now, this task will have to be concluded without completion.\", response_metadata={'token_usage': {'completion_tokens': 93, 'prompt_tokens': 8304, 'total_tokens': 8397}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-8bf8f247-cb86-4ef0-a81b-14da2d27b6f1-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"It appears we have encountered a limitation in obtaining the complete GDP data for the UK for 2022 and 2023. Without these figures, we cannot create the line graph of the UK's GDP over the past five years as requested. If the data becomes available, or if there's an alternative source that can provide the figures, we can revisit this task and generate the line graph accordingly. For now, this task will have to be concluded without completion.\", response_metadata={'token_usage': {'completion_tokens': 93, 'prompt_tokens': 8304, 'total_tokens': 8397}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-8bf8f247-cb86-4ef0-a81b-14da2d27b6f1-0')], 'sender': 'chart_generator'}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_df3UdS3vJkJFB30O0WYq38k8', 'function': {'arguments': '{\"query\":\"UK GDP 2022 2023 statistics\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 8389, 'total_tokens': 8415}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-e1577cc7-5673-4821-9683-34947c7a2bc5-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2022 2023 statistics'}, 'id': 'call_df3UdS3vJkJFB30O0WYq38k8'}])], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_df3UdS3vJkJFB30O0WYq38k8', 'function': {'arguments': '{\"query\":\"UK GDP 2022 2023 statistics\"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 8389, 'total_tokens': 8415}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='Researcher', id='run-e1577cc7-5673-4821-9683-34947c7a2bc5-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'UK GDP 2022 2023 statistics'}, 'id': 'call_df3UdS3vJkJFB30O0WYq38k8'}])], 'sender': 'Researcher'}}\n", "----\n", "{'call_tool': {'messages': [ToolMessage(content='[{\"url\": \"https://www.statista.com/statistics/281744/gdp-of-the-united-kingdom/\", \"content\": \"Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nGDP of the UK 1948-2022\\\\nUK economy expected to shrink in 2023\\\\nHow big is the UK economy compared to others?\\\\nGross domestic product of the United Kingdom from 1948 to 2022\\\\n(in million GBP)\\\\nAdditional Information\\\\nShow sources information\\\\nShow publisher information\\\\nUse Ask Statista Research Service\\\\nDecember 2023\\\\nUnited Kingdom\\\\n1948 to 2022\\\\n*GDP is displayed in real terms (seasonally adjusted chained volume measure with 2019 as the reference year)\\\\n Statistics on\\\\n\\\\\"\\\\nEconomy of the UK\\\\n\\\\\"\\\\nOther statistics that may interest you Economy of the UK\\\\nGross domestic product\\\\nLabor Market\\\\nInflation\\\\nGovernment finances\\\\nBusiness Enterprise\\\\nFurther related statistics\\\\nFurther Content: You might find this interesting as well\\\\nStatistics\\\\nTopics Other statistics on the topicThe UK economy\\\\nEconomy\\\\nRPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nCPI annual inflation rate UK 2000-2028\\\\nEconomy\\\\nAverage annual earnings for full-time employees in the UK 1999-2023\\\\nEconomy\\\\nInflation rate in the UK 1989-2023\\\\nYou only have access to basic statistics.\\\\n Customized Research & Analysis projects:\\\\nGet quick analyses with our professional research service\\\\nThe best of the best: the portal for top lists & rankings:\\\\n\"}, {\"url\": \"https://www.statista.com/topics/3795/gdp-of-the-uk/\", \"content\": \"Monthly growth of gross domestic product in the United Kingdom from January 2019 to November 2023\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nContribution to gross domestic product growth in the United Kingdom in January 2023, by sector\\\\nGDP growth rate in the UK 1999-2021, by country\\\\nAnnual growth rates of gross domestic product in the United Kingdom from 1999 to 2021, by country\\\\nGDP growth rate in the UK 2021, by region\\\\nAnnual growth rates of gross domestic product in the United Kingdom in 2021, by region\\\\nGDP growth of Scotland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Scotland in 2021, by local (ITL 3) area\\\\nGDP growth of Wales 2021, by local area\\\\nAnnual growth rates of gross domestic product in Wales in 2021, by local (ITL 3) area\\\\nGDP growth of Northern Ireland 2021, by local area\\\\nAnnual growth rates of gross domestic product in Northern Ireland in 2021, by local (ITL 3) area\\\\nGDP per capita\\\\nGDP per capita\\\\nGDP per capita in the UK 1955-2022\\\\nGross domestic product per capita in the United Kingdom from 1955 to 2022 (in GBP)\\\\nAnnual GDP per capita growth in the UK 1956-2022\\\\nAnnual GDP per capita growth in the United Kingdom from 1956 to 2022\\\\nQuarterly GDP per capita in the UK 2019-2023\\\\nQuarterly GDP per capita in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nQuarterly GDP per capita growth in the UK 2019-2023\\\\nQuarterly GDP per capita growth in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in GBP)\\\\nGDP per capita of the UK 1999-2021, by country\\\\nGross domestic product per capita of the United Kingdom from 1999 to 2021, by country (in GBP)\\\\nGDP per capita of the UK 2021, by region\\\\nGross domestic product per capita of the United Kingdom in 2021, by region (in GBP)\\\\nGlobal Comparisons\\\\nGlobal Comparisons\\\\nCountries with the largest gross domestic product (GDP) 2022\\\\n Monthly GDP of the UK 2019-2023\\\\nMonthly index of gross domestic product in the United Kingdom from January 2019 to November 2023 (2019=100)\\\\nGVA of the UK 2022, by sector\\\\nGross value added of the United Kingdom in 2022, by industry sector (in million GBP)\\\\nGDP of the UK 2021, by country\\\\nGross domestic product of the United Kingdom in 2021, by country (in million GBP)\\\\nGDP of the UK 2021, by region\\\\nGross domestic product of the United Kingdom in 2021, by region (in million GBP)\\\\nGDP of Scotland 2021, by local area\\\\nGross domestic product of Scotland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Wales 2021, by local area\\\\nGross domestic product of Wales in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP of Northern Ireland 2021, by local area\\\\nGross domestic product of Northern Ireland in 2021, by local (ITL 3) area (in million GBP)\\\\nGDP growth\\\\nGDP growth\\\\nGDP growth forecast for the UK 2000-2028\\\\nForecasted annual growth of gross domestic product in the United Kingdom from 2000 to 2028\\\\nAnnual GDP growth in the UK 1949-2022\\\\nAnnual growth of gross domestic product in the United Kingdom from 1949 to 2022\\\\nQuarterly GDP growth of the UK 2019-2023\\\\nQuarterly growth of gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023\\\\nMonthly GDP growth of the UK 2019-2023\\\\n Transforming data into design:\\\\nStatista Content & Design\\\\nStrategy and business building for the data-driven economy:\\\\nUK GDP - Statistics & Facts\\\\nUK economy expected to shrink in 2023\\\\nCharacteristics of UK GDP\\\\nKey insights\\\\nDetailed statistics\\\\nGDP of the UK 1948-2022\\\\nDetailed statistics\\\\nAnnual GDP growth in the UK 1949-2022\\\\nDetailed statistics\\\\nGDP per capita in the UK 1955-2022\\\\nEditor\\\\u2019s Picks\\\\nCurrent statistics on this topic\\\\nCurrent statistics on this topic\\\\nKey Economic Indicators\\\\nMonthly GDP growth of the UK 2019-2023\\\\nKey Economic Indicators\\\\nMonthly GDP of the UK 2019-2023\\\\nKey Economic Indicators\\\\nContribution to GDP growth in the UK 2023, by sector\\\\nRelated topics\\\\nRecommended\\\\nRecommended statistics\\\\nGDP\\\\nGDP\\\\nGDP of the UK 1948-2022\\\\nGross domestic product of the United Kingdom from 1948 to 2022 (in million GBP)\\\\nQuarterly GDP of the UK 2019-2023\\\\nQuarterly gross domestic product in the United Kingdom from 1st quarter 2019 to 3rd quarter 2023 (in million GBP)\\\\n The 20 countries with the largest gross domestic product (GDP) in 2022 (in billion U.S. dollars)\\\\nGDP of European countries in 2022\\\\nGross domestic product at current market prices of selected European countries in 2022 (in million euros)\\\\nReal GDP growth rates in Europe 2023\\\\nAnnual real gross domestic product (GDP) growth rate in European countries in 2023\\\\nGross domestic product (GDP) of Europe\\'s largest economies 1980-2028\\\\nGross domestic product (GDP) at current prices of Europe\\'s largest economies from 1980 to 2028 (in billion U.S dollars)\\\\nUnited Kingdom\\'s share of global gross domestic product (GDP) 2028\\\\nUnited Kingdom (UK): Share of global gross domestic product (GDP) adjusted for Purchasing Power Parity (PPP) from 2018 to 2028\\\\nRelated topics\\\\nRecommended\\\\nReport on the topic\\\\nKey figures\\\\nThe most important key figures provide you with a compact summary of the topic of \\\\\"UK GDP\\\\\" and take you straight to the corresponding statistics.\\\\n Industry Overview\\\\nDigital & Trend reports\\\\nOverview and forecasts on trending topics\\\\nIndustry & Market reports\\\\nIndustry and market insights and forecasts\\\\nCompanies & Products reports\\\\nKey figures and rankings about companies and products\\\\nConsumer & Brand reports\\\\nConsumer and brand insights and preferences in various industries\\\\nPolitics & Society reports\\\\nDetailed information about political and social topics\\\\nCountry & Region reports\\\\nAll key figures about countries and regions\\\\nMarket forecast and expert KPIs for 1000+ markets in 190+ countries & territories\\\\nInsights on consumer attitudes and behavior worldwide\\\\nBusiness information on 100m+ public and private companies\\\\nExplore Company Insights\\\\nDetailed information for 39,000+ online stores and marketplaces\\\\nDirectly accessible data for 170 industries from 150+ countries\\\\nand over 1\\\\u00a0Mio. facts.\\\\n\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp/bulletins/quarterlynationalaccounts/latest\", \"content\": \"Looking at the quarters open to revision, real GDP growth is unrevised in five of the seven quarters compared with the first quarterly estimate; however, it is important to note that the typical absolute average revision between the initial quarterly GDP estimate and the estimate three years later is 0.2 percentage points, as there is potential for revision to GDP when the annual supply and use balance occurs as more comprehensive annual data sources are available at a detailed industry and product level; all the GDP growth vintages for these quarters are shown in Table 4.\\\\n Overall the revisions to production reflect:\\\\nrevised volume data from the\\\\u00a0Department for Energy Security and Net Zero (DESNZ) for electricity, gas, steam and air conditioning supply\\\\nnew Value Added Tax (VAT) turnover data for Quarter 2 2023\\\\nnew and revised Monthly Business Survey data\\\\nseasonal adjustment models\\\\nFigure 7: Revisions to production output across 2022 and 2023 are mainly driven by manufacturing; and the electricity, gas and steam subsectors\\\\nConstruction\\\\nConstruction output rose by 0.4% in Quarter 3 2023, revised up from a first estimate increase of 0.1%. Professional, scientific and technical activities: the upward revision in Quarter 4 (Oct to Dec) 2022 and Quarter 1 2023 are driven by new and revised survey data within the advertising and market research industry; in Quarter 3 2023, six of the eight industries in this section are revised down, with the largest contribution coming from architecture and engineering activities; technical testing and analysis, because of revised survey data since our last publication and the new VAT data for Quarter 2 2023.\\\\n This review covered:\\\\nprocesses and quality assurance in making revisions to GDP\\\\npotential improvements to early estimates of GDP enabled through enhanced access to data\\\\ncommunication of revisions to GDP, the story behind the most recent set of revisions in particular, and uncertainty in early estimates of GDP\\\\nWe have already started work looking into the recommendations of this review and will set out plans more fully during January 2024.\\\\n Important quality information\\\\nThere are common pitfalls in interpreting data series, and these include:\\\\nexpectations of accuracy and reliability in early estimates are often too high\\\\nrevisions are an inevitable consequence of the trade-off between timeliness and accuracy\\\\nearly estimates are based on incomplete data\\\\nVery few statistical revisions arise as a result of \\\\\"errors\\\\\" in the popular sense of the word.\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp/bulletins/gdpmonthlyestimateuk/latest\", \"content\": \"The following list contains the full SIC names of industries included in consumer-facing services and their corresponding shortened industry name where this has been used in Figure 5:\\\\nwholesale and retail trade and repair of motor vehicles and motorcycles - sales and repairs of motor vehicles\\\\nretail trade, except of motor vehicles and motorcycles - retail except motor vehicles\\\\nrail transport\\\\naccommodation\\\\nfood and beverage service activities - food and beverage\\\\nbuying and selling, renting and operating of own or leased real estate, excluding imputed rent - real estate activities\\\\nveterinary activities\\\\ntravel agency, tour operator and other reservation service and related activities - travel and tourism activities\\\\ngambling and betting services\\\\nsports activities and amusement and recreation activities - sports, amusement and recreation\\\\nactivities of membership organisations\\\\nother personal service activities\\\\nactivities of households as employers of domestic personnel - households as employers of domestic personnel\\\\nAdditional bank holiday in May 2023 for the Coronation of King Charles III\\\\nThere was an additional bank holiday for the coronation of King Charles III on Monday 8 May 2023. Source: Monthly GDP estimate from Office for National Statistics\\\\nThe main reasons for revisions in October 2023 are:\\\\nin the services sector, the upwards revision is mainly from updated and late monthly business survey responses primarily in the information and communication subsection\\\\nin the production sector, the downward revision is from source data replacing forecasts in mining and quarrying and electricity, gas, steam and air conditioning supply, as well as revised and late monthly business survey responses predominantly in the manufacture of pharmaceutical products and pharmaceutical preparations, and sewerage industries\\\\nin the construction sector, the upwards revisions is because of updated and late monthly business survey responses for new public housing and other public new work\\\\nDetails on the revisions to monthly GDP prior to October 2023 are provided in our GDP quarterly national accounts, UK: July to September 2023 bulletin.\\\\n This review covered:\\\\nprocesses and quality assurance in making revisions to GDP\\\\npotential improvements to early estimates of GDP enabled through enhanced access to data\\\\ncommunication of revisions to GDP, the story behind the most recent set of revisions in particular, and uncertainty in early estimates of GDP\\\\nWe have already started work looking into the recommendations of this review and will set out plans more fully during January 2024.\\\\n11. The main data source for these statistics is the Monthly Business Survey (MBS) and response rates for each can be found in our:\\\\nOutput in the construction industry dataset\\\\nMonthly Business Survey (production) response rates dataset\\\\nCurrent and historical Monthly Business Survey (services) response rates dataset\\\\nOur monthly gross domestic product (GDP) data sources catalogue provides a full breakdown of the data used in this publication.\\\\n On the negative side, the lack of demand for construction products was prevalent across manufacturing, with manufacture of wood, rubber and plastic, glass, cement and plaster all seeing declines on the month in November 2023 in line with the two consecutive monthly falls in construction output in October and November 2023.\\\\n\"}, {\"url\": \"https://www.ons.gov.uk/economy/grossdomesticproductgdp\", \"content\": \"Quarter on Quarter growth: CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: q-on-q4 growth rate CVM SA %\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product at market prices: Current price: Seasonally adjusted \\\\u00a3m\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: quarter on quarter growth rate: CP SA %\\\\nCurrent Prices (CP)\\\\nGross Domestic Product: q-on-q4 growth quarter growth: CP SA %\\\\nCurrent Prices (CP)\\\\nDatasets related to Gross Domestic Product (GDP)\\\\n A roundup of the latest data and trends on the economy, business and jobs\\\\nTime series related to Gross Domestic Product (GDP)\\\\nGross Domestic Product: chained volume measures: Seasonally adjusted \\\\u00a3m\\\\nChained Volume Measures (CVM)\\\\nGross Domestic Product: Hide\\\\nData and analysis from Census 2021\\\\nGross Domestic Product (GDP)\\\\nGross domestic product (GDP) estimates as the main measure of UK economic growth based on the value of goods and services produced during a given period. Contains current and constant price data on the value of goods and services to indicate the economic performance of the UK.\\\\nEstimates of short-term indicators of investment in non-financial assets; business investment and asset and sector breakdowns of total gross fixed capital formation.\\\\n Monthly gross domestic product by gross value added\\\\nThe gross value added (GVA) tables showing the monthly and annual growths and indices as published within the monthly gross domestic product (GDP) statistical bulletin.\\\\n\"}]', name='tavily_search_results_json', tool_call_id='call_df3UdS3vJkJFB30O0WYq38k8')]}}\n", "----\n", - "{'Researcher': {'messages': [AIMessage(content=\"The search results unfortunately do not provide specific figures for the UK's GDP in 2022 and 2023. While there are references to GDP data and related economic indicators, the exact annual GDP values needed to create the line graph are not present.\\n\\nOne possible approach to obtain these figures would be to access detailed statistical databases or reports from official sources such as the Office for National Statistics (ONS) or economic research institutions that publish historical GDP data. These sources might have the most recent and accurate GDP figures available for the UK, which are necessary to complete the line graph.\\n\\nSince I cannot directly access or retrieve the data from these sources using the tools available to me, I recommend consulting such databases or reports to find the UK's GDP for 2022 and 2023. Once the data is obtained, it can be used to create the line graph.\", response_metadata={'token_usage': {'completion_tokens': 172, 'prompt_tokens': 12099, 'total_tokens': 12271}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-7b4bcbc3-3ed0-4fa0-8e5d-a366c5a80d5a-0')], 'sender': 'Researcher'}}\n", + "{'Researcher': {'messages': [AIMessage(content=\"The search results unfortunately do not provide specific figures for the UK's GDP in 2022 and 2023. While there are references to GDP data and related economic indicators, the exact annual GDP values needed to create the line graph are not present.\\n\\nOne possible approach to obtain these figures would be to access detailed statistical databases or reports from official sources such as the Office for National Statistics (ONS) or economic research institutions that publish historical GDP data. These sources might have the most recent and accurate GDP figures available for the UK, which are necessary to complete the line graph.\\n\\nSince I cannot directly access or retrieve the data from these sources using the tools available to me, I recommend consulting such databases or reports to find the UK's GDP for 2022 and 2023. Once the data is obtained, it can be used to create the line graph.\", response_metadata={'token_usage': {'completion_tokens': 172, 'prompt_tokens': 12099, 'total_tokens': 12271}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='Researcher', id='run-7b4bcbc3-3ed0-4fa0-8e5d-a366c5a80d5a-0')], 'sender': 'Researcher'}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_JPVxDAzEFi21crVT7Rt6SRJl', 'function': {'arguments': '{\"code\":\"import matplotlib.pyplot as plt\\\\n\\\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\\\n# The data for 2022 and 2023 are placeholders and should be replaced with actual figures when available\\\\ngdp_data = {\\\\n \\'Year\\': [2018, 2019, 2020, 2021, 2022, 2023],\\\\n \\'GDP (Billion USD)\\': [2851.41, 2697.81, 3141.51, None, None]\\\\n}\\\\n\\\\n# Plot a line graph\\\\nplt.figure(figsize=(10, 5))\\\\nplt.plot(gdp_data[\\'Year\\'], gdp_data[\\'GDP (Billion USD)\\'], marker=\\'o\\')\\\\n\\\\n# Title and labels\\\\nplt.title(\\'UK GDP from 2018 to 2023\\')\\\\nplt.xlabel(\\'Year\\')\\\\nplt.ylabel(\\'GDP (Billion USD)\\')\\\\n\\\\n# Show grid\\\\nplt.grid(True)\\\\n\\\\n# Display the graph\\\\nplt.show()\"}', 'name': 'python_repl'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 240, 'prompt_tokens': 12291, 'total_tokens': 12531}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='chart_generator', id='run-6cff57bc-ba87-4690-9528-4d15bba7986c-0', tool_calls=[{'name': 'python_repl', 'args': {'code': \"import matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# The data for 2022 and 2023 are placeholders and should be replaced with actual figures when available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021, 2022, 2023],\\n 'GDP (Billion USD)': [2851.41, 2697.81, 3141.51, None, None]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2023')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\"}, 'id': 'call_JPVxDAzEFi21crVT7Rt6SRJl'}])], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_JPVxDAzEFi21crVT7Rt6SRJl', 'function': {'arguments': '{\"code\":\"import matplotlib.pyplot as plt\\\\n\\\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\\\n# The data for 2022 and 2023 are placeholders and should be replaced with actual figures when available\\\\ngdp_data = {\\\\n \\'Year\\': [2018, 2019, 2020, 2021, 2022, 2023],\\\\n \\'GDP (Billion USD)\\': [2851.41, 2697.81, 3141.51, None, None]\\\\n}\\\\n\\\\n# Plot a line graph\\\\nplt.figure(figsize=(10, 5))\\\\nplt.plot(gdp_data[\\'Year\\'], gdp_data[\\'GDP (Billion USD)\\'], marker=\\'o\\')\\\\n\\\\n# Title and labels\\\\nplt.title(\\'UK GDP from 2018 to 2023\\')\\\\nplt.xlabel(\\'Year\\')\\\\nplt.ylabel(\\'GDP (Billion USD)\\')\\\\n\\\\n# Show grid\\\\nplt.grid(True)\\\\n\\\\n# Display the graph\\\\nplt.show()\"}', 'name': 'python_repl'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 240, 'prompt_tokens': 12291, 'total_tokens': 12531}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='chart_generator', id='run-6cff57bc-ba87-4690-9528-4d15bba7986c-0', tool_calls=[{'name': 'python_repl', 'args': {'code': \"import matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# The data for 2022 and 2023 are placeholders and should be replaced with actual figures when available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021, 2022, 2023],\\n 'GDP (Billion USD)': [2851.41, 2697.81, 3141.51, None, None]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2023')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\"}, 'id': 'call_JPVxDAzEFi21crVT7Rt6SRJl'}])], 'sender': 'chart_generator'}}\n", "----\n", "{'call_tool': {'messages': [ToolMessage(content=\"Successfully executed:\\n```python\\nimport matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# The data for 2022 and 2023 are placeholders and should be replaced with actual figures when available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021, 2022, 2023],\\n 'GDP (Billion USD)': [2851.41, 2697.81, 3141.51, None, None]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2023')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\\n```\\nStdout: ValueError('x and y must have same first dimension, but have shapes (6,) and (5,)')\\n\\nIf you have completed all tasks, respond with FINAL ANSWER.\", name='python_repl', tool_call_id='call_JPVxDAzEFi21crVT7Rt6SRJl')]}}\n", "----\n" @@ -281,11 +281,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'chart_generator': {'messages': [AIMessage(content=\"Unfortunately, I made an error in the provided code. There was a mismatch in the dimensions of the 'Year' and 'GDP (Billion USD)' data arrays, which caused a ValueError. Additionally, I mistakenly included placeholders for the years 2022 and 2023 without having the actual GDP data for those years.\\n\\nLet's correct this and generate a line graph with the data we have up to 2021. I will revise the code to only include the years for which we have data, and then we can create the graph accordingly.\", additional_kwargs={'tool_calls': [{'id': 'call_1zGQMGouC0oFQJRUkNPvs9zX', 'function': {'arguments': '{\"code\":\"import matplotlib.pyplot as plt\\\\n\\\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\\\n# No data for 2022 and 2023 are available\\\\n# Note: 2021 data is used as a placeholder and should be updated when actual figures are available\\\\ngdp_data = {\\\\n \\'Year\\': [2018, 2019, 2020, 2021],\\\\n \\'GDP (Billion USD)\\': [2851.41, 2851.41, 2697.81, 3141.51]\\\\n}\\\\n\\\\n# Plot a line graph\\\\nplt.figure(figsize=(10, 5))\\\\nplt.plot(gdp_data[\\'Year\\'], gdp_data[\\'GDP (Billion USD)\\'], marker=\\'o\\')\\\\n\\\\n# Title and labels\\\\nplt.title(\\'UK GDP from 2018 to 2021\\')\\\\nplt.xlabel(\\'Year\\')\\\\nplt.ylabel(\\'GDP (Billion USD)\\')\\\\n\\\\n# Show grid\\\\nplt.grid(True)\\\\n\\\\n# Display the graph\\\\nplt.show()\"}', 'name': 'python_repl'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 359, 'prompt_tokens': 12796, 'total_tokens': 13155}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='chart_generator', id='run-0d4a67d2-696a-4955-990b-9a9d775b7635-0', tool_calls=[{'name': 'python_repl', 'args': {'code': \"import matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# No data for 2022 and 2023 are available\\n# Note: 2021 data is used as a placeholder and should be updated when actual figures are available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021],\\n 'GDP (Billion USD)': [2851.41, 2851.41, 2697.81, 3141.51]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2021')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\"}, 'id': 'call_1zGQMGouC0oFQJRUkNPvs9zX'}])], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"Unfortunately, I made an error in the provided code. There was a mismatch in the dimensions of the 'Year' and 'GDP (Billion USD)' data arrays, which caused a ValueError. Additionally, I mistakenly included placeholders for the years 2022 and 2023 without having the actual GDP data for those years.\\n\\nLet's correct this and generate a line graph with the data we have up to 2021. I will revise the code to only include the years for which we have data, and then we can create the graph accordingly.\", additional_kwargs={'tool_calls': [{'id': 'call_1zGQMGouC0oFQJRUkNPvs9zX', 'function': {'arguments': '{\"code\":\"import matplotlib.pyplot as plt\\\\n\\\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\\\n# No data for 2022 and 2023 are available\\\\n# Note: 2021 data is used as a placeholder and should be updated when actual figures are available\\\\ngdp_data = {\\\\n \\'Year\\': [2018, 2019, 2020, 2021],\\\\n \\'GDP (Billion USD)\\': [2851.41, 2851.41, 2697.81, 3141.51]\\\\n}\\\\n\\\\n# Plot a line graph\\\\nplt.figure(figsize=(10, 5))\\\\nplt.plot(gdp_data[\\'Year\\'], gdp_data[\\'GDP (Billion USD)\\'], marker=\\'o\\')\\\\n\\\\n# Title and labels\\\\nplt.title(\\'UK GDP from 2018 to 2021\\')\\\\nplt.xlabel(\\'Year\\')\\\\nplt.ylabel(\\'GDP (Billion USD)\\')\\\\n\\\\n# Show grid\\\\nplt.grid(True)\\\\n\\\\n# Display the graph\\\\nplt.show()\"}', 'name': 'python_repl'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 359, 'prompt_tokens': 12796, 'total_tokens': 13155}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, name='chart_generator', id='run-0d4a67d2-696a-4955-990b-9a9d775b7635-0', tool_calls=[{'name': 'python_repl', 'args': {'code': \"import matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# No data for 2022 and 2023 are available\\n# Note: 2021 data is used as a placeholder and should be updated when actual figures are available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021],\\n 'GDP (Billion USD)': [2851.41, 2851.41, 2697.81, 3141.51]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2021')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\"}, 'id': 'call_1zGQMGouC0oFQJRUkNPvs9zX'}])], 'sender': 'chart_generator'}}\n", "----\n", "{'call_tool': {'messages': [ToolMessage(content=\"Successfully executed:\\n```python\\nimport matplotlib.pyplot as plt\\n\\n# UK GDP data obtained from search results for 2018-2021 (in billion USD)\\n# No data for 2022 and 2023 are available\\n# Note: 2021 data is used as a placeholder and should be updated when actual figures are available\\ngdp_data = {\\n 'Year': [2018, 2019, 2020, 2021],\\n 'GDP (Billion USD)': [2851.41, 2851.41, 2697.81, 3141.51]\\n}\\n\\n# Plot a line graph\\nplt.figure(figsize=(10, 5))\\nplt.plot(gdp_data['Year'], gdp_data['GDP (Billion USD)'], marker='o')\\n\\n# Title and labels\\nplt.title('UK GDP from 2018 to 2021')\\nplt.xlabel('Year')\\nplt.ylabel('GDP (Billion USD)')\\n\\n# Show grid\\nplt.grid(True)\\n\\n# Display the graph\\nplt.show()\\n```\\nStdout: \\n\\nIf you have completed all tasks, respond with FINAL ANSWER.\", name='python_repl', tool_call_id='call_1zGQMGouC0oFQJRUkNPvs9zX')]}}\n", "----\n", - "{'chart_generator': {'messages': [AIMessage(content=\"FINAL ANSWER\\n\\nI have generated a line graph for the UK's GDP from 2018 to 2021 using the available data. Unfortunately, due to the lack of data for 2022 and 2023, the graph only includes figures up to 2021. Here is the graph:\\n\\n[Graph Image]\\n\\nPlease note that the data for 2022 and 2023 should be added to this graph once it becomes available to complete the analysis for the past five years.\", response_metadata={'token_usage': {'completion_tokens': 99, 'prompt_tokens': 13412, 'total_tokens': 13511}, 'model_name': 'gpt-4-1106-preview', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-3474a61c-0773-4e44-bd6e-2e88cf56bb90-0')], 'sender': 'chart_generator'}}\n", + "{'chart_generator': {'messages': [AIMessage(content=\"FINAL ANSWER\\n\\nI have generated a line graph for the UK's GDP from 2018 to 2021 using the available data. Unfortunately, due to the lack of data for 2022 and 2023, the graph only includes figures up to 2021. Here is the graph:\\n\\n[Graph Image]\\n\\nPlease note that the data for 2022 and 2023 should be added to this graph once it becomes available to complete the analysis for the past five years.\", response_metadata={'token_usage': {'completion_tokens': 99, 'prompt_tokens': 13412, 'total_tokens': 13511}, 'model_name': 'gpt-4o', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, name='chart_generator', id='run-3474a61c-0773-4e44-bd6e-2e88cf56bb90-0')], 'sender': 'chart_generator'}}\n", "----\n" ] } diff --git a/examples/pass_private_state.ipynb b/examples/pass_private_state.ipynb index d34ecc7e1..b790e312e 100644 --- a/examples/pass_private_state.ipynb +++ b/examples/pass_private_state.ipynb @@ -92,14 +92,6 @@ "\n", "graph.invoke({\"question\": \"foo\"})" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3ffc2d8c-717f-42c9-b0aa-15b178a5cc8b", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/persistence.ipynb b/examples/persistence.ipynb index 715f7c0b5..376706da5 100644 --- a/examples/persistence.ipynb +++ b/examples/persistence.ipynb @@ -561,14 +561,6 @@ "):\n", " event[\"messages\"][-1].pretty_print()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eb20430f", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/rag/langgraph_agentic_rag.ipynb b/examples/rag/langgraph_agentic_rag.ipynb index 73d02f14a..56c4e7f30 100644 --- a/examples/rag/langgraph_agentic_rag.ipynb +++ b/examples/rag/langgraph_agentic_rag.ipynb @@ -500,14 +500,6 @@ " pprint.pprint(value, indent=2, width=80, depth=None)\n", " pprint.pprint(\"\\n---\\n\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "189333cc-5d34-4869-9f9b-741210e1096f", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/self-discover/self-discover.ipynb b/examples/self-discover/self-discover.ipynb index 9c4dbfeef..2ac5ca8fa 100644 --- a/examples/self-discover/self-discover.ipynb +++ b/examples/self-discover/self-discover.ipynb @@ -246,14 +246,6 @@ "):\n", " print(s)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "20cac598", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/state-context-key.ipynb b/examples/state-context-key.ipynb index 3b6191fe6..e487106b2 100644 --- a/examples/state-context-key.ipynb +++ b/examples/state-context-key.ipynb @@ -465,14 +465,6 @@ "for chunk in app.stream(inputs, stream_mode=\"values\"):\n", " chunk[\"messages\"][-1].pretty_print()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "296c7456-da05-4326-95dc-47d6b312da9d", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/state-model.ipynb b/examples/state-model.ipynb index 897c3c571..9487c9801 100644 --- a/examples/state-model.ipynb +++ b/examples/state-model.ipynb @@ -439,14 +439,6 @@ "for chunk in app.stream(inputs, stream_mode=\"values\"):\n", " chunk[\"messages\"][-1].pretty_print()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "296c7456-da05-4326-95dc-47d6b312da9d", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/stream-multiple.ipynb b/examples/stream-multiple.ipynb index 1fa7e8533..79c14af1b 100644 --- a/examples/stream-multiple.ipynb +++ b/examples/stream-multiple.ipynb @@ -50,7 +50,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "OPENAI_API_KEY: ········\n" @@ -175,14 +175,6 @@ " print(chunk)\n", " print(\"\\n\\n\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8cc57240-243c-4d9a-a845-cb55ef973a59", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/stream-updates.ipynb b/examples/stream-updates.ipynb index 95d99ba4f..371883fc2 100644 --- a/examples/stream-updates.ipynb +++ b/examples/stream-updates.ipynb @@ -55,7 +55,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "OPENAI_API_KEY: ········\n" @@ -151,14 +151,6 @@ " print(values)\n", " print(\"\\n\\n\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8cc57240-243c-4d9a-a845-cb55ef973a59", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/streaming-content.ipynb b/examples/streaming-content.ipynb index 8c2c7ab36..76117c029 100644 --- a/examples/streaming-content.ipynb +++ b/examples/streaming-content.ipynb @@ -120,14 +120,6 @@ " # So we only print non-empty content\n", " print(data, end=\"|\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "615cb9d2-bfa2-4f83-90b0-c6c2d1e6df95", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/web-navigation/web_voyager.ipynb b/examples/web-navigation/web_voyager.ipynb index 8959d2025..dae64975a 100644 --- a/examples/web-navigation/web_voyager.ipynb +++ b/examples/web-navigation/web_voyager.ipynb @@ -751,14 +751,6 @@ ")\n", "print(f\"Final response: {res}\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dcb656d9-fff5-42b0-9b16-60716a51ca0b", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 112700810ced2f93432ca6a91441dbd6f6f0d0e7 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 28 Aug 2024 12:31:43 -0700 Subject: [PATCH 166/287] Fix --- libs/langgraph/langgraph/constants.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index a7c9896f6..c1024ea9c 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -85,7 +85,6 @@ class Send: Args: node (str): The name of the target node to send the message to. arg (Any): The state or message to send to the target node. - id (str): ID associated with the Send. """ self.node = node self.arg = arg From 03f6cf779d25687f88ba9f144be533d866a8b529 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 28 Aug 2024 12:48:29 -0700 Subject: [PATCH 167/287] Fix test setup --- libs/langgraph/Makefile | 6 ++++-- libs/langgraph/pyproject.toml | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/langgraph/Makefile b/libs/langgraph/Makefile index feecd3fba..1500c84ef 100644 --- a/libs/langgraph/Makefile +++ b/libs/langgraph/Makefile @@ -20,16 +20,18 @@ start-postgres: stop-postgres: docker compose -f tests/compose-postgres.yml down +TEST_PATH ?= . + test: make start-postgres; \ - poetry run pytest; \ + poetry run pytest $(TEST_PATH); \ EXIT_CODE=$$?; \ make stop-postgres; \ exit $$EXIT_CODE test_watch: make start-postgres; \ - poetry run ptw .; \ + poetry run ptw . -- --ff -v -x -n auto --dist worksteal --snapshot-update --tb short $(TEST_PATH); \ EXIT_CODE=$$?; \ make stop-postgres; \ exit $$EXIT_CODE diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 8d1dfd694..de8d1fcd7 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -66,7 +66,6 @@ omit = ["tests/*"] [tool.pytest-watcher] now = true delay = 0.1 -runner_args = ["--ff", "-v", "-x", "-n", "auto", "--dist", "worksteal", "--snapshot-update", "--tb", "short"] patterns = ["*.py"] [build-system] From c4010c03a60f9f8287bd551393c745a25f664c74 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 28 Aug 2024 12:50:35 -0700 Subject: [PATCH 168/287] Udpate async tests --- libs/langgraph/tests/test_pregel_async.py | 71 ++++++++--------------- 1 file changed, 24 insertions(+), 47 deletions(-) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 4b1a0e72e..6d66ddac8 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -6788,7 +6788,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here", @@ -6826,7 +6826,6 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -6951,7 +6950,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here and there", @@ -6984,7 +6983,6 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -7179,7 +7177,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here", @@ -7217,7 +7215,6 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -7347,7 +7344,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here and there", @@ -7380,7 +7377,6 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -7470,7 +7466,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here", @@ -7508,7 +7504,6 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -7609,7 +7604,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here and there", @@ -7642,7 +7637,6 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -7769,7 +7763,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here and there", @@ -7802,7 +7796,6 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -7884,7 +7877,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here", @@ -7922,7 +7915,6 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -8051,7 +8043,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here", @@ -8111,7 +8103,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here", @@ -8270,7 +8262,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here", @@ -8303,7 +8295,6 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -8331,7 +8322,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here and there", @@ -8364,7 +8355,6 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -8720,7 +8710,7 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={"my_key": "hi my value here", "my_other_key": "hi my value"}, tasks=( @@ -8756,7 +8746,6 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ) @@ -8790,7 +8779,7 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here", @@ -8829,7 +8818,6 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -8853,7 +8841,6 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ), StateSnapshot( values={}, @@ -8873,7 +8860,6 @@ async def test_nested_graph_state( }, created_at=AnyStr(), parent_config=None, - subgraph_state_snapshots=None, ), ] await app.ainvoke(None, config, debug=True) @@ -8934,7 +8920,6 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ), StateSnapshot( values={"my_key": "hi my value here and there"}, @@ -8960,7 +8945,6 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ), StateSnapshot( values={"my_key": "hi my value"}, @@ -8986,7 +8970,7 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "inner": StateSnapshot( values={ "my_key": "hi my value here and there", @@ -9019,7 +9003,6 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ), @@ -9043,7 +9026,6 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ), StateSnapshot( values={}, @@ -9063,7 +9045,6 @@ async def test_nested_graph_state( }, created_at=AnyStr(), parent_config=None, - subgraph_state_snapshots=None, ), ] assert actual_history == expected_history @@ -9154,7 +9135,7 @@ async def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "child": StateSnapshot( values={"my_key": "hi my value"}, tasks=( @@ -9180,7 +9161,7 @@ async def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "child_1": StateSnapshot( values={"my_key": "hi my value here"}, tasks=( @@ -9210,7 +9191,6 @@ async def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ) @@ -9271,7 +9251,7 @@ async def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "child": StateSnapshot( values={"my_key": "hi my value here and there"}, tasks=(), @@ -9296,7 +9276,7 @@ async def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ + subgraphs={ "child_1": StateSnapshot( values={"my_key": "hi my value here and there"}, tasks=(), @@ -9323,7 +9303,6 @@ async def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ) }, ) @@ -9379,12 +9358,12 @@ async def test_send_to_nested_graphs( "jokes": [], } actual_snapshot = await graph.aget_state(config) - subgraph_nodes = list(actual_snapshot.subgraph_state_snapshots.keys()) + subgraph_nodes = list(actual_snapshot.subgraphs.keys()) assert len(subgraph_nodes) == 2 for subgraph_node in subgraph_nodes: assert subgraph_node.split(":")[0] == "generate_joke" - subgraph_state_snapshots = { + subgraphs = { subgraph_node: await graph.aget_state( {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} ) @@ -9413,7 +9392,7 @@ async def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=subgraph_state_snapshots, + subgraphs=subgraphs, ) assert actual_snapshot == expected_snapshot @@ -9462,7 +9441,7 @@ async def test_send_to_nested_graphs( # test full history actual_history = [c async for c in graph.aget_state_history(config)] # get subgraph node state for expected history - subgraph_state_snapshots = { + subgraphs = { subgraph_node: await graph.aget_state( {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} ) @@ -9501,7 +9480,6 @@ async def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ), StateSnapshot( values={"subjects": ["cats", "dogs"], "jokes": []}, @@ -9526,7 +9504,7 @@ async def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=subgraph_state_snapshots, + subgraphs=subgraphs, ), StateSnapshot( values={"jokes": []}, @@ -9546,7 +9524,6 @@ async def test_send_to_nested_graphs( }, created_at=AnyStr(), parent_config=None, - subgraph_state_snapshots=None, ), ] assert actual_history == expected_history From 4769122828da2ea1293fb3430d7f667e2caee663 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Wed, 28 Aug 2024 16:40:06 -0400 Subject: [PATCH 169/287] docs: fix map-reduce issue (#1523) --- examples/map-reduce.ipynb | 40 +++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/examples/map-reduce.ipynb b/examples/map-reduce.ipynb index cefecbc96..055ca43b7 100644 --- a/examples/map-reduce.ipynb +++ b/examples/map-reduce.ipynb @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "3eb04cd1", "metadata": {}, "outputs": [], @@ -36,10 +36,18 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "dc292321", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "ANTHROPIC_API_KEY: ········\n" + ] + } + ], "source": [ "import os\n", "import getpass\n", @@ -55,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 3, "id": "0f0f78e4-423d-4e2d-aa1a-01efaec4715f", "metadata": {}, "outputs": [], @@ -63,7 +71,7 @@ "import operator\n", "from typing import Annotated, TypedDict\n", "\n", - "from langchain_core.pydantic_v1 import BaseModel\n", + "from langchain_core.pydantic_v1 import BaseModel, Field\n", "from langchain_anthropic import ChatAnthropic\n", "\n", "from langgraph.constants import Send\n", @@ -87,7 +95,7 @@ "\n", "\n", "class BestJoke(BaseModel):\n", - " id: int\n", + " id: int = Field(description=\"Index of the best joke, starting with 0\")\n", "\n", "\n", "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", @@ -161,7 +169,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 4, "id": "37ed1f71-63db-416f-b715-4617b33d4b7f", "metadata": {}, "outputs": [ @@ -172,7 +180,7 @@ "" ] }, - "execution_count": 11, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -185,7 +193,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 5, "id": "fd90cace", "metadata": {}, "outputs": [ @@ -193,12 +201,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'generate_topics': {'subjects': ['lion', 'elephant', 'penguin', 'dolphin']}}\n", + "{'generate_topics': {'subjects': ['Lions', 'Elephants', 'Penguins', 'Dolphins']}}\n", "{'generate_joke': {'jokes': [\"Why don't elephants use computers? They're afraid of the mouse!\"]}}\n", - "{'generate_joke': {'jokes': [\"Why don't dolphins use smartphones? They're afraid of phishing!\"]}}\n", - "{'generate_joke': {'jokes': [\"Why don't lions like fast food? Because they can't catch it!\"]}}\n", + "{'generate_joke': {'jokes': [\"Why don't dolphins use smartphones? Because they're afraid of phishing!\"]}}\n", "{'generate_joke': {'jokes': [\"Why don't you see penguins in Britain? Because they're afraid of Wales!\"]}}\n", - "{'best_joke': {'best_selected_joke': \"Why don't you see penguins in Britain? Because they're afraid of Wales!\"}}\n" + "{'generate_joke': {'jokes': [\"Why don't lions like fast food? Because they can't catch it!\"]}}\n", + "{'best_joke': {'best_selected_joke': \"Why don't dolphins use smartphones? Because they're afraid of phishing!\"}}\n" ] } ], @@ -211,9 +219,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "langgraph", "language": "python", - "name": "python3" + "name": "langgraph" }, "language_info": { "codemirror_mode": { @@ -225,7 +233,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" } }, "nbformat": 4, From 6962aa5acc079596b218db5eaafdd719605bd47e Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Wed, 28 Aug 2024 18:50:55 -0700 Subject: [PATCH 170/287] ocd (#1525) --- docs/mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 20b357da8..779a804c4 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -176,7 +176,7 @@ nav: - Create a ReAct agent: how-tos/create-react-agent.ipynb - Add memory to a ReAct agent: how-tos/create-react-agent-memory.ipynb - Add a system prompt to a ReAct agent: how-tos/create-react-agent-system-prompt.ipynb - - Add human-in-the-Loop to a ReAct agent: how-tos/create-react-agent-hitl.ipynb + - Add Human-in-the-loop to a ReAct agent: how-tos/create-react-agent-hitl.ipynb - "Conceptual Guides": - "concepts/index.md" - LangGraph for Agentic Applications: concepts/high_level.md From 6111d37b99a7e5710a8e181db3edad343b4401be Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Wed, 28 Aug 2024 20:45:23 -0700 Subject: [PATCH 171/287] [docs]: different structured output method (#1497) * wip * spelling * consistent headers * slight changes * spelling * more comments * link * changes * diagrams * format * final nits * x --- docs/_scripts/copy_notebooks.py | 1 + examples/option1.png | Bin 0 -> 23492 bytes examples/option2.png | Bin 0 -> 31338 bytes examples/react-agent-structured-output.ipynb | 270 +++++++++++++------ examples/react_diagrams.png | Bin 0 -> 81547 bytes 5 files changed, 195 insertions(+), 76 deletions(-) create mode 100644 examples/option1.png create mode 100644 examples/option2.png create mode 100644 examples/react_diagrams.png diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 9cc2e8a76..1a3af4487 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -63,6 +63,7 @@ _MANUAL = { "human_in_the_loop/wait-user-input.ipynb", "human_in_the_loop/review-tool-calls.ipynb", "node-retries.ipynb", + "react_diagrams.png", "react-agent-structured-output.ipynb", ], "tutorials": [ diff --git a/examples/option1.png b/examples/option1.png new file mode 100644 index 0000000000000000000000000000000000000000..485a48b6520bf64cff00ef2a653844326647dc61 GIT binary patch literal 23492 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 Hap3<4u93~| literal 0 HcmV?d00001 diff --git a/examples/react-agent-structured-output.ipynb b/examples/react-agent-structured-output.ipynb index 91e2b0dc6..6ffd9775f 100644 --- a/examples/react-agent-structured-output.ipynb +++ b/examples/react-agent-structured-output.ipynb @@ -8,74 +8,65 @@ "\n", "You might want your agent to return its output in a structured format. For example, if the output of the agent is used by some other downstream software, you may want the output to be in the same structured format every time the agent is invoked to ensure consistency.\n", "\n", - "This guide shows how you can do this. We will be using a basic [ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/) (a model node and a tool-calling node) together with a third node at the end that will format response for the user.\n", + "This notebook will walk through two different options for forcing a function calling agent to structure its output. We will be using a basic [ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/) (a model node and a tool-calling node) together with a third node at the end that will format response for the user. Both of the options will use the same graph structure as shown in the diagram below, but will have different mechanisms under the hood.\n", "\n", + "![React Diagrams](./react_diagrams.png)\n", + "\n", + "**Option 1**\n", + "\n", + "![option1](./option1.png)\n", + "\n", + "The first way you can force your tool calling agent to have structured output is to bind the output you would like as an additional tool for the `agent` node to use. In contrast to the basic ReAct agent, the `agent` node in this case is not selecting between `tools` and `END` but rather selecting between the specific tools it calls. The expected flow in this case is that the LLM in the `agent` node will first select the action tool, and after receiving the action tool output it will call the response tool, which will then route to the `respond` node which simply structures the arguments from the `agent` node tool call.\n", + "\n", + "**Pros and Cons**\n", + "\n", + "The benefit to this format is that you only need one LLM, and can save money and latency because of this. The downside to this option is that it isn't guaranteed that the single LLM will call the correct tool when you want it to. We can help the LLM by setting `tool_choice` to `any` when we use `bind_tools` which forces the LLM to select at least one tool at every turn, but this is far from a fool proof strategy. In addition, another downside is that the agent might call *multiple* tools, so we need to check for this explicitly in our routing function (or if we are using OpenAI we an set `parallell_tool_calling=False` to ensure only one tool is called at a time).\n", + "\n", + "**Option 2**\n", + "\n", + "![option1](./option2.png)\n", + "\n", + "The second way you can force your tool calling agent to have structured output is to use a second LLM (in this case `model_with_structured_output`) to respond to the user. \n", + "\n", + "In this case, you will define a basic ReAct agent normally, but instead of having the `agent` node choose between the `tools` node and ending the conversation, the `agent` node will choose between the `tools` node and the `respond` node. The `respond` node will contain a second LLM that uses structured output, and once called will return directly to the user. You can think of this method as basic ReAct with one extra step before responding to the user. \n", + "\n", + "**Pros and Cons**\n", + "\n", + "The benefit of this method is that it guarantees structured output (as long as `.with_structured_output` works as expected with the LLM). The downside to using this approach is that it requires making an additional LLM call before responding to the user, which can increase costs as well as latency. In addition, by not providing the `agent` node LLM with information about the desired output schema there is a risk that the `agent` LLM will fail to call the correct tools required to answer in the correct output schema.\n", + "\n", + "Note that both of these options will follow the exact same graph structure (see the diagram above), in that they are both exact replicas of the basic ReAct architecture but with a `respond` node before the end.\n", "\n", "## Setup\n", "\n", - "### Structured Output\n", + "For our setup we need to define how we want to structure our output, define our graph state, and also our tools and the models we are going to use.\n", "\n", - "First we need to define how we want to structure our output. To do this, we will use the `with_structured_output` method from LangChain, which you can read more about [here](https://python.langchain.com/v0.2/docs/how_to/structured_output/)." + "To use structured output, we will use the `with_structured_output` method from LangChain, which you can read more about [here](https://python.langchain.com/v0.2/docs/how_to/structured_output/).\n", + "\n", + "We are going to use a single tool in this example for finding the weather, and will return a structured weather response to the user." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "from pydantic import BaseModel, Field\n", - "\n", - "class WeatherResponse(BaseModel):\n", - " temperature: float = Field(description=\"The temperature in fahrenheit\")\n", - " wind_directon: str = Field(description=\"The direction of the wind in abbreviated form\")\n", - " wind_speed: float = Field(description=\"The speed of the wind in km/h\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Graph State\n", - "\n", - "We can now define our graph state:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Annotated, Any\n", - "from typing_extensions import TypedDict\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "class AgentState(TypedDict):\n", - " # list of chat messages from user, LLM, and tools\n", - " messages: Annotated[list, add_messages]\n", - " # Final structured response from the agent\n", - " final_response: WeatherResponse" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Tools + Models\n", - "\n", - "We can now instantiate the tools and models we are going to use in our graph. We are going to use a single tool in this example for finding the weather, and we are going to have two models in our graph, one that does the function calling and one that does the responding." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Literal\n", + "from typing import Literal\n", "from langchain_core.tools import tool\n", "from langchain_anthropic import ChatAnthropic\n", + "from langgraph.graph import MessagesState\n", + "\n", + "class WeatherResponse(BaseModel):\n", + " \"\"\"Respond to the user with this\"\"\"\n", + " temperature: float = Field(description=\"The temperature in fahrenheit\")\n", + " wind_directon: str = Field(description=\"The direction of the wind in abbreviated form\")\n", + " wind_speed: float = Field(description=\"The speed of the wind in km/h\")\n", + "\n", + "# Inherit 'messages' key from MessagesState, which is a list of chat messages \n", + "class AgentState(MessagesState):\n", + " # Final structured response from the agent\n", + " final_response: WeatherResponse\n", "\n", "@tool\n", "def get_weather(city: Literal[\"nyc\", \"sf\"]):\n", @@ -99,47 +90,52 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "## Option 1: Bind output as tool\n", + "\n", + "Let's now examine how we would use the single LLM option.\n", + "\n", "### Define Graph\n", "\n", - "Now that we have defined our tools and models, we can define our graph." + "The graph definition is very similar to the one above, the only difference is we no longer call an LLM in the `response` node, and instead bind the `WeatherResponse` tool to our LLM that already contains the `get_weather` tool." ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "from langgraph.graph import StateGraph, END\n", "from langgraph.prebuilt import ToolNode\n", - "from langchain_core.messages import HumanMessage\n", "\n", - "# Define the function that determines whether to continue or not\n", - "def should_continue(state: AgentState):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " # If there is no function call, then we respond to the user\n", - " if not last_message.tool_calls:\n", - " return \"respond\"\n", - " # Otherwise if there is, we continue\n", - " else:\n", - " return \"continue\"\n", + "tools = [get_weather, WeatherResponse]\n", + "\n", + "# Force the model to use tools by passing tool_choice=\"any\" \n", + "model_with_response_tool = model.bind_tools(tools,tool_choice=\"any\")\n", "\n", "# Define the function that calls the model\n", "def call_model(state: AgentState):\n", - " response = model_with_tools.invoke(state['messages'])\n", + " response = model_with_response_tool.invoke(state['messages'])\n", " # We return a list, because this will get added to the existing list\n", " return {\"messages\": [response]}\n", "\n", "# Define the function that responds to the user\n", "def respond(state: AgentState):\n", - " # We call the model with structured output in order to return the same format to the user every time\n", - " # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n", - " # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n", - " response = model_with_structured_output.invoke([HumanMessage(content=state['messages'][-2].content)])\n", + " # Construct the final answer from the arguments of the last tool call\n", + " response = WeatherResponse(**state['messages'][-1].tool_calls[0]['args'])\n", " # We return the final answer\n", " return {\"final_response\": response}\n", "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state: AgentState):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is only one tool call and it is the response tool call we respond to the user\n", + " if len(last_message.tool_calls) == 1 and last_message.tool_calls[0]['name'] == \"WeatherResponse\":\n", + " return \"respond\"\n", + " # Otherwise we will use the tool node again\n", + " else:\n", + " return \"continue\"\n", "\n", "# Define a new graph\n", "workflow = StateGraph(AgentState)\n", @@ -172,14 +168,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Usage\n", + "### Usage\n", "\n", - "We can now invoke our graph to verify that the output is being structured as desired:" + "Now we can run our graph to check that it worked as intended:" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -188,7 +184,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -197,7 +193,129 @@ "WeatherResponse(temperature=75.0, wind_directon='SE', wind_speed=3.0)" ] }, - "execution_count": 6, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Again, the agent returned a `WeatherResponse` object as we expected." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Option 2: 2 LLMs\n", + "\n", + "Let's now dive into how we would use a second LLM to force structured output.\n", + "\n", + "### Define Graph\n", + "\n", + "We can now define our graph:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "from langgraph.prebuilt import ToolNode\n", + "from langchain_core.messages import HumanMessage\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state: AgentState):\n", + " response = model_with_tools.invoke(state['messages'])\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define the function that responds to the user\n", + "def respond(state: AgentState):\n", + " # We call the model with structured output in order to return the same format to the user every time\n", + " # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n", + " # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n", + " response = model_with_structured_output.invoke([HumanMessage(content=state['messages'][-2].content)])\n", + " # We return the final answer\n", + " return {\"final_response\": response}\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state: AgentState):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we respond to the user\n", + " if not last_message.tool_calls:\n", + " return \"respond\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"respond\", respond)\n", + "workflow.add_node(\"tools\", ToolNode(tools))\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " \"agent\",\n", + " should_continue,\n", + " {\n", + " \"continue\": \"tools\",\n", + " \"respond\": \"respond\",\n", + " },\n", + ")\n", + "\n", + "workflow.add_edge(\"tools\", \"agent\")\n", + "workflow.add_edge(\"respond\", END)\n", + "graph = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "### Usage\n", + "\n", + "We can now invoke our graph to verify that the output is being structured as desired:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "answer = graph.invoke(input={\"messages\": [(\"human\", \"what's the weather in SF?\")]})['final_response']" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "WeatherResponse(temperature=75.0, wind_directon='SE', wind_speed=4.83)" + ] + }, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } diff --git a/examples/react_diagrams.png b/examples/react_diagrams.png new file mode 100644 index 0000000000000000000000000000000000000000..9d755884ca4e0fdfad6361f8bd93165f42afc771 GIT binary patch literal 81547 zcmbrmbzD?!+cpZr0E5)f4MPYb9nvv$w@5cghjix%B3;tmA|28pDBTSbD&5_&2k-lN z-{<#zd;hhEKW5FkVqJNz>pYI*BwSTl2J;!|GXw+#OgULebp!+eCISMYKL`bW2T_{w zE&K~;DW)WbfKVNSerJLV{|zydRaZhl@TNyV2!tUZ+`w-IZXqDJu_GYt7$YDEq#z&= zIb}4f3BeOoEwts{Dk&kngkOUY0EoBCin*_2l0PXa{&KI z1z_eN{pT9d|0(0(K7Kg7p`6yQwOzH96a~y2?O9FC9ZfA*J?)*IG9UhQU}N*}@L=`eVs&)3WaHrH=VxQ*WaH#yf#+ax@p5oA@nmsuq5Y?l z|I{OC;bP`&?c{3h=m2@D*TmG(%~hC&=IKHI^ZBQp7M|As`y>aKf4c?mAlp+48wV>p z+y7fJSL?U`AH|+h{wemiU;jK#=&3RRRclWRI~_@DdkY5_c+*6<_&A0BKIZ>Q`QL{A zS4r*vE%}P`)qj`#uay6@q=ciLqqBySiJ656$G=7X>(+neermUXinFx^d`O=L?kW5K zb?@K<pc?`d_2`&lLW>3!g)gXF_cMvmQjAnUcJPuO29ZoTS)mPsF`8)C_v5`iB7e zQM+KJsw#m~2L2HjsOR2*3iy9Q4nS}1$vx`UgH8rQpjd1`$PzV?V{QE_W5VV6> z(YyY?cR)b$APDBM#J?GyzQm}(1tzNOQhEQCYXaQy-__s)#7I$)HRzuGQ|(hJcqL^2 zKPoX^m&z$G9~KZ0@J8%x4cy<~pP8SZ-}NQOzzT}{Xt2MUMpXK5Pd0|rE!kR+f?2ey za)8pBnn}}BQ`!;|Axf#7-|1Dd?A+Yk3U@=6#6Ae!?$u^phLSm0l~pwF7(22QrVDyh zOwZ2hnVIFQRsI5%mzQ6}1l)Dc82Z?+K(H%YPd>IC{~){ANO$!1D*ICNvAk&`ZTVth zLGcTezB9ZzaJUA8g@r}g<^@wy2lF7)=xzP1VJ*MQ#vG~b7ZW^s#TOm=0w{x8Mt;-p zkOX%GttaSuqf#t+6RW-~EUna{Pp~I4;9DaFL5&NgSG!QD>S{+t7PEW~_|pa4K5J@O z4(N(pfPM&H{+v`6*rr>ws~rwiB!|!%U40|ko6&Qur@G&gaqq#A!C;KJKHXe=HLGu? z=g?HBlr6FpKIA0yWivZqnYDR~M(`I5FN0XGW%ibpm~>7VlglMz!o+)Ffe$y4YG1**>AYWx@_%jSgf$)Ghkj$gmIRgagew2^*r>?GoLPmKj(z>K z-NoMgMdqPz@0L!s*G6h+tb>C?gWzH7By0PXDG`k7;V7(aKj3CEd#@FpdfImm2c_ghO({QW zRcPm~pm5b6cnBY|eZZ&IY|r0lyqaI?XLQ?!FxJHUL5@S%Yau^EmLCS$I_#{-ww||N z{f^+VU#K&DiwMNOoTycL=^DM_z00EKcRuZtnJ-o@2;gMlW z@FRvw<=p;u|LbK{ow(Wl*ha^&$eQW5w{X76=3XKI}|GDsa{y~9dHqXh> zk4O>;9!jgt-O7%GeG*=W3>N*yvB{#{bq?!*%a#?d2cx^+ME744Cibeju^zs(U({F5 z!^a=BH+56&9+b2IB`Y=ZYUv7XHfGH)7m`^Ra10_=yd5O6Yb?H-H+iYhF$=-m?w2Be zG9{hrkU-<^tdZW-7#_rFk%h2Dhn8W{4Up~Bl?Y-$PA zf=?N6_m0=c_$}X2CsJE9h;d@_RCqNINuOQcYdwkegNn$tc?VT)$BO@Lld6=JS(pr; z3uSPK>U(|PqYx@r05tXMihs-fzGJ|tx9LKl6XfhT3i?9qi`Ay!%C&L}eEX5(ar5>U zJ^rD3oV!#J+~=NcfMT$=ovAat0ZG9imj`W^%dA#vw-VxW;F4qm_f*jcS5e}-^U8qp z%_Dd-SCr^QulrT*QX!D84ZlO5>~xUl+p{|ksbis89ZQPdlo~6h;IzAbY*7^U9A$8G zJD`X>>$q3sf@9G*b6TWF{Z*P`Cw+7nI7)WNN8eL z6y968q5MFTkRM25a!U|SFyx@He9AW4@1oiWZW;XTSNtyBG(eJwrci+i$F}nsX>3xl zXI^6K(F5r<5MnEfqqdyZO-X185nn;z4A5D8l!V-xA^5T+Kl()AZk;XQ*aZv(F2L>i zF5v#C8{303vA!oQUhbK7mvsYQ{x5}F@?5NAL)){QGq9iDsyWy|@YEGM_^R)eTG}rPK>~C<{$m82 z1PYl9k?5pOITnU`(yVd3qif+2R)xJ1C&+!M&AuPX^4cI_!1b+jY(17fEdTQ%e3f36 zzD*p63qv9h^rDGPXz0eBGHE3H(X)Y5f4~5ME=V`LMKcDs3M-zSvgPWl8O%4mOU}7&ZzpXg4ue~~n?!Uo&s!;U6jo@WYVLNWN z6u~iLV_gJZ7>BS8H`l7r90FsG2uK6)Kx%wk$mZ8y&jIFN2$<2aY|>!ZhjP^{xi2=o z4gLc+jY~=re0~=Moum_*;p_!Iohd7Tx9@3!GTCMbCxt=cVy+ob~4j8v62 z&f!!p0>J>mjOQ=5De~$A?Q%jH*kFKdr}z*kaEbI z43*zhHbYuI49y$?FLKOfL`Rv9G1V@+#0-e}`0ro%unMpKC zFan8KRlZc`j0zh*bqqviKMNrK!M;iyWtyuU(MlQ|3wj3{TTeDR*xb6F^eOasHZL|d zM(=$L973@oED?44h8-MLm!j-+5z+&7c!@0gOuRv;z}+dUp|&u#PqTVSAF1VG!GJYK z&S)rE=;L{pB~S^phEOZzd_qUzhJsfy>7V7OW)Xu@q~6k)90+CzaLS`x*M;mL&lsmw zZM1jQ1-X8Qwoy}lhx&DKTpKXUwKH}=Fww$4{GDKJ01%v0+;4)d0cfy?vAW=mnaO~B zcQmq{Wjg50AxLM2+g*QPVMd4xz`b+OG5!%trw78h5GeqLc$pWkf>;xT5}alJObRh$ zq>%U&=tiSrcMS5}JDL0OP964>-0y6BIYJsqe3a58btgK3P7=i)a4BXAI-5z&2Xr9U zF@gHzgU+PHvZK3b(Of^g!5vx?YpMR_65a4lbbB1d3A0wg3t}_@$x>n#|Xb9-hs$;vDc{<|#q1 zXz;YdWDFtpA$eZg)~kO zEC+Jhg_vZi*1-D5;c_t39G*ZGuNFos=B}6Znavaabre=_PzI8P2C@y*xNy=?z#sp* zI-Tk?ftw=VQkMCmojt#D#Ro0(n!rzeZek%Gg59^^p)0^DcQ{Ipep^(PWcN|8=)-tD zF`uY7r~i4GQBzua7E{(eA1S8*B4VhjnJ0tr8}hUGw)Q#h6Z4ek>v{W7&5`>^P~aV; zZmaN*WzaUw{6T+6%~4R`8KiN%MD=i!4vO=^ip&Zfkqc8i?m|OZJ?i%R9rh+88Ls%! z_(UwWH~|LQl!wu^5BndQg21!Y(BUke39jO3tG5s}E6>I-Uv@6&TnUq8&QIo8t$h4n z2p@kS<6*fKi(tH`!{%l;4zYg5ps6U3h-8u@vc8d^UbOH_CpRjuck~oV?+((LdeRqy z!+vKNzw!fW53Nf6n$AhBgmL=5L<}Fo2o~j*0EY&hJdZiCL`{|N7)^7*m&K?4$3x5s z0A6k7f0oV}-(`J94I>7ix4aUz;zqhc0)tFaN@sysULT){Y|GKm;oN|cfLNrs zL-e4?somnJx=f#~APG~|rb#BtgGUZ8#am!>1Om7!NOjPHQ@Wm|j}XR!6vUF=R2WHH=O+ zNq+0=_!{=_O&F04EDk%yY<_dZb|Ha<-Mt$tT)HzvO>n%f&9}FLfi!Qb&oH9PrzZG_ z{_%aQUV|V#I9s4-XNLORw?hnqrSmw3{VR}l2_29n&>8n)d%@W3BoIO^1itV?{(vH^ zw==G#lJ2h2d*V%as%Q@&gJ~F@hh|i%8SY#c`5p=9uqGz-k*A>n8jD>R@1Y#_`MjfT zJ?lXyCFtkc@sa+&KqDBP+R-FX+F46;qY!i>L$1HB7=S>?(nZu)m)C zCu)Fi=u`CEfyS~}|saq`>h*!2_jt`0dog{~l{2KiiE4!O?{ z^4q8aysC>Y9CNtulj7>g?hAh{KGTB}p$T(j(xJS5^g0rIhf%YI!5_#< znftd36v))F-7h>pjQ=}i&jFbROKD}$1f|m0Wevi{I%a%>RB~63CD!ZkXMlv6NdXr0)`C0t!7Jj*W*pe&&FIXUM2J3`|T zP~5kr@1N&>QpS)Ztx#aEE6!IE^V z_nrvnkd0*}lurx)UJ>~L&mi+ezB7|r?(s8B@1wPgN=wAgAe5rTIjU`wvn@sGn}iQH zs83T=B@P**z4QJYQf^32eHnaKPyLy*xHCRRLQS3UvWWxd*@cX0j<8pH^LM=F=FeQX z)Olk|OV-o%#&77rwAFDTWiD2vC}A1}f59bVeCk-BzE{FaFbeE>f5K|PVn*}GcNg=d zi5eBz?V9|!+pnz@Xh^0D8OBV7NL4`6;SpIPA6j8pkx7AQS4qHM=&L&z8t?1%7SxR| zP@e$I8}YCK<`G)g!GW8rmyusWKcv#^zGwQx53q;X(w!`O4%pEB&TlWYj+@R10@u{jhjC zy>=TFzwJoBR`_Ns|Ke;?W$_0Y9DZ!S4lQrL*@<9@EzoZiMyK^_EYwxgT(q0!>}5__ zF+nG)v$n0Cuicz0D8;i+HuzeL$wQiLP4>CvOTC3jLtbf$w54THXh|KnTd-xpcQI1w z1-yuxzvv3PIq;ddZ?z9sP2iiHwzkO~n^!Q^nrN15CZX#w=9cprJ$txYRIABQVJdjg z7;aQ)ugzRLz@Vj1QyCv=|-r{qeT_D007gQ@LRYEBzBU z=T$%_iOtfA$#=)!#}M#x=IBNA?wp1>dvx=%8s}0`}MPMSnkj!-HxYUNjO}nV1^+s{o1FXmX-l)g55bw zbBg=#cC-kF`sjW2CE_GN6y9!fA)0fWT{1olc&wg)nt$Uw|H5Cv=oE-mx;2hV3W@aqT&xJ`p;+piC zTYp-{OLm|j5Nx9moQ7JHLNI|t0@Hggx?_!u630}m9n&*9J_d{Lk}`|bD-Ufd}kieQdtFJ^1oV&FEd35WK`tvSE@Y^ky}%jW|Y1a->|qhn6| zopR;$^@@KbkrPS8LGedpR{e&(7{%W=PHh;*zM7#I65zh5D=LyOlOnW%Nwt6^!*YEg z^EfhlW?Yu-B{&S_BIMIy1)sc-BI#;kDUGa1adC0}F(Wn9fo~w#81=1d@q_z~W-FF^Oy?vp1hxQ$UG6$Tvv5td- zxAv*-Ke8iaK|hDD^{izWB;~z%qO~GB6aG2&7$=6BSblXB!fOi6 zUTK$U^UOGFFuJvVOFr>|nASe7IFbdgRk7PS;Y$&kY;e%xb0yfDVHo^OH81r zvL2P(r*3PT|D1V^3&o)~NaQy?WX)6W9%mC`7OpgR%HPzaQ-=%>J8w;{|H3UP#^E7tqlAK;08o3cv7mElBz`fF!K%o5)A?!^U` zovqavj;GWuz3Zj8n9@+WewGv))1Sa_J2p|Ut^Kj+y1dkYoGx!NA{I2dndL9hj#)YH ze`~+ro_`ej*+w?1j{3S5$Nuz=b92Ji137~8&&abkr6tP$PDhPYXR*X5>~00NHXAZK zA$~SRafSS6p9@98pko@+#6+*~71Yb7T2vO0ydNIVjUEfPON-}|+s~f+ob)s7NAWH@ z1^Mih7B57wwOdtoJT#{>Ldg5S9QWX<%t@2kn}<>PH{QO`_dWhzA$0`@jf@WGHMBl- zer-Qz5&RR$We4wSHJm+A2=*4ez1tLZ)U&I8pXt6}-7xU7EU^v;bZrRmmm@9lQ5RvX zRE$7)6<8rQ*SuG+wc?8Uy4bOa1TYYb43bzF_0WQ9NX3TUy(3E)0 zG_w7Qw{7()HOkKA6L(gbXXQw4$`dC9CME!#f$9|lz4GZ2H>?M?j{nSlH;n6NZtG}I zAFTZ4?ZHlVWUzY6?sH-HctGylM-AtO!mdBT_HK474_BIz(Fy0^{N2`y$j-p|sQ*Rn za|Ve!94Ahp$Gg*eJr4IhZPAVPC+f(3El#Fc4(WF-oO_%8gZPPg zU-Hf!e=d7_`ia)pWDU-2rQhFnJhpTobgS7D-@PX?+!BAB?RYqkt(6=OYmOzt&&|px z@>DB1ZG87<@9Iy?Wz>BxaLVuUr%TKebltFBly0NDxELlW;^}4NwN8-FaFROeTa--U z^5XnMT~Mc$Sm0$19pv19wIk3oIOEix6IlDK~H zx5H@`u_z1T!f@u!L-|(3BEIsLaLj>%XxD%a`=^Aic(uV)xa+oV6Myh2IP5`OLLzGB zEM<3tN;+l6wmE`1Taxys#pinyVS&kaFT%pKqx{FYUXiyxd(~l&ots0cX4t}Kv2bo; z<&oG;x9XGHe!vaeueAZ&rH9BP6t znGNiKtC0Pc!}bdM#{Gsx{pb91idQ*HzNf<%a28}q)Lyd}6f>EOdK;^~BgXA9Wk-0I#&10i|{o!QI z9B=zE=56-lHB|^G!|!6Ab+13h5wxA)q--%R<^?As_TeY>ukh{Ee~+sjo`6LNLyV78 zq)vgx>x2kdqprsjU2EZvK zkM`5d!WbfJxv-Zf(^^L7DXkDzKK^&2%Bta=IiC~@=#%i?L8@vXu`v3>R@62@O1i_H zU0b`0bQimCpYbx;%}K))Cu5(RsuxB&LGI96wLoWiE@qz^PK=hP>8@~k)+C-7m3fh~ zTb{cl0Cc-BAEu(WV9MW4xGHPuouoHpy8-}O+BGt@OjG7c2 z?02~|YveET@@GpI3R$B3_s2hQ;X!^(TPUCLH?j5WE^Pm?#VAysHq3>`AZ*`G4#p*N z!Jtn0NN6&>Wr_Q?1&_aY!m?B;!AUeLg=JeXZoHhIVY#CMPZRD6c-gicdD|yYRe9xe zJ7hgmdbKD^WP83b84Ky~5sqdC9gf0X&%G7S18P;ub$UF>8x$u|U2TAu2Gs;T7wV=) zn|j-J6<9qh(a11h;#1hvn-hh76^h!EI)a#K{7+Plq_n@|^*CPFNf)mo#o_UHR5u!N z6tDYp%62;k03);lsh)Sof{*TGQ~NPK-T!nMO?tf#hS^rO&A^x0VIvV|8rmJT>h-u_ zuU-mAf5{{G!Slr_txT-{zO7L$oTOcO&;RX{FIvIlaZJFT|Fb8xgT7f%c#sd=!f9RV zUcf8@JewHPok6i7aMc5ujl`Ttv@&oiEJb@v#P=+p>Ria0Xh+VQ^6|p)abp{T_m^0+ zyn;GK`SOJY0D?FV;ra(hK^TI|h*^yA1`v@3U~Sr6dIpy`xI}$iA%^B8rJmxvKsn9Zm!H+5i_5@}$ z$}^SF#n_XRIOd2{`j4Zw->LD$NI!N&5}0RG{f4V3>;w2qaDj+o1L&c=96?kZ+Yf(4 zAG$|`P8C&Z=ijE!!^I}HEB6N;ZOK?6KXj}P+b-Q*P}wLqKrpuJ=a|p|YP>M+176ORx4+W|vAmtaTeG-$OrFju!D(e3sUJ zTXNhc)<>XuBIm*$JGN#xaU_E48Q?K*LG%Q0v?YA=zyv`33ryCKG7$t=tklP)<0;Fj zUD$KN&3lM|tB((dk9UhzhwZn`KZ0%@!X4d_cuh2_gb~6VpE{>?}rf5f7k3_CH&TV(%49L3;z2c^r~r z8~Ov-IC_vUffk)tlU~0qv%G%1B1rV;#^m)BP$aXW@jO1B%(;&&2^7kW`jsCn@%l3v z6&E)a`DxsTC*VRDbYuuA5NeE_3{w8mu)votXc<(`lm5yoRwK-iBIDvrSHUluG0%f7 z`BIplgdVq(H!OBpN^~sSmGYPTs)6)wr%yCWf}_FsYba?uHZWA{H@Oam6-7nU2C1mj za-s|aBeG(t6$DMcm>IBwaxvb6`+{~lil?CU!{qfzgR07cGGwFWXLe;WgJJ-1bZFD4 zhBjtN1LC&j^PvMpgoq?uq2IeV*Lo{ThWtDnfsGgzWFpH50h;tyS=6C}9&Zm?Mp+wI z0aTp*3J5r>g^EE$$i;GoAK_Z4?+2}?$^q2N=M^6Fn*7bx{J=c*Zd3^*;n36&)G<0h1co$Fz(aQQ8hoSRdz_SvQRpLUSK}=mS zN!QP|swbIxbwC|R{+;aZLm-g-)MM+^wp8I}j2RsnUC205)jp=VfUF2GaYP{)8<`D) zCBsL6`vO}D<>H$;=`p(BU2awiatRqdh~Cg-gVzvxvMSYmOEL@$@>ZvWA=-ZveY{qg zr8gez*+>EG_s9erqHJIji{uNihZI1UQmsC^EZToH5TJmcGbU(P7aY9nA-Ns~evUC` zx;xLg7fq7g_I_s@nJykU<<&>9ug;E9Z}u*D1Ish$UeS$ZUQFq&*k20o7mKb8bdY?} z==zG(D~~;-u*75zY;O=e4T7+UXJA1wLw)0Ehuqo)D0W|C-rNb5)vZjuz%%o&2;zvi zZeNi}z2<0P(_S4Z5N z>1~RPcYjEeG|SSU_q*lgNq3sj4-va1jc=^^KJWh!taQg?@^W|Mz5zYU0wBg0)!rT7 z)==~fd}Peh8q&2WDX$B-?#lb268)as%n-V-f=&J{5G*A;S^^+{FmAVc7SDqy9RvA+ z6EBzRB(R{><|Qq`?jacbmDtmI#wfs#0V`30EznR%oLDq?Qdw~L75R6cn*|xn518@% zE7iQ5KXL1+&x;;F{-N^KF&9B-DXQmU+{BpGH#=o5MKt_8YFKTUBHU9H#f*1URz#!` z8p{0N(STV~iI;t}F^$~}lE^e-B;d#b`TerxcOqcoa}b@J=8USe6&LgMuP~!J`=ZKy zPUc+#g~1#TS<5JtWHFzdy>jWbz3t+x zWZF%j34LC{8aLafoIl&j3%_<5MbJbQK;)(OWyulF9MJzYA1bF&7e_DopXm1mCd&Zw zU_KRW$|!_<)+;euC{A+ zlq$f&IH6}8jev3k|80<&w895O9wnGaL|=|rr+fHKoGqU)AsW)UxJ?OmbVB4{=!@0K zpi=s%#UBhIxfxc5ok)2IL0!cmLK3%dZ6~p{3O#X;;dzX z*ruR?J!@@&+|4%e3JT01876AuCa&Z*QI@RGR$~@5Wn4ntM9uT+G(T3yn|G!1BwnR&f~IX`qPVd#wkv%L&n`~F!c$~-yN*@c+?#t@6%w+Z2CHmVl-LFYCx@V2!kR0pIYaFo;ZxE8; zn{@@z7wamQ8dkl_6?t;da(H1PclC0WynjtT1#fGK>m{HTnANwJ&)+GA<1F@2ax?x0 zl7e$!^#X!!MLwntxq~aW7Qbhqnuj*4%|0n~+N57V#fi(s46IR$2zEkNLyw@Yec3+8 zL4k=mj3xl44+@2Gq-%05MsS5;c7HrtVTwa~YswqKT1<&*;%h{m9A8C({#F1zoKj4} z4We6Pe+8+keW>`YA1im2%T6rn@TyJW>*5@@~QIAp4JOiJuHa4@k+CYtO#{u|%$5FJA_R zr+vCLc2bX?a}LVs|GIJkQ_8IQWZ%3lsXW*tf!gP&aXq9FM;%`;Dwut*v>CDaCh}gJ z)aSb=mV!PyP}Cp&?8`0Ln-AVpuY`p?VTVw}UaC5REB>4fssjWPxrNxSsp5_kYqBEp zK5Y6(Qv@n*;@FDFaiYo}{(s#p@0xgBWl}3%aZldyxST1!}~{*lGkliBxl*SkiVUni^|P|vWg__UK@UzDlU<-UF~ zklc+Hqy!_D)f;%gnX{xZ)QAj^ zYNq(4lk@Ix+3nTLbh@l9Ob)}J(8<-T{sMFPI6-HnGOvs7r$!VDnvTLGUi1s>Di7R+ zu{J4lk=XQ^aETkbP_Tc67Tet{xuvP9!*Y={g$pEja}U{8#>RKa&jRD$z0a9FC}VlS z&(W(dQ^Gcwa#A+NmxI~A`|3&?u5EO+eWRktWMy2K76cq8qg*?;p671ps2^DWF1BdubgyCR1E7Irr!I zjzLK9kDCYUvgaSAdW0n&);nE2Zexvn7EqrN%!D!4piq^Wbsw+ztY_O(r>9ieFR9yY zppYeLpfYr74Rc)}h;N06D9&j959!PUw%}2YpZzJztTU#frd#^a_KelntaBj#Gs@#W~XE zO4W=Sg|Bk*%kSiSsL|Hw4&^NvGX=H@-oIoLFc5xoiLE2!HR!R#pD^&1tgv`L)MxELs7v!*UJSli-N0h-23zl=)G zkX%~+_^eXMRnl zLQAB0S$2Q-TM=74|&H;dannE3IR@dv&MvckeKV9Gk_>l||L+=@j0U!T5St zJ1M&M`6{kYK4s9ESZ;3Ha`@RYxY&asCL+g4bcN=5H+fKYH`i#Zd1VKW-nW*KjbVc= zVJF!0Q@)(~zEJhIsjuBEG?a;I$p=KDKV1QG7A&L4LQ^D&Cw?-4KUk1(&*#S|n;D|E zi$;ns8i%~H`!ch06l=C4@(J1Z{+2~iyWl1Isy^fE&Q|prf>nJh(oc&L#;m7j68&gM zZv93FSpHVc8M5LkuWG3%y2EbJ}wXT1t25Stv61_9p?0zX#jY-h?vZ7#_js{CqEhir&270^MtpP?Pdl<4v4mpsI* zc@nyggiCpmm@80BFm)szFZ)nhpl`BOEjo1c|EPIAqQ1eKa@%HbZ7DG*R~b^p4N6=tl~2WA3Hx>%t*S4hXIZT|A3! z8dIL%(J#ycm-s0Lv6q~(*zc#=7Am95xM@rHl`Ey>S!6E|X^G-3(hXZnMcbv}=%hYDp zXcd~*JI2SCicCt}+EGwxX=;AuZx0T8d`M;F#(6XIQ|*{nw)@08@+jy`VP6x~-)e5j z2er-p;l$#lHwzVd?&|uGr_^B&d112WF%#1dl5)ls!L>?){qf#`;!>75M(Y4V8*je^ z@69eXW#XTL-Mm$aR{?^i?)QFHf%Wr5UfZ*}q}Rz@n^{nTsL ztI2w+@ZG}{39fyfrN3CxDvG&0J?RYQd%?Th{XG0#LZF9JK(XCd3TNov-o&ke0o%8+ ztw{;Ewlt(1Tgf~QT2x$2o4_yddGEk`R-{1AWgLYo=rsen2k6g28VZSe@{fSeF`I=y zleM0vDfTj1?tOCagL-j7-$Nwx5t1w6qD$#x8qvJ}BV+QUrrN~=pzQatG?THykm1Cg z_NEVc1Rv@PU*IJAkEVY;M{izF6%-uY@Vuf5P0fbZ!#qNEIJHjYxG!`^O-in@c>FB2$_9eLZXdzy&LMJ{+9LWh zIC5$?R#$wltvMO$zSS@PDjmPZIHW<5YL))&9~-)UHZ`y;7vA<;A@hDrUnd7kT#jH= zFQMV};mMyf!h&5U8^ty>zp2^942j_KiTZRe-I$4cWkOgo_Tg{&?cfU^_WgWf8!$B( zuG1pU`KsqF=B%zJpgd-tm&fW>I=Nlb$o8vI+uh<4(=|npx8^Vq*TYTz!)$m@9ZOBM znX^w9G?n@LdD;#oRb4#bb=2qKh$6pKD&528v-iELW$~ghT`N^X8YtG<8{kV#=({sX z@4}PbxItl9A-d*w_64ugq2)B;S*=hzM{9!JPcIdRXuen7_8j5;IV?L{4``oIVMDuS zt|*_hIdfK_mlsL_!74Xt*ViY`iH$Uh6d3xjdsb>8mOtGSWB47CrkQd^v<+r0boPf@B7ZDC2*l z+r|+W_Pk=broH;JY?``BlWH>Qk6w$i``VQZTecxzO(xTaNv-et@W37W7JH8>rG^PU zrodqC7Z<-&o7)!mGf2Dw`P>G-Qm503B;maKmtTyhf$QDm;`9b?1?p<#>M|<7 z?68xY9>VY9E*u^99DMNK6jla<6HyO3A{Ixg)+OkPN4WANswa&m2-jE!!UL(QN2R^v zJ_iUtgbH6z(Ykjljn2V2YwN|sDt{gfI}ZJ-LQX0q(RXvi-Mq*d;_oI%L~-Ca=-=ZW z%6n*Rs8|>GUHH!Dv@e=wF=RWvvM&azQE>yE5`9|^(mM1M6e9LpPe&TIHxg=)%}gPh&oq4X|BWmx352)hSxzfOu|R#RT}T0W`!fv0 zw{p{8-+o=J^gkyP+(`LT$6ekB1L&gigY;@Y71`Sjr2x7EqEihx1J>_BG*eedVsdq1TP(wQ3%a=XDoe%I9r8?NT@{ zSqF+lIp(6FGQ_+o>DuOKMT6jQl4#@ecpFm(=d<6zx!_qh zb%|_Zn#_~uR6nfGOQ;4e+=%~GdfYIl#9BCdLr9k?wCa}xpHAeeAQ&Bp|P1v`|sEj1K?4fp$Ich^il|%%UXEl z^YvOr7Keh@#Jf}68$*uD#l&H~%EN@}24zyFHnnn^Hm}HZXm=|wc3xpy!XMwtiMXs` z>Imq1<}RnF8iJ!#fL1)t!&0fm@>5`u2rZP=F@)Zww0xZbUB_u776&~u z*w3da}&Nz|!KNjL6d==)>ypfNQl>esN(sv`DzJa!N z5I>fHmyLlTT>bh0dxdwDl$2qvr*wjH^{RKW&!=nYV`{g)pN*1{yq2Dg`}`$IRw^myCzG4u(T3zx5N__?ddo>F5 zoK;YhWbz@uV2|WNCN!d+>XR04#+Mz$NC2Ah@DgnC88CGyldQjdtZP(|A8bpMl082T zd488bU+3)6AKYXTS(Iqtx8PatmfLpxnNw@Bd9k2+oOWvlL-iGDWm>%h8hZ5NKTbzD z4*WgJOxf<;;dRL~N|CZ2$S`lP{_;Jk&)zX|!=b;=4Gn|!!^zCd8#J%Nv*P$Ajfsv; ze$40MvAn#z$uKu~052Zu$n2&v4sS0w%x8k~41D!9MDwf^dLEu&ru{rOdddC)G%>A&Pd)(OTkFIO%sB=2xYkZ~rLG)TWLON_YJMNA7>NY`~I&2hP@W|D#|8 z1UUog@@O#F08P+QpP!oXZ&)R#yWPgi2HD!LKH9f`|6#~X@#iK~KyRwGlawq4nR8Ow z-0%E(C5gBnJX-a6S2W2lIN@Dw1ZU1K;6jVVG}{X6UP_-YnV!GHD{vY&(rhz?o`O#+ zj*UNkFV!esNYXO2?s&XEeC0LKVfPJ&b`kB-+Wl}U`Z#$>jT~@2XtejGcBID9|1|v~ zA^Wb!(DN6vAY8LsPqQhc;C0Zj=+Mr&In2AV=S5#ry;eSWI7AZinOYs<93qbx3Mt2` zb#biOM!7gksGNJVG8hr>7~J9Xnb=_P^#4-MKf;qf8CrRsSn%Qnh{@(%7F zinA!2Pjz=x{`0>8HghBt2`bw;2X{{A!#{r(C=@uXD|V+zu-bAf;g{n!+?7?UJWQ;yw0C+hODY6o`pUv z(K{yw0FjYzwJr9;Wx(TXKez{R^85cODH@y{N_#b!74YD7d-(X^$Uw6e{)Q%1_NMfq zGnrTi(Bri=BZRRuaZr$(#ymDYr)@hr;;EeDki=qBNSzL(kNzL^p(`+$8-t>@Ye|fa zkI3c=Rc>@UkW|<-VLoSmX2sXf>?UY|!xW^sFJi6cmwAVsi1K@t79CYfJ-akMPF)2%kn@g$GnPE7xv|K*+4UI_Owz@wMq;e$;>F6*v}Y2%S#@-rVa3t8toH zbEhFcs_Q}tda1Cr) z&U)Qu)B7;AJb~AbHr8aGSpNxQ2#tO+`DQ$gg5XF+gBM~OT%k68~MnKMs|6R;`zvfmzr*Y3Jp4l&uGnMv8k$g zWfTxEA#ANc(c49HC^FBOx+g62u?RZbsjkPV7YtMHo=mZD4nZo;P9Av9}HX321m; z?{`iC)x>oG>L9Gk43)&7D9$voAR98@wPFmQ?(oIF2e1p}7|@d9EWw80QvPjm8t~yv z4w)GTtRog=&d_=oiQ4h9Z4P>;!9UQH_zjzp&si2{j2KtO7?6T|jep zG@I04vRJf1oA<5a6cCiT@WLk;L8<>hLbBN)_+u`mO^7mH{DVL^CNY&RV+bj8E`MRktBqKn&}-pDNG+DL zp}<3uq(5bMu>6GC{Cb?6ui$Td@ z+KVYADw?Z9?Bk=2PiSJ7a+IMwI4()lp&v;y04K=dp{&Q`49JW_)7^02&uaR%Fwg(& z^lr^F?&X#o62sOrl^}3Qd}6Cr=!z;&WigAujy8bMhS~lj8tw`PGd#x7KHl}TcHSlc zWg5&&fI55z4OANFP{H(it|D<+chqg`s-|MTi~Ct0O@kT?JO<4KPnqPqfFgyz z`W)XAvIw-T%GfgwnWw6zV7DS2WWjM<-Wb|Xj9b<{Dp34I!9>7_pa`2xv#=CZJ)jO^f}Q$mwA}@*Ly_HU{iV z*&+LL@m>y-$>T&6!^0xD(4fqf=~jUCmdfI^@{nY{6}sF5jmyXx)O~M z)bgoNO(O2Sj{l$uz*w3JTn+v6q4laoeybwf2ajf`$s|SDOKO4=9fTpo-j0I+f#td( z0^@`QMeqskX}zjt?34qq)E zE-Yw5gv3Yd6T5CzJdFfiOG#208IU7d!pu;^ofV94i~B%^Xi{E51d+@`ZL|2kdgai^ zd(lK5{~7Qpe*%y!r;)l!RFa5`G}yB+tja8CALY|1287isUvDWL5%y*$zf=jur72Fc zgjIqA65w6*RUc+#&44iaH0A>#RIfVYV=ofZ9*wtl zodT2wFPTG0-DDpv{Mm{jFF~c}vV>^^v+Z(z8B;xxw?YYcXvgSreDDWw~&j%To`*Y2!XWh8J`UG|OIOAKNLBola5tbJk33h&0C;rqG{-~E>L>>k+@=ie#h0q|2(vC%nw zyufpq?|E#tBNl5)rbKk_%E^X~V?K-G% z359-_Lo{7YX=Kk8%#y2l?On7F$9lFeW(yJxJ=GFuuC)i7Qu ztHPLZ@(QxZZS~hwEo1oqU)nyq&8b>&cth@B>DU>*E0kZfMIhH?NqQ+4q0t~iB=Xfa znL$ewB4EEipb~)d+0B4LMD^qyznmwj@>}8`n9cTUIK^;NI>>5bG2ck-l9Y~(#r5W$ z5ZUI+eaWFsTvI1SvV=!>S7b2&lMQ5fe$;d0KO+;o-S9=Bf*~N))u%jULk~_8WsbRj z=-_w1{eNWuI$&F6mdp*|NvoJ!ZLj(OBMGHNds0TVk?8Sq?m9{n+J2i0vdL4W7z=S= zu@c10)k%j`y+RL4GJeMK@`lzk$gay!rCn$kM#M_lPHQM2O9+f*B@jTz=mDv=)HxmG zym`flht2_*9Q%Fb7^t~KQEh};!C;7oqfr9<#!v4pCFn>&k2mOVj#$1)4d4+qp24J! zu7e}o69dBWY}cNECWn54y`RZ8#P`aZn6JMccT!H)FmWkMJ1piImIi6|N(0$Di&24Z z_%_9E>`3KH{LjXGwlSSXBE%E2OAMEOYu*VHl&hZKx#t+#pS9^RZ~PEMAx-Ea zC5|{FyG7hII-3jkBx;B|+h3E1q)NXSIi!_IOq}y$Hy2%@A5_EPoN+Ohbm$kE%)>Q+ zZ^YBOb3S$ppSa|A&5d253zABAIaXSu@ZDGHIrK8xOt63p9yoB?()F zjtX_{v~Qjk@Xj3nuGBJT@dANcwhnU1}bTI{$m(>9!r zH{=nuG1t=NoqgJH$Iq{%Pq4l_aBgB?`+1?HhOOva>OyBm!+5gfTp{8{kB9=W13P)d z@;BD?fd4F0!|*YDZY77j0SC$(&rN^qQ-E>Pf}QO<=(=jF!F;M-wmG;wM^hnPYg|0c zk-l|B`psUOg~Q@3(;(i@3Uwi8p;+)V6drY+_(|6weL6Zus3ulle83X_Ck_sGor6)s z9abvpk$W7J2l6Mo);!zpV|DFGo($h!0mR@AhrY^NP%C%Zeg;1x<&;!UQ~qh_*Z1{p zC?@E4*QNbkZXGmCcO9{oj(8-B z{(8v6J}pP%EzP;~r2P1i6M3dKGaHE|D%R!ugMS^x2<3Kcj_}$fN|#=j*Ay@7>iV}A zxlSe|4vU3Pul1Nz{6`lM)2Bb=Vpx*N3(GP_5?-FpyEoJVEB(?=Vr1g&r})?u^L}nN z^k1jMK<~03m}Z$fik@=WuCVF2)a&yL?hvRV8q1?w{W%ElEWo@>Y5X?Yi#($kKn*-m zV7>U}@CmH*eIMKDLJ;|j-EDA~`W=2R_+5=I4C#dct4N=PiITW&&_H? z#0emB7P5olo7q6@@4%|I<`1MFXf{1a5dk%LvmOA`Dngb!l8w{YWmGo}Vw zouAg8=PP;C*FhR54AK=PjU&pPd=l2t1d16SRImRgB< zhWVomlUc|VkQ4+f;xf*g<3zekZTQjHe{LZQA3&8_0(AYigI;}D9xi*g*Z?%tC z08-(}!($lR+E5t$naq$@@5_taM(|}Dc1+~c%PxCc4HW#?@q@aN&Jb=aA(OD8lF{)I zx>2bWdVlqT(^B+NTuV4uqmB;DT^ylQhCO=Fvg)*Z<&|DrjAsg)oko+kwq`-Tb|in9 zSKX79%Uy++ayi9RO9{Wn&;zw4-9TfM$x9&C7Ree#)Dba!I!%2+h1jljutFSVBlLW_ zZ(gxFt9b;@4ovw|e8FXLpPtXRdM-7V z&gjDk8VOXydMZ)yCEss|HDDxkD+_fP?Dkc>n^B1^;BaQ7e*JABKu;PMS^vyW#I2-{ zyhv;7WqXkNSC4-6ypLm?yG-<=ff_pVC!EZ&fcm&I`3Ve2(@uc~E#z8c+^05N^nNT~ zhVL~B_OEM23af~D)0FR!)NuSD(^JTx$IVg0^I=|#)!`v6TKv}V z#&MB>L5-%%Ns2{&UI6a41RB(-Ly33=5-Gq zKv}_;!r(T)QIdQo5A$?W-aXzQxiZfT8pc9XC5v}9OP7SFvK9W){WS@P+f8E8`Uf9f zGw^gz;*$DJ4_XPz!r6pmmo?XzhVD;R-SkyyU_I-s?Go>M^ z^t+l#EOkzd`yh+9#sNS+Gg#Ej^=X1G=mL2h2(}e<4t^%qYlpiAuvLnp@TB9t_vU-* z1#{U%b!S}muIxb^tKC0h^}L%v9ByY`AOxzkHYscAB=AX_jGwkaV_^x{p zIZ+CY(!8-}1VEMv9))=yi0b8Yi1v~+&aSvgiWPF~bnF`&g1{UFufT+)(>G7x zeO0(outiPo<5m5e}w%v4X`BrM&&$_Iq6|BD5|4SxR-Eg|V3Rr+ZIZq9VGqW~yQdLIayw6Zy zQPZ{n2=8IBYj4nhmR|2qs=>oO6fJ8vtwZ7qjmcWR;)NKEbqkRjDp{9Qv4BwB$qq|J zBF2tHvJtPcr`Xui+rOLqE=T6J(eT_Zdol%g=O7~(4TydIR9|qdkwgo18#z>jK;}aQ z@J)P!e);_?2a_4WOOFB1huy008-({3jmHh1;#TL4TCHs1@<}oiA#3w!y3R_ao@`#V zK1PSsK|`zBY$~2kt7M;hkC&7RNJY%0hB1*MgjKw-P%a8`LMl{!di&cQpKCqf)z!2< zr!K3&W~u!3)v$ipyc$0XGH<4aR#JpQbzAp;4a^T8M1!02H{~?YGUL;x_}Mqqb!_89 z#HY;ch| zayF1Sn;-U8;}Y6?_-dJ-dZZnVrlPB3U_x)Z#$nf|nQ@iB&@tWN>VP4v6S^0UR@Wmg z*Y>d&D~ z2nHrE$zRboHjQ903mRVVuB3t^$BtSRAqwHCj}8$=|D%u@-lGny-c+iVO}4y}e~^R2 z1x-V_5qypbtwDa-of}YXa5sd~!>4gHG%cl|GeySKjL6_H4k6i`1p^sY5IV$%on-$4 zdg1++?(x6A|M#pgz@=tm<3e^-!+j#;RQL)U{RA5uhZ%+Ab37JD@tG9`MTE4QG1cdq ziy!GL0DoV-FvsOT1)(8@P;P1Y6dQt=^S~txiL?56BOxlREJiA|W_){A9u5TL%q#;N zCQafB_fZFyG6LO@5FYIgx&_3){{b=?%%9YM^Y(IwV%Vw7UTP1W4eF%94pJR%9C-x0 zpj){92hLY}2IfdxY3-=jGa|V~V1X5v;M&5yw{i@*i4~7r_Fa7-nfrcVj)rn3G9bFb z?c7nv@H4*|5N2q;4e%;M2DccIV3_$dn;!y;VuFN zrqTZ!|Ea-}fAmqlqmmH8u1?<+M3AuBE>u6Bc|?Xa5@#llh6EiQ|B~HwDE;W-?V=YV z!6N5e@B!EkT>4@8uv#acqqD5%eAna!}ZHjt&hF0IDs{Qqqe6aAmz=jQ0t9GUpz^b@9ZzL$2HX?N5(1A(IwsLHcC?zUwabA0 zROV!qu4c}$9RLcezni9c03Yb-K|zcw$=fj5)Y5xz`t5#5K=-fxkKJU~y)+AeFEeZN zeDCNwrzEj-SdwG+K21=t<@0&|#sdtbPZL$-9fdE}&V3eNuS<@zMBn}G^5g)FSb*zC zX&kM;2c>QQXsHy=!3fN|+_aVOzb_6j#OCIIRdB`y@7uf+dmxaPHawsHnIBg*x}3f@ zqMDorATZRv(-DGS8`;$x0N#P3waj=60Ap5N3Bk+Yz7l)-z0Ys)e(TP19K@C99p33D zdad;afV6eMDD(FS*w4*CqUT(UV4wo)l^O&_9_GNbE%1GYdJ9ZS+7Wq_6Pit?$v5bW+{3n?@nX@%CP!w$zEhJOn#^D>R~t@ zSGBCX`&CyU8_@wgM8~-=;FxQ%u4!K(^!X#S3ecNvM-yolmX<6NDCOz_WB!?GUZnqvv;fvV7GPEN5o)Ub2hNHM5iF1tid@JLXL>uaz+RQ(T(flE8^16o zP6gc6?3{3$!(q)}EFXRWq)e3k3$=F&wk-gFZhmL}nv;?do{qK;WcxgV(Bh#LhTOfd zrio*xfXVZrIr2F2&LHR`eenD2{1PCFyVb{zqtAsq!JSAP2oXz)pkUSg9~=%5wPz$& z3Be-khTHS{m&B(y*1)PQDUwfW^!7l!N}WZc2g*);5Tn_sk( zW(Z1V1r)85&_J|;{|;;?Ai0?S6vOpDb&n?WKf1~-U`*6|MKGuX%v&QBu;>5%I?f;OP6`Ogxc9(3j`fnH3rB z!-Us()e4s7>@!q4VP3I`a5i;!HIEmm zq4%$3v@5Zloo&O>%9nZRq%w6MpQ^zX_Un52F>UKnsEQez*tPVmvWCtH$|xdh=XDPi zI#uZk!5dYgX1k&(4zq$Qm$o_;3?Ubdx?i!MEf-+c_KVE!Xqv9EM@1K94n+kk)pVTO zY*l{(e6Wu(%y?-3Zhou5g7FdVIT##UC70ny(({{K83}ov1#+f_66dFHz;4jkkL+0z zQwWTKeYPmWQ}jK07xdh0c>s2V?U7Z&ZfDReq5R1jcU5<*tbk$v};iC z+TOv#bu8ymOzySInc{XKyU)2~$;GFU1DxbnA!=&f6B zJTA1@0NC=n@Cy0lPB<5)K{;)a7v22160h59^=j6=DL58J(hi;*CSK|nYf zcafQ&{O_Qw1cN1pbT%6Kg8VTH(g>9)ocEwsN_dH_VdstKuHyEc!E)BQFTV=(icXK; zHr^84x6z;0D7x#s@0Q1!SDc)m08X}vZ`-Q}6@c7inb~sA;aR<1w>DVyYLGj5n|#am zeuKeYVq|>%F>hsc;Ig1U^_}mGZw9te#^jfIg=KX}^`t9fLz7t?J4aMq$YY(lPJz-V z0<}x`(iVtYf#GZ)s}MZ^nBY3Kc87b#&a?qR9^6E2I^Yohi~dGf1KV~%-BQ_VrH5HF zieX!k!gB&$h?z+<&bW}QM}chl?!Cx3$7SNR0=UOShjNkcNV^_XR7qj*q>j+DOkUNd zG3`7wgsX{3fj4NL7nAeELm|G$^pn+J%fTunD%@j2b#+qC44*$gd?e3-2vJ8ThLzm= z6Vp=}JwEph7&4FT=dSGAY<}wta-4x?^Yp;S1?=OIf>ANx53=cOLItc?tr&$NOAjN^-+@yqGa`tQut&YVaS58Ma)~!Q{>e}VBbl{{IoT2`d zwd*7Zff&;mmd*+4zZQU81E<-pefk_R|97z9fOBgdxDXzu!RcbBGe}DHaKnPBBgDY~ zCtiAJ-jDvlXk5Cy$*+^A>yGD%w;vL~k)zNzZhha}x~4|_3dpaR$ANk%+j~z7kX>Rt zdn7bMBkZ>fn@(Fc$#HR89a%h(s4i}2D4OlCrw8O^O(5`8b~Ot*bxNCkS83B#A_ga0 zgo*p^iWc!@w(1O}S29}5zS)XRJRw>MD7&;P^&kt!j&?Cfm=DRNkfcvWbW)o12KGpd z5_1$voTH>e$-IDDJ*=;v(#yZ=#kxMgyJL2#>jamwbP~iP|GZyrN?58E(<#FDAXcRB z6^)Ozs;%R-WLhH5mP@f@RbhKNfNjt;#`soncV8l(WpnO*PUHH!j!G)b?Qt09hin_f zPQb%*8@ikvQd`RyJ7S?xP!LyGBj)Ks@)Orw7_6aFDrN<$GqV}*1p349N^xn8qV%FhK%6_jWdE?2ozn4tUt1g!^%bgK zk(OIP!oIz{znp@)eZHNFW-?6cpngy6XlsmDky9KGN^e#W7UiC9_@hvs_Fkm;s{IDU zZhLJ`LZFRPu@z8xVy|6_l`3mT=Ua;LkOX(L!KJrL%_>@akNoUCL8@XiF)`O7{wG&E z9VVehB-Ev4VdbY(G+uof8qz!xKf-RlSk(*{8&|KN4Jr$UL^AgGgqs`M!a}hN@`oH! z!o;EE(84syp#o}#W)Dnuy7d2DshB!oi805-#4os5r4mO!!^=}7eDYs?EmAa8Eo?-T zE_1F|Y?aRsjtiImW7-S#t%<$G)O}&3#qbQD(i~lD`mfA5{9nxI#ztl*n>kn+Zf^63 zW|?EZK+`PBh;$x>SrIyt$>{CRq&Ss@TKio{vDjYNOpc0_Bv%%tZaFaO9j2I2+K5z; zn5i7-ci55=k{vWWcprTu6}(j0sQ=q*fsW5_Sj$PM*Il10QG#@>UxAS1>4A^X#rm#4 zYGz@5xj&8SD)vjgSlB^xxUBP4Cug1s-XG)8!3;Ukf3_{9x0)tAz`vX49F0?2L71^{ zH+^~tlV%KjwlcGlG@EA+GgOoV39azp1cdL9r$^jI{VXNn5>t=VVzQ>_LK^nGU5gRA zQy;_3Q)3aO?r3_2aT0Q{6MQY2J|^bT*;1u>fc*_O!vs#Gt;+r1UH{Vue55QkN!Wb$ z(yCda)0Sb(X|{En=OD_P(FSWoNlTDX7EA~?DSO%hh^fw^Og5Jq-hDh1fxRAGknW4A zZ|?JgD=-{&>Pv!r*`NB2I1V}Dg%%6RhM4`S8L=r6)*u{<+Zx?U4M&P8P%xHKA zvO>0zOm~VM<9Etc=}EvF#ni7)ELr0Rwj0!TX`jI98mE5LOj7ZuZu6FPhkM+xU56P- zgYXuNffumbd$Oy74~cOYSgp@uCI=KE>8&X#u_Ourrz54}MskR|8i^0_I-g!DTck$p z_xs@&Nj6PkG;sjEev0hv@onIReR03IP6`GvbTm(1Y2i~GoX4 zOm@kPU%!9TSjd$zT_XkJ41?!;SYw15zLk{_HHcKWC2~+C_1w{3dHM<5+r;`Q63*FNA>g z`EutdYUY?wZmgUW6cxe}O(dZIQ-qP+{=4v=Xd)|yz`wz%VIi5H1I2U$WucGIRX)~v zw6pu_Y4h}OXc`_x-m#bdHLm5NWYEP9UI**1s&j;_h~9y=wONacO0A>D(mRD81h)6Y zVI9MZeh+wfh3Gg;8}i(2w0c>-CHHpbWK88|`wr-ZI90-=BB)SouyWDnfXQI3Ij!r* ze6NzKkA_BtTjtk4K>=I{hKb%9vuVF@W=CPk&_5i|$DkPL{pQlR)r{K$%`$wL=?St6 zijrF#y-k(#xa?9{KNmIZyI0F1W}9oU(#_Im!1DMdM~f}|tNda*=W)*BD-qqOqkbo{ zyC7>$JIpBg^+yYhu~+4?ZFN^}WT1cs>R;lIKCguPFAy~%oyi@8Ha&@B?WfFZgkFDz zJa6W;jue@&!3o^FpsvM^lF9PBKFQ$Ku-IirTUXYZ)4M2@gr~Dj1dfRH4#t4$8Sv>~ zhP%}_hV9lwR=zw?&lQ+WTNF!+=IJ4)I(h%yQJ>es`lulD_@ja6q*!3X5~kbPVjp5H zme8Av*-em^7|%OIX_UxVe3(Dy9^QupZd-aD|4+=v32=c|%mlTSw^5DBChO`SgX}1l zC^MgJW05rZ@Zgg(JArvpVes%t;NiNo8s0s=sDJGCNogop5?bC@?sx5~<{s?uryko^ z>TA6~Vya<0G;ohSAhPIoT!qzU&4pt|R65#q3#Gczw&eUkg{9`lLOF-$Ew49Ox+z|) zNL~oTEs+ez@oI>P>AB_=S82iwF{?xsTQ)(uSiO8d$x0z*tbxfM{<6Vqxf{n0`O3UD z8ZJnEfVbYPk#(m3@#C^h0s{M}aaV>Zk>?3B&&?c}92pz?iU@{yG?Y~;H~|G#Vf-&> z$>*z|;Od7@Ng(}{hd?c4C}1pvoQon8CpM!PM_x!|AE8qAdl+fCu_AMTJnk7)GlNhd z7`SmtMULX{{q%Fz4P_#q0Smu>2g^wij#?;9x*_`&(a+zGerkL|EH+q2Jj#Z9jQ-V(|5wx+Nd*@F{ ziJXd6m*>?kXBDB=D&xGt=hw&MSSnns{Lk*xQDdHC3Ezh8M@ClyB7*YJam142ma!5X z6h!@Y8rPqJG^ec~Fk6xIYsl_U8+_-iy6TtqjTbvASP+i-XpQorBLlF1i!*>nnQf}5 z)bsA;Bw*`*6KdepXV_XToecHZ{E-M0j-cij`Pq8t1lL;cfG%R`6aQn{y)2k4nxSEC z*Q<%{2FjpEL*YkA(y?WY-wY)~o@efp{7=gYo}r~;w{XQZ{FlT1e3MTigO@nlaPm0y zO|&djlJ!n+p}mO$(lhUXWB%c5aMf~OG19p8mCt8KGv7>$WQhEwmpiU!=v=qMsUFAR zu0k*k)BVccYiLe!<$D`YTbkEbni$pwi6UMYUxxXX8lG=@WbX}X2a?#vs4@&eivW3H zM^(&%DjX@X5`V#T1cuh0cT@_eMD7`tA-v!xDPn;cdO7Naa4$gDh5|xJ_f9);ymZ5% zRxO9v|Lw1Q0VwG$o%bi{=P@=u{{@-^cUz%EXBCD38mKtC7(c~-6zHA>zIV^KOPk4! zK3_o!>(?(5kQ6y@w~8=&W~sWbSD`XL9Q`n8`1IzL(A3CKFNVOb&=JBNO~ATd-TsP4 z&rNrOZz?pNgS)QGKe_!NE@<-@5$SSztEt4mJ&AH##DCW#YW~j2F8nZ=-MANOB0(5hi!r{iWsN^hZ+m(M~gEo0r z0gZsa5tayE6B=l73(BV{ml1TVz1bX*=F`!8%ClpWZL@X7&}$ zJ*cfgfV)z!qnu=2V^^TsMR58~_aijg<4osC5o5TSN?oPZ$hIQ7@rd%bmyTQZP!4zz zgN~!V={SYG?&jk@t{%#eT7{DIL!cdcGKif#fV=iY9O925c0f8K@oP5=ip1;9c#(SF2)-ry*jrbRMoZc0(B;u_|u zgxVSFV8Tr=JVvr@HCS?8i1xJKuen++KNGPG1=l}wuklQh5b!t;Lz7`Kv7I;1NIUm@ zELX0)Yada0)U^_Zf$bz;(BqV8)Z`7y=8vf;Cx<-2ctlPFbv66yy#FKy#6pJ?%t^@o z4o%JxSp{w;f)UlXGq>(`eCKgyHOqQw)J*Z?tN>SX@r_E{w3-VRyl8oR22lxKtb^QG~dtBkp68yjU8gmrlEo07x zD=y#mGz+6TW?+s&m@eA186FM8h71w?zN1D!Kn29lg*de_7GS;t)ou-9g z)LN-gjz6hGEjDD|RQePJIpy1J*VQY^-dmSmp#aE4JcUlqf!f!lbE%^^6#lg z0s&T|G5n)7J@Pi=78CmCkjLdg*0YD|FScf44Jt-pUv_C(VuJ3^;|zu_`Pdw|IUSX6pwZA3&| zh{yJ>GigI0p6g;BvF6;sp%-cw01LXSA$!dd!TnW5R;OxYog#r)O!?qn^_h1wKa!#R zd#t+RCIQ^o%BZ$V2NmxY+ApsxbJGb`cUrBP$*-ea$!naoF{{_tqiA9MPhw!``gUVM5>AXed;osJSTpHZxLV zLU8mx$pB15UaYsW-=t5f2tA92B1&@44}MmBXjVX7EQ_2My;#}I-Y~iznLgMVe^e~q z{q#b`Aq-ltA$iai7`XV@>xK8N$|ize6Dzdo{Ar^*7f2)IGpV^dtr*BN$7HHN+0Fa8 z$28F|ecZ2&U`^JDW64}Aj`%Kn-UW;(^i@g+-h+QZb%(KmpeT&)WTQv~5^{oRto_XSv$k;A6-f8Z{C-;yHCOvEjI}ADHZiY+*&SOM)%)uO}%1 zS%zK!k65WCipmQG=%O!iJWzaup+RJ|YaymQhREIukjr-1YXNloYA}iq{UZQ8R`agh zeeGCZBkAL(4R0XPuzWH>%SV3~qU1B0u(s84zxZXzZih)D3EXAQ+jie&ZRj~6+1GL6 zFN$+!O0YYi+b?#DJc^iD8VGj zV%0RKZC(}d!qk+X$s!*=BY*yMRJY+1L(~1^e#XJW#_+r)U9|4hX{<*I*z*y41mWq0 z89qW+5GpI7^1Xausjj$>@wqODM2go3a@H0nes?GY`=eg}M*8p>I4Mr^s*7bp@cz&+ zg1>pa2kk`p5EdJ%J@lZ=G?fT)Vo2IUL4uUJzx z!=#G%UHS2X#IWXh?YGli`vg2GQfObBKC?L9uYAX^UVMMe#DKTgSipzFKl+#0^LA<3 zqcib%y3ZKXcdi9H-IKA}pGQ!W7#9dw^@Cs2_Vbq(jl52(WtiG|S^O*@#5?lNb?y zgi1rb2R8`JH2%x*@k0fP%_MnrS0gZiOMJJG>_Rbq6Ov1~_#>p~1`j$HCnLvC<_$PU zvi~OH4nzp)-N|n*GR=XZjOL1VUPRkhst^HYzM2Z~*qahfZva_YWpU{c;dK#dpv&;% zPkgy$4+R$s5tI3s2Rqq3HM~J9YxGJqRLTH7_nYbmj&xrF$OLaQt#NMhE2nOTEJ-hT zQD~Q_H(N{QX=6in9OBZT;`7SiZOdWHJl*eTR~K?`fMlBj^tU{d+zxay!Kxr7XWKeb z7m5x;z{EeCgOTMx&qDXJ`^EZhE4(ItxWVqYlg|wU&ThxuLj_+C?;#~jLJZ`KzuPtt zAavKePa+547xm1!j+jTN;OybNa9Eg-wk5TC6sE-#;#>zi%l??`{Npf*xCEAZIB{Hw z-@k~VZMp`Mvh`70H`p^Xkt3q2k02;-j?}7HK1|NNMrd^8Pp-Mq{#6IZ0UftggUl89 zS;;p;QgIM{j)qrq2{$3EEeJxCWw)i)l9FyTirq#>HsT}*wMwXEQ}A8plm?K~y0&z} zJ@2wUDI-BO+exnySebGfnO63o2Vgo<=0Wfv+FW4oz=|`E-e{uh2nNc9Vk9%{;?I5M zUF09|j#gygq(HkOz>MYiw{3YKqG~?k8AVMLsUjgLX^Eq;$uA-|G49YYkV87 z*L??>F07FVT*k#D0(j2To1O{?WUgERGQm=g zpZMVEeS*X4&VexfE+Ss%UkdH_U)qwFaqa0BBFxDJxh>^8bQ^~~&cs2Q_}>#JF6y&z z*I)J5g5-nqXhoEQ<;4zf%T`LTATlVSl~Z>Ql4JQ!8)qXxVejO0GayZ|dX?PsJX;V` zLx5sQxwXiJDGBwVc-N?r8DT-y(#8VGr*Hi!?=t9Tu$R+06qi30*O*h%{NJ_ga#QYx zzPU*t{#Az`hM6xBhvI?MZ~{v_9n!@nKpt|3$hVCYa5KM zpjf~Ged9OgFoY@oZ6$Ly*bQjP` zg=tX(J6S-LyNf-)D#H$+>`ZDT;}<%5U32w@w!ExhiXFfGcNN}WT_%hcU3 zWVlE=oT_XG-X=A~el(W1pGe2T>-(IGKZEVFHtv>8Vw`5AsOH_0LGKZQ61Et%pzg_A z>2FOsK2vTcTVEdMn6PT+ikLV=e}A>Ro2cQ*gihEx)cg_!fJ++B##w9OBsNPR~(){=^sUPHzhmrv2$6j8Twtf%j9+mYUMl}|SGFC)9>+}o3 zOOaF#acq|sTb|WHng&rWN?t+Q!B#}hQqz}u_UHE@4?i!KCys)b$ntPsB`i$h4C4!- zuB3~_Vj}M|-DVzYFCxvfJw=n=O?PnFBqD; zG}Ho^&whDIL4WLz4<3&r6QUZWe!bc<*E$;dDGIGV#`?s~!aT^+uOV>n+p&^RUi@(0 zUpUR7L47Ao{s5ATD+EVh5R`jM!(kpImLFyrUoN$h-3632cG2n`5_ZcNb^lmJ#L;@{ zT^6A#UYk#4>+#?Hm`d_ozseriQ^=%fiUGR~5?FP}W%Z0++V$0z7-q1~?G4~`+`gZ7 z!VYmePc#&jgQ+Pl}f;8>(`aqWUNX5JD!#>mZb4o@Ll<i1fTuabbE11!r)xZRC`l2Dhj|5uI ztk)}wC0PLqG18c4ESu|$=T;E}98_}15RZ_(r8%(GG@s!ZHfG5-`0}KfQ#ow4x%W~{ zj^J8uQBZ%*`l`U;mT%{FAZFcJd!%fH?7%Y~#TQ(mcx^)5@WW7vRBb34v1P>>0h9M? z`O+GX`HG{jakf7cxVD1o2e5SJs1x~Y1F>C;$FA3WY^SmhZn3lpyc`YZL)YJuoTl>} zp}p*x(*EN1XdX$2OEwt)c&Ojq)v@qpT|muItL`{DDo;9Z9;)7Dui~w}cm5p3#Fg04eQ|X;&F4nfaQ>+n`nHxoyNR^9$u2cH<$c9C8XD?BhWQMD8$nvz z`%Q4HR>G47utF5^Ls^!@PMnK$7GKV!DeUrOF^X^M;~v2&9PMWdC%J5(GF6soe?!Na zc_d*Nm{|BN@U%PyFSZR3?nRpf6>(T38)zv$cf0MlP&B!aeWjNT9P9~#>m;eAN`5!U zGBkMr9(4VE-qZYBpbq6(z}&#w@QgGup3Ki(q`?yKsXn_C_}p))xid}CqIj;ZI;km!DD?pvgX!rG3vu^} zFxq@}X@#HRCo90UA)yZ}I0Hhgf?w$2BK8&jZwHnkd9}_U=1p*So|bW?KJa|HSl^F zuSwQwXrYlHbP%Znok;%OJ} zx-Md*f4>VK_iDh|7mw}RbvH|kw6shUZ|&45aw@^=yp2%h=uSSpKDfBgM4ajP#rz)& zVCh6^=nXhZa4q`n4NAzl6ec3C8nvL#9Anzmz*`dGw%!0M0PQ=u;3&BvNB{{hE{14P zXJ2uDQcp1nI(y}C{?rirxX<@0V`D|Ebrvt?zBf|;{`Zi|8inE2{fKsjc8=ccj{&z-S z(O-}ZjPXPoYssamZPF=cA2G7dEpQ~;l%7;a*{nusGN%L1-VXfdBN3qV@;<-pJojzM zyy!N72hY*{+S z8KH0aT|ZezJ49{xcwMG;KV)L2J6`cjDVxwbq^azf%k z!rvx(0CccOA317E1KkX#Sb7uqVXARwNK`i57&|?1{7~!H!xaw9*Jmj8>+x^;0}J+i+Rwmk2d*}>4byGieU{r^Yxs6K zW*D(_7WWzD{!#}JZnPD-4aR8{k6de&j2C>Wu9}%fs$v3XBIYOa=0^Y+)sD6~6Endn`qwT2kvqqhbEPXYe21{w-eoG&g}LQ$5`SZSvIAy6Mm=(U zfXQwYZbPpQD%}m)4p8dWusBy4E?3zj%O>W_3}ZC-7WWF^J<9H_zK!>NF)PS?M(!FP z`-=R5o{`B#&EIegI1Mh@|S_P1cwdKQ0kh*N#S`eBftjjw7_dF1$bpoG~s4 zyr3IJ;#|}9ppr$|2qFkXckxrU>%1UB^VQdCve6C^LfY9_{VPiN3*F=zVhd@OMzq=hR%+`G!_?W?{4bPurk}J*4>{2ML<*63f8HfmXnbdtT-7XG>L6Ql8DObc)L_hReSg z2K;9#d8iwmhnlBiByp~K!p(0cL{CeXzhZnE8G0exS?PL64Q}FdD(Vy0;I$a8o1-?+IFnQXzVARn2ph>vF)aDW7}3^qp@wXjcpr^CiZvouJ!$ZSu-oy z=j{8w*c`K*mfva1I>m@bhJe5nKn9Dml)B(;g3~bE_vJ8CMT6qF8%7ciiY7Sp{`|HY z1t(G54Qp=;o*i%PIaxg+C?f~bpC-D+rxaTed2W^M>oI>MLo^RI>ZIs_SsLB7ASr{4 z934H~cSKir#g45Z0r8%Ca1yzK#wD0Bcj^zJ?$&+6XJo@!b!;dRqW$pQjMvY)bd1M` zYLr#mu{U7s9-G@#3#0{f9d8}cPdQsSy4%8SKR-nW9dngvyDgwW?;S(Dv62&^(*#2Z zp0~IrZR)v*BRn>TUi8_HdTwHk65o3{J#M&ms}#wnsWxSF+O)p4S&jO3UES_`19<(H zgG$L1ptQPhUyRhtR}(6E;o|(F^Yx!1ub_tf(-`7@a)vGFS>TS5% zMEs0FuOC_YHSOPLMN-_z8$3ht9han)T&|&D%5$P!P1IAQgjK12lfd9Ku_LEA1 zqJhnG4NB|SW1kVxf-3Z%!b0Gv35<4z1R35j!8$Bt2&>WrWEkgeoWmXuBsAZTj5x4m zxh4iimGeKguwAUK(HrF&_9FVrPmKmr#Pj@jDG;dg<%B%O^LWm`Z%c21CW}#})*t%O z9PMi1^B&4))4ssVxKRFrOoHc4Yd{~o-*G)r&ah6)9ni8)uri+2)@Zr_r!lnvjKurk zk-!|ngLg8vZoJ!SyiosvyXo2ihmpi^(_R}F9}bEdAbVLzaF}DL12!|LNJ292ktwnz zAs=fp7Ig|MIUMVxZVZ?##>kEs^C_3pVp?V4zrR^=rHL%@#!i&Xo}%-rrT^nvwf5Ls zb_)2X$%{XEMA9Dw(o{jAo_ z2Eo@k?;Z5A$&_WM|1|qvfsv){Nm&UM$ae7a(TCOmN*rwUf(=jLbQ77LyQQ83a658~ zUV#^eDkIkAV-O909+T^UZI3-r?3Mh{@HOxIiD_TjXxA|p)WYQrMWRmj?NiJAF(jIs z@GBFF&p`(1Cy5a2Q?Jh&L|(skpyfxP-DF!eBM@Ue0iboCJ?Bw&5R2$j_~DpULv>}L zQGKJi*_cjz(hc-xV2R61SlN3l=P%ICl&cs0Ox$k6z@h0bOYD0M*>@BRHH;%`6ce^s``66@H~u?1bN zr{l-xpAQ6mXpbO%N@!Ez1$Q7lemeOzBIZ2}>r0?k^a;X31P=->pjj1KbUfb(lt6PI z^k{9tbIL(7Q7hw!_&$&-s7XT1I_4iK+7=42;nV({l;!GS_c)ZcS(XKXA#}hRTREX| zEoM|h1U|?(v)|+~T(+3mKt{O=<-XC)m`E)*r3?Gg?ashyyCoYrQ6ftm!%6vMk%rlY z9+|>iy21rJ1fE>WFq#>#>!}D`K*=0csrJ9H3|&8!) zEl*XB9MW+Z)1M^?l$9ZM7LrTyA_UAdms@U@+v69tZmC&9(#Sv8kcUQ4 zU2Cn4Wh{z6S&qg|m3~^97AC=R7?n4sgEC`(9JAIv&(JP2Q&0ee$l6a9dsIDN#>%8F ze-jc+{$R=?Gs+w|J?I22{{3865MAXrauHvQ(RBPSLy%jF~18bbs&Vf02)#JT@ zMxYdFRW$}#P*z9Reh^2E zD_H^Bc}C2pQ<`Bg;Q!`#FL6Cn>~}B6I|)|iwxO&vo263M`WQp1OrMLa-Iwtc;^3Hi z4t(T9w8>W&I$a+HPwhwOidC&XF(g`Gnn5|sdrEPy0GPf+J8Ve#i=~Y@!Yp{mem8v@ zL0cbt_7j##jIoj?E|F0uf3nzJh+{s+Ys;mCE_$fJZBXVp;$fI3j@Chlv+=j6H{9UM z96?GV97_F)ii((os>VnOr3&ay`>4AY_iZ7JtKHm@rF33!xNR*(z5ej9Sc{ly_ou63 z`3zN@RC}y+l_$V3sGjrqt|(d*)FjMgB=IyM8U*J zTpv?4AwkhCU96o<9+G~*uPsw?!2|mUg<*;m zy8Dy``S~x{71+>7;SF^E3a=7kUGasIU?M^tNp%bFH=GpB@Mk_58j392G_RlhF~}Z~ z_n#!lM;>LOLot-wHekma$We9MyKWg4sL}{#@zGt5?077f0y5Sn`{&mW+YVX@E~-?|J|!&8tG<1#x{yJztcJj zN5G)!VhtylhS?3f!36Vd`B^ZBZ19BWctn!ykyi!fFRZ$yJccTm&O>{6_94Fz~2h(jrLzn)bPymPXzD24=Sue^K`I^iBb|M*Sftyig-4QxT{FBGIY6 z5axcO(cvLl2K~#Ygivri%*_f)C7F4QScoF>~hq+KR1kI52d&MnnwG|h5m7DF0dI-Ikyu`tA6U$KMDo2IHLO+=-B8q66SUMS1Jz4iMPB5LCgm-qvM+ zNhq0?U4t1Q-hYB9B4GTD4Mf7~jXCU~QAxG2$a_?y_FSU$@roQmhC>kN`1eh?g$5l# zK(t~Nx&=|TZ!xKcD)$Q^=8nWrz(w{W4qm(tN{{cZg71@x8U!t}I+a~wqWY_MO1Gz5 z-hVxaz0zTQDLD9-;$QM)X$|7!FXgoZ1cSR99`%SG1EcGpTpk&8y08(Bugx@c%Rw&# zI9>_*oqLs;*ms%b7Q}l%MNzl}2gR$2QYK@`#No@IDx?NHdR(tI6&RJK%$f-m=>wFc!)j)3#i^Y_6Wu30$AFy&ZX028QTx&=%+PHG4fyr&hs#Qk&f=-O93 zUw+vM-Oj0;mTXG5TpFh5sw|TYY!9Ht&|+jtj-QP#HbBsP73EZzr0N+gtSC^XwjF`p z7gmHzr6U`A+hTKe04xg{n~*lW9%K`Fp(NTq0KXX2y;vLE9RmHpKRW5JGNOd*KI&Rx zQzGlWME+~%`f0JdOHgI1XI?I^J@=S;%!v=gy5rLmb5GYi(MjjSpFx=S$y#*&rMIPq zD^asgGdc8eY|=sEVn<`#nmkLx)p=^}<_Np$`hbgY78LmiPyF5Ef%$(&=YrD|Lml>W z=m|8&$i#~~%1m|pPyIB`XMIyq&{D}PaV>|VmL?y8V{U0Awf=MSV2@@mgk!TBG(ew= z!KPhG8|5lEd*-sBTN_Sb@EOVXTu;}eZ`t}rMz4x~scAC=BGf9bZnV0H)G6%1&s*?9 znw4nDq7GpL%rD9!aooBXL)egy3>mo^aIpP?aDa5e^~BY_K%qB_Pk>smS0?;Y;9{Tk z16r-o9QkPdT=b?)6#GKnKu3LN-vV^xX=vzYI?YJW6ByA*)B#2xJ7Sp3s6`qn?V2Sxj7Vfiq$^-RB3`tRMmQ#TI?6+3JdwoR-f zY?t=v)2S1V5SKfMdmU3gX1m@nuuy!P<%_cf&$kUCps)e);`sK@1?s+pwtUJ9Yrwr~ zsz%Tbx>qUiGjK=CQ9U8qzaD1=X$`K~HfGqx?J(*Itye{y0i*oSVF>@Nlo8S9em-&&bdy2)WaR zvc>N!!tCv)Ic$Rvpol;ta=N3WV>^F={3>i{4UyIt8W@%cO7DeHS9>|R)REkQFleUD zz4g|S-sW2ivlj0Wm2BlxGrE(1%k>N(!#__8xO$hxS=mPc5_rf6a5O>ZSa+N4UawZ0 zRWi863ab@kZHrCwl0E0qeLTMepms)kR#<*|rJfk)gy7UR0QFFVGPp@P)j;uNg2!ln zYb|!TLH|Q4JaSHv87l26~fy&r++ zN?HGTC9rSyG2P+fd0l`Wy}gPNRRW`2NRx(u8k<+vigNV8y4nN+I+@z%L?x%JuV?!k z42cL$MYSJ(^hrRx-GF3IwjKKg5KB^I`;;8v_k^*qVMTZjjpDw{O4+wGJ z$l>2hOPkZ_laHviz4tXE_X1VcY>|DKt`gtqxu-Sh>Zwne*FIqmbG7>!!{{e8H+g|> zQC9=Z2nmJieR-veABy#PwHwKc#)gV`El2s0fLuj+6ib1>*1*phsZPYS!ynT>!J!99 zIJGEXOTdVC#orJIL8}Ji1o#7Os!B;<%D>@Y6O07KnYsw7u5z7P0yqf9)zRh!y|d;Xlh>t^BiD;v*$t&~i}d6Ejo z!8(bSBNe;~k4=LA=X9Yt`?lpq8Y5bpX)--y$7}3h{;>!LL5hy8dBk)eei9{zm1)+X zl04a>`lDU zW`M`TcIYA<-j#}x9S+=qmO;!+&H`ZS4l~fHEe$?!R=e`Pru^=#jVd`7jB1Zh&wj!Y z{>A?+j54M~HZ}njKJO)_q&K&}0&zjDx4X#oLxZrMG%mt~?)NKvv%rOruwri__j;a{%2j@c48n(FK~Y4hn<=&Nx9{F*IPr5cqnk;?rbJ2144=8wfNea-HzC7XxYjDaxxMIVMAz;OHv^G zc?Cv@SxY6iQJoECS6!@OZc%|*H|{0B7L2El`mwfiZ9!i05HtDD$bWCVPC@KOw>~w! zv?AVH!fDC&tlPxcG!v!mw)m3nJAT8ScFlPJerhEAE}U_sYR4Rfqdh+ihQIr_Tcwqv zou~8^h`Xhg2n(;0<$bsO%3qqm9nIlpc!d0zd4_6Wc!7M&6}AdpD;!~vyXQXm4N}D$c(s1 z9p)^HZJiSQJq9P2{N*oikAIyzE}$8)tqy%%5~dDxz!KVMos>_pU$KhxpB{qnB}1xa z41nc`s2WD9bV1U8U9Y!#lOXH{RbM`S6blo1UG_ScEs%*x>DQ4P2E{s~xRr~LxIOA4 zBfDj6KJ9&BiH&#rnG_P=bHcJ0uBa?koHour2uaR5OX{}eKi^HZh#YySV+KzOq6I&F zfHDG}Nyl2lFwi1CfOvPY!dYscZt>L)qoo5BsI0Hek^QnqlQQXj?%0SN^A$DMXaP$B zo!ef`putxg*$EL2p+<(OwSX6kM(Sxy6EQ1*`?(GHx$Pu>Q-E4>HImyBNi{$smu7Z% z(aAD%Nwpam+i?NH^7?YrFL@>*-CH}m2QU_!?~GTVu8yD%mjmwfdw@MkUosF|Mi``p zfg}*XifdxtSES-mJC74XExphBjWduOq#=X~Mtg^CZ~prU1VRH(KesKC2d3`m1sr}jOlDxliWGqptR;uq_-~JMbj++n+A|Q%wz0p!e-fxBRB_L+ z{X&LKGuwL61gMUgMJ>MP=7a8R+G}s7WXwlEZSi&nQGep+kfQW%A z&%ZLDN!72va(i8d@Xb^ca(PO{+eu@Dr7`AIma}`~Z~L7pcibi%vsDzvGuE2}(_6wb zw*7vO{{|GMhPf(Qa!-!)3NWh}?2;-k^=R%nH)ma|fn7| z>(u&xHh{d~TZKDk{|T<}B9UK8kVG3mA<8B&vt3dH@CC2$?o)vza|YJ*@jDNQ?q{)c zu-s=S;Y{X1YW{w4ujtW*Jb;hBG@{>1|2m!bJ@_FZw@$@Pq9EI%Neofq0(cxREPZ6( z{=V=w`!`NU23meN>PHZ;NbJ!5fdc)hPUAJ)ofi168?a|HbLCyn8W)poy-#)Kr8&Br z7e7N2LP2LawUvRDwT}J~mggU;720f85<_RT3Wxs= zXNBvmA>iP>!1GQO5}zo8w&oSNR)d!v+))5BofUeef73(&;J-v|Iz(&3tH%v~ZzM|@ zlCxgJYqYP`>`s`}0XH%~8mf>2>*|o*f7B>%1N@nzP%WjMJLU*)uBQbZI%-~?S|E*) z0uNhvuYNs=dy;}#r?aroYIjlc8{jKw&a~@8-0!)(ZvkpIPhF1(PAh)9W|4QB67zO9 z-ZR22gh1W|%O}Lle9_p%;Qy`XEvDPc`E1}l{f5s0w?*uO)%qYU3J7j?BIxPRc=_! z!3)aJGs4aZFgBd!8QK|{m8on z)C8vktfI`Usx+ZV^x+!GY?h|&g(Nm~7_vQ8EhXDz!|r0|x%2$kTe-n&8KxT&pS(a; zI@k^()$Th!3~jYOaRfCo)7J?)*88kx?fwbVoh5&VT;|{yo|(M*1bUe?L+-j7j5*-L zCuQc3MJj#e5WXjYt@Q^z-5xDvm#YSRA9w__hZO8jEms*pre(?Oc9mjIEWh=COVq16^klaKf`n;I=;#*j#0)7Jr^r;?Arn?(PZ z?OEnvT0b<#Uw$LO=2xr7(5Y@?tYEApa_CGhvb$-!=TM}wdrdWpnM)uu;X(Kz4XRtn zC%F9>Cv&t40t@77vA0ZTYj`=OTx?C8n*C&*g~Vk<7QAkGqb^jfYA&Pzxe#j1K`NY( z|6EMde4(-W;6CkB8~u@|6s)409^Sy{>vudAHS&)?V$e z;X$m&Aw|$FLZ(;`mbZA5lKv3 z=GhD#Pgv#YSJxJT_3MB_j%a9Cao<_m7TfsYGc%yr5J_C3mPYPj(Eaq}i24)rLOp;O z(6fraH?wASF_|+*^xFjy)%Xo|k_PwA9TleNYjZp7lo!d`pz7QIIz)v>CZve{IBACN zIHqLUvye?momWGIr_&1(PY9@vBC^t#zYdSqT`xij5L89<4gVS>{{|4bFzrjZ4plNI z1akbsXY8JHNj6&ee?dNMyFuKea@0^=SuQK3@=AL*F<4i3zXeFo=161c{WK+02%L>J!YUCyBp360)6rL)wfhY%*2b5fM&?{&GCl zG?f@4Z~QG^CSUYl7n-DE(+i9&O8j$SfZruXhkUV@P?6_;ah&rw>Ms`elq~Me=mE7a zr44@dShmYXy~z(JL95U86TIaM~N$<~jy<6PlbcNZq2-JF8J*ZsAYliT{<%xnLj-ZTHYkxQDrsT_I!ZOJDZGg3G`% zL&g98g^fTH4yA}L@!7-m9eJA(O;X~_SjIh?VB_~pArdaJyS)QdB){3L4vcKbx^j3` z=wEonT<-jB=J1ON&wJBqT%h?gkdR@}+5$W^bUIXew6LaAbGyUMzGsYlc_%nTYcMj$ zJNkFJ?Akc^LAUok&OuNXgSN8!{Co>`xEf|OiqxCVLZuxna=#JOI}<5~+WaZ%I*am1m1Er1pYsoxRFZhe!7EfvYX9b7k#9 zzJus86dOc(tF8?4JG-rACPtezus0+HN(57=`` zP3?Maq6?v;M3Xa2D;eK^K6TCfSd~+R1({%^K`Vs`qnDOH*#(KTFnb@%FievHS2;f7 zj$EH5{$F8h)C+dq{@SKIbn{Rk_P*j=&6d#SY%hZcy9+>zmw|!$RQO2!-p8=x&V9E> zc;*|@e~k`?zxKUiQYFRI574Q1S|;6H8W!*F^QIy7)4iFB89#EoaCI(8y>F(Smz}jy z8vT9#rMxt=3rMjsUo29YcUwz-i@+f$%HwHWS%m9W(g(Y_dq{9UylW!qeX1NfIoNFD zsIw7cg*)UafJ+CMN0mGe3f@t|YabhDgB0*8Px-JPY9#CT_MYa#stS}3E6_3!+w`0i zk@Nb`>=l1ss_25-uj4^}umXY(?I`vwiYLkwPaAC<9gY^7b@ z2+u?3IHeT(PqcBa?*$=dF(!n;{{4n*fT!IR|c zok`Ityhwcc@*wr3si(jG(q)wd7d=1T-NwjuutJoKUJ3(OYt zT-~k!tXdv>^yPp{A=ZcbIvz@oyHihnqei`9$FA;v&e_U?6+Xcq9+cHMN(~0%>^n>b zY+WzIMz$4;;Q}g=n~piReGZfDoApDb_E;hFds*%%#{{=}sAz^V4?F7-rb}S?@>73WEqnh?T(3!qOq-JXpEezQwj0UVy^ww#jwo@cGpcN74>sq{nhs#IZ5A_wP zZ{-=Py&lQq%@Q_SuA1R;5Lb|&h=e9cFf)yJ-5)ClI&80od2UFIC^y|h_t zq$vi?L#l2qq+)lczfLHQ9ORvjL9fuDX(z)%(M$dZNap+az6F{^K>L3+;lk8cIl6(+ zSLwK0Eo049Ian)Jf-n`~>uGnniovvBY~N_i^m#gV9u~ZwU18o0yP&DesUZ(`nL$~( zlHtKjR$KJi^;*ppItWFKMTpENrjd(rR7JpJuxHi3mt#-bC;wDfI^B6s2-HQxy*`TH zGpiKKGAX!&9Y@p5vb7G@n>vVGjxsDCccH8NuRE1wB_v_aAl@ z7(XbCG`7|+y{@|9rP~GZe`qv?Z8DBF)X2{$%l~+xkk5EuLg#=SZS@GAFFINe^tPk# zch#-+M>Qn6%8y8J%@7A&N2VI$@Vl6GKF6eE{SDm`u?Lbc=D$J1McKC-@6g#@6GKL7 zA$6@1{asmFd(!k0U(9j?Sg#A4p>M-_GF43J{bt*!fEnjHF_MM7qiH@B3Dl zzqodUSW&`)l*V!woy@ZPt)QIVPFpDS0Y{BAV7(PP{YIy`%^6(jd6rXbkWbZex3LU4 zQ6r4C^7lE4rjutSPISAhvN5GeCLLUrpx;2X`FRvf8!N2>#?{~zA(0;4aPDJ5>%x2d zX|8#EFbAXkc8*{+>-wvAs4QjUz_7V6GXGsR$_Z`^<8w$aMVskXGX}E%iFx%OtpzcJ zX}RAh=>~D4Q~e`mKrsDrHSMO*eSRCVqdPJSiglpgoI@e8<)am}_l#5as}Z%vk2zuz zY6ZDUhz+Q)GAbAfs5=7P@`f85C$XC`q*3ByqJNzXNwB$=qb*FC!_AOFhhuA7tI7D6 zKsYgpCQ03_;H_*JzDnN?o_%Aa%M{77k7d@`v5&JzCJ=L^)B2D5offU-qJMNF*2}o{ zcBrW0sEZTrQU2d=&NCL)ABxnRhtLuRu{>9&;TPB;TX#yZ>fv-s1-z*7!W!@oWE-w$ zbSaAlzTT#7sWhU!9EjRuAOC=K2vf{`RgnKZm|AwN*pJ*Abe6znRD@Tc{7aO4TZ8m+&y$6UL>G~O`Z^xf z@Q?gS(4TT+`OVtd`XA#2$NAivZzr@R#hzy+uWMEe9Qo^0kWW{$KX)FhUg_*szUnrR zReIXf!ARGyq+lh#cq^>@Qr8c+Iv$kMRtj;M-13~UrbnqFbT#Z#8%BjAkQ+`1eZ8n& zEXsl7jAcnl_7<;*Ee`>ioTf4+J=Rv#1OETT6U%*5Ye zfr$xN{G{Ie;Toz9z}1}J1@;tNyGFrwf4b`UA4J-d0AR3QmO3(jCGs;PLqZXZ{4nRG zIuVCnK3)9|w>{f;7Pu@CyF)2L^-jTUd_A%iMo`eMNv?WdB07}D?CUA%^$6O8P_^#s zVmEy38rRdxOP!N+h5Ye)$DIs>ITp`kSGrBScS-%(z~UzFbB-%?AjgvvDiKfX&eie; zL$X{<6Y3XMG~#-}XogRpH?*6U96Xk&IywdTJAov>FR$D>hp`5F7RN-a+W;O zmI;)QD@sGjzj{$lZJU&s)=XuVIOE&4nF|%>w^ID-ulP#}oK?3+8HRv!9qNRI6d&FS6&J` z&dkvse-gGV`u`XU^C7G#c`B39!LLx@e3e||Jy`Ovp2a(YXGAd9;LH0vCYHT2$?cSC zBbRD!b?hx^YYy=m5lKBA@4DmBeGEHRe@tUl)-I?lhJfBI1Fa_1eObrm`W66wcqJu~ z^-$JAPlkg&LcHo6k%W?>@WrleE9WUk^wxWDG5Fn@Kc22JZN0zwu8$W84~=p&-H{UG z6MywHrI$re7P)^^j+?3ETVJSr%(C9jo#f z7Y@vrcF!K7rM*|bS0N69^(J2qV(5n!##sTj4{|q!WH&{h;0p0URjTJ?12~sVaJB{R z{V3dX7b7D#U^4VMW*p+0(&D&pQZcfHD)RK4oXXE;11z~`4OGGYeL&`$<-N7oSzaZq zO{xtYDsX+lDs>ca-j~8S%_O^GV_EDK%4#$qe-?78dXZy}J6KYK9|f0%?3?~orsPQu zOda_|o$)jXduX64=^E>{_tSJ_4v&AJxpaDP=st*(_B`$6`oyP3=o>-|4p)9zT*VlT zo0wMyx3Bc_2@~r|RMF{i^c7_<4q|c;#qsmV1Eu3+-uZ-UHs&aQaSC_GcGCWUf-jTA z7LL2gUaZ7zpsApgw_HcQ$V-PJ!Nlu?`@Ytshv-dS>9j3@xF34T_uf7`H&k)CXAp`} z7sm@+(XeXp+GpnQ{p9JC^53TDv93FuldOu-hmDPsFLrI6v!8c3&niKW^+zCWqJ^sm zvlDV;(pxi2Q5&vqm4|kI=S_!kPG9Ekt|MEVTqH-Y-=K#=FlKcd1#)TT0YYdxmXSE0X$HlJIBUeG#eRow%_L&wD)9Z9jj{p$bGB#R2&;7Te|A~?bf2SiE#)0ZWa-CBygmq~Vz%y{ zt~_%G-uv zMf?ZKBa@Cy>*<>SjM)QgiDR6RJ2)Xnor&jtMw_d!P>932D*liA=6kKo&!=K#sO+A` z_{Y}6m)wq{$7jh^t_A3NP3z#@2K#|h@0x%~UUHa=*gLHT+T>oa`xVztt$ym~DccM; zx#voh$%H;@yHPoFTF|!A-7m$jKQl&!=-ql7j`{_flMoY3=smTkHiA;Y$di&0GOV4L zgBR_S{i6nNH^UC2m5{2~6**Sr*;jdFYHsa^3V!ohkh4Ryh(^dbDu&VzvpH|769V^Z zZ7Xh@mR=nX0NYhLE zRwM+uwsS!*02ajvG{ouRK%pk|&Q;EC`o6!80@*ou)I+{09}zYC9}Y6PXCRBIn?(M4 zm-M|QJ0oT(Gwtm|E1S&R4T#=t=C_BW&0t`R$R<>>!HEu+z3>`FeM(@UD3BIc&q*n| z{`GEk(W?=)FgRB_Q_*^b1&k+bYd2w>t7kKFTdLqqPn6A6-TtZ-xL*37<>LEe&U@*w zP5rku;QN`(k3R5!*gL?*Bxzq(WMlf#RGAEfDd*3C2x{1%JM)eB5I%Y>D`yR{hu{T{ zLqh1y-+>OT!!tWFF&zBOiAJE6Scr zlF|iO(f$ai?9=&E{pP+r4R^VP&Ni$6->m1`HTm#2*{y1|Jrsfk2!3(`+}YYeTomn4mUw{*Xt#l+<$C_UcDhCvi_`# z=fSLdd?NjA9@xWLXf7IcNf%~hh)^i3^L6dvcrt-z*s^h-)$h}qLN-zHe#oy<-%p%} zUH=OI#@Q_4QkuBvioHoL3$Vua{MsHNlfuaACMz1Exw1!szmGHN#7aN@Veb)Wo*WV& zqSY&OFsY--)jm7NtBJt(qi6Qv+YO6~le84u1*V;PKpqCGB&D5(Xu>by8eW8fM-&)= zkO+K}mJgBjX#2)(RUhegFte8t&YvQ}x6|&o@|ZVTSc#t>Uh*=Jg7-9xyA^ zl!wL_m$!r^W9G_ZBV3j)epuWk(@>S_%sLxFNj%JN;}yBx!7@=7gPy#%<01w zk@};N(F>eNkxoYW#l}1i3gl!uSTDu4S6TE+opy2Sj90;@x~MI|Z^)Orh;>uqft#EQ zOE`jSOdPimyCa)t6j6O*JoJ*KL$A*NF35cnJFTi)YoDUka`U^2e3OHQSeWcK3Dv$^ zXky4y1!Y?x(wLhjlEz-*sfeOe=TQWh)uT zlbOxJzYm6~`Hu!x4+C2#pO`PL>+%T+D{V9tuD3JcA%Q>Jj2yY*MiwL%H3(5bG&zns z?5yD-eX*k`W(H9KS-T&r1hDPvn@$y>S(8Q}eu!9aD?&d|jQt)(I2JeLGzxOxF)zTT zR(iY_I-LQPCk(XsmOlaYc@kM47T?qRj~2|n0$riu>Xla+{@=azlY;Hq#MXL9F7_}Q zlZJnI(|$o@fB{+Z{f+M?A9hk8DsRclM+2~$b%BvRYn@I19}@^0MYd^p?Scwp1k%?d zNe0FTrX{8Y&j<#qgfo9F)P{LLQG98+Khp&y5IvXI6=%_f$ZsW_`$4190`YPdJYuf~ z%RuSe$$1_)H`mhfhnhM<`?8WUZLxVRN_mgp;~6>uIYXc{7Pk6;;@tC&dLH_HVCzVG zWPiT+xG;rO5g0u;pZNu=a%I+!gmKt-JZehp5A09%C2Lw34JYlW^1pGs_)A&$JFJmR2Q%FFe!pR@HpC=R(zfEyi4gDaK^;lfXIUrum%peT5* z%+lAI>Tj{u!IqPCxy~R(lSlNIRM9sN6b;~NgoeFev8raQK>Ub^ zIK|GqSzbtuua}iAq(ZS7dobv3Ga7RJhn0k}7*9#E)?2g4d`<0=GW$GpR6JXp@x(&b zyGalw8*v<07_#D@UE{~C873KEEe#SM_(m}5x-;3h%`G=5fQI2JC((b2ON1TZpLZO4 zf(iY2_}1|sIE$Cq z*FdRg(i%IsbhbEA=YR{k6Gu(S)c$C+Jl4Cf3f)787Ujv)>$1)#R%k~q2tudYAURwN_P^{;{@T_-`ug}d zgy}bJveo6D;=h#`8~50DzoC#F6dF(N#?K(*gKr?vQGC?#zf2cwI^Bv}oH+q+f-S1i z3ewO^c+d$9^Cf~hdG$FW;}2J;NSqMShFS*XlV~mkhn8Fj2gOVQPZw_q=*zPt}OU(87a1+Q_H7yb^v|^=7<~2Ee2^pPAIz zVUGWLuu3m+FfxE6rN2(L-&2#rUw6#H#=37FM=4&jtI#!$5o~i%cxgRrf;IgS<$^oR zFT@Y?IHy>x6zfsxtIR6Jn#^-6*o{Ks-2clVy06|}NsU&L!92=-6Ew+TI>p12{!2?2 zC&(!TWnaKfCHMw?1t+KFHd%bT7_NlM@T7 zp0IssNOD@09+`|}WD8mdY;=(?Ft#ypz2XRNhlKwA_JuR#HuOAQSa1PebkHynav1~h zDNbOpoR?4^e1#Z_p}e@%W57$V4xgqI8_WMf115a!wvbUC0$f{GQUiJW*uBgpM<}h5 zn*4At=v*%mY>7TMMjsC3-6TTv3UqiRhw=nG90}{MK(5#E@El2bnfg<${+UbVTDlQM zs_jY_GsuC^rJH-gtEKH4Jo>4Na4-CO`G;-jXG`~Z1zdexMkL2)4$5^r=}?ox^H#g< z89Wv_#5df}gV}qyNj>mo zwS<~?jnddsJ;bf&E&ZkbU0q62mWw5S<@GaJBnj5vEeV0whE63xUxEl~UXcW! zAOYS6Wzp5k_UVnTyxNc1V!l0Ua@~|IcpP!mQ`~bAIb~f#*_RK5n){auzHXIq&Y5M+PZ5}eg=_k zsgRldIQ5<1+@5n@;eh`8!&U3Wq=Pf_m4h3dPD$M>xk+e^+@o)n)I`;mz>+`>U0tsw zgnQD~>71nBYwkCRTIXzRBvpJ9XEyK(P(yG_Aq7J1_VFj(Bm}TST-z@2o<1m5UL>zE z6O!A$#F_uk0#HFjQ2AlG@K>3orP#~dkWwTjKB#zCb=<^c)Z-B|MV<45J1|n)}=ao_}P*?bbyeOussoIv| zMm8Iu!fQxOG7USPaBc5H3_LM}klhs*&yClhS2unMqk2C~e@Lbn>wxG&hp9~esYIS} zds8H+h&n}L@x1xSifnRQ6acC0Ql)f}ez1hRnmVMcsq26{HwT*=-}4h_-ptjfIdoDb zo7R1GG`GdRMD)+0nx@IHI8Ab|e9tOeQkS<6Fx#HqY+iG)ynOuet$mm+#$SZ^iL2OB zkbFY$1%3?D`MRxC)o~Nnw_V$UCCl)8=wHlC5Sm0+vg}G9E>;s}08Lvwo{{{N5_YES zA7#IIown5L@gT!~*>oj@gUI-dYF7n1vIheoDNUoEe{dVH7aOP%DF0F69q6BWMS#dk zN~S~8_)2GR@Wb6dTv-^gu3*N2PsGCqD%pmD|B99?2z6M>l%gky{qRrdH$Lzf2E)TX z+VDnsBo=R_gg99Kp>O0Lg+FZwJ@legp(Y(E6YX1+%dy`e zvcEL4EzBE|cf*(FYr`EK;j2zUg&nZhjxMoKWrYjB>fPr@ofZ$tUHw4aMpv zyGHO1uvBYIa2kW1^?><_clUSyL2KqA56!y-l?`R(=1e>?y;Nkr-z*gK#x&Vb{i_Pr zrO)eaG#a5VmM8aGw!W=_zVB7(*S*6PU2hIPY=}@+n?({$Q*H8K0>~(7Og+PYvB9q^*=5xP%L+?wj4tva+4_`YaE%CgkM#MqE$fKXAjZVA*E?}CeXW1n>KGj<$S$>) zn*CSTBAKzYlchYi-FlpJ(2z2KF|Al`z^Jp4$+@FX*u&t(zEAV|J072%NP9tZy+QBJ zR#NUaqPGsoGSdrq3Wnfng4u%JVsc)A7YRZL)|A@|YCRflB~#;AGOO22%czgGQjePP zqA_Yw!lVLC#kf!yM8xt?b(@Xu>^cHu0}nWya|^cfWPP~}nJfMJ^Q#(}1TMaF>-=Ju z(l9~Ke_ykzUi!!PQ*ht%_>ZMM2dMQSE&Vz`VLhzJ(xfuGH*A4T-7ovhA6buQ7mh11 zF~I}fdc44*(d#&9VcA>gYAqI9q9g} z-$7J1TD};I#o|i9)n#kKDf(cY#rhu4Z^&t!;~X)HNG_7bV8kDcfkFYDa-S-OglkGg zwDDI*_o65GVL<6Kh0KzdD|5`fRtW#h$gkf@_C5c-*#&)%7?Fc47;Em>XH?T-T?aO# zXVug#IMe~PlxP8S1GnM=OG+CQ_{%+frUv+ss`0G>SDyKEC<63NN1AKtr-WF~8@TFa z)Nlj$zl0!Z8Sw2|$ZbEw8j>8JIo9ckj5YRsrKL0<;GIljpWylZa(luc_P}3yo{%>l z{f)V0E8w!lHdF8HrVVJRlX$K!c~geDNz%~QS`FenX?(ZWEQIzBRbl6?y*@n1Sa0PA zfSXFb#T}ZJWX=&mQjo{W9C+PcYDW+ zX*Dg{ihFYyAs?|qpTXzkwAoz<_DC2N?h-1O`&AD zJ5;;QgNeZ}=Oo=_ ztP48G<-72}f~1b87}Nr+4%Z6-?KoVkc#BUqhhL5tIdS60n798PPBcy0j>N|r%s33) zSE9HMK&){dc$8OVGR#Ev$hG%o$+ZJ6lSOqZ`@4V3jcBWHS%!!Gb+>vK`ou|xV`(Z^ zZ>cq1X-(_`ru9sGGAlH2j7Kuq@&QAt1)V$-XxqLcr8I-fP~7tl2g9{dp(J-FNJoV4 z*Z&7YLA<{8l~$aVI_KD9>Zz&d#k~C`mtGuOA3%? zlky>l98#xb9bdoY7I&!%?ZSo5w+LnTjwsor-ca`Je`0FTG$Mt{{yg*MEvWu*jwav- z=ZFDHoGE2X%}q_S3Th*5`l^FpQKy`>)ggx-Tz3+#Q`@mZ!m12(Fh1r&_@te1;-{*W z7Oh8xie5@U`3rAOH*!b>u&bQC5o5}H? zG8JfgBAueWqe!rNTJiGS=RPM+N?cT}2--y3_*t>_geN?q#xvc=7mD*N$5_(`MxdO! z?Ov-LZDH@M5<(w5Iry+W{fy~%3c8tC-&1g;MW>v;DGV$gmJ@P(q{#g74}Y%pGG)^! zwHsTDu3vrljae404Xjpcl==zb7U!VN*Ud;LI4lO?y84p=H|C==ZS%lZ9cknbSf} z#K1DdoP8pCyJz-ct%&VfrC~Vd?AMBgK}4`;0f#rHe4W-F3&L1o+-Vlisv~lWf?#xH z3+x=tVmdlm3aU&qmajUvfXQcYj}iNp?U~;ObA>bXz$}VhIJVjzTK-13LkR?(yuqw} z1oxfw8A|>eTUyO+o|#VrABI#OT;YsUD`g)}SqgrA(QoQOLZ$}1=PS?%-^N0xZH*wm zaQf%U2R``zYHdI}`iMNVvETA=9+)|EdZe9D9k%fdd(E&A$k$r+& zy_7>S;j={HbL_$H58bUQsS_qlioQRsEDT>0ek-oirUooS8>6fb8ipLVYn6r}*KC9{ zurA*U4JOKS&N-(FSj+}7js?b@rgMq`hQrIBAeuF6Ry{8vtqD4U_bHfCc^F~zZqV#I z+e>}5;ZQ>-o+xNWr5JqTd#L$mdt^QYRfZH=`P^QuQfj=~Il1*2W~y*7Le&0NX6e5v z^sT&NA^O&G5>=*Z?SnfH5}_KwX>6_WwGL*BQ#*tsjm?x*{2A6 z>G?K@=e=^l!V1M4J}*u?%+YJoCxMmxMSh&cm0>DTetuvS-~Da3HQxKRjFsfENs>dh z2YgWg;0Ug?1^%Y3#bJ#S3>sK-;87b=@+kvwDxUY+$~S#6X+!(Sk7dI^!BTtY96jhE zJC~!5d`r3e-L?t(SWy)Q%E#1D7%a=2XalG*62_2Gq`NTs^rt_)&Q;Pj%~>Oq6gHSZ zw+BRz_%SPlP2)!VPGuB)6T}mAGWAl=jUi;c)Wv=0^KjHJg%1H@UexjI_8IYhID&Ra zm=k_&zeJBt7P2iUQi8j^t z-skiJg-<((uZ;w_HWg%(K2{3prO{T(kPqd=W`w4n>>#;S>?-5fI*qE9n2)SAJJrbyY1#`w$?k2B`$_XE1^_3N_E| zn_XbFpMFJgXOYUCSEZ-e`0givLX0w_y(y#M=JhXd>QgSbq$B7oxs+y84{!*sNO}*1Dc7Nr+fG?V=wZD(Hf?vN&F1fTSYuZs7I8T15e^cNn724Hi zQck=z?Id0_Wq*I|8W5TYqzwB*^6@f4(ft-js^-zW0Oj+e?2}k4>LCWqR57G5eMOJ>#q8=%e0R_e4RD zraM_Sp7P|UmEQ0{SQ>NglO~3rMdaI}dGeE=T)zJGuUA1wDZT!> z8_W4W_+i~rpR!`N!#)z+P^j+8a`Z*YyIop8@rh4Vr&9v`agW=#E}=DW(r$RTsV2Pf zB`#E1nMmv^jIfRMR zDJz3cBM@o?5Ju+LsW2G5U`885P~T4FA?vSho$m)z*Hjn?59{;^8{YBE*u$lCCANG5o4@JXw(~Z1_v0V`IQsV2>vr%~Lr^l{ znzF!pplz+!=hMKUa_ZD(WPkU&-@UFzvYNq?&4Uj`snJp&ulF}&U512 z?QVCi76R`mHAczPr_Tt*pz(`POW(lFv6hM4%BP=S_{+Ky16=xbDhvAW_?}Ih)x(vx zV_D^lYQF{BwCj&}pXn%;V^#*+{#$OVJy@MsYphto=PgM`E83akxG5t2&we)9&uxxp z`iT|7-VWmLxbyw1PlF|v=q{))_-dnE(ip6w7%QP`cZ3`Z2uz`)^PhFrS#^H%$Rm%e z9UA=k9l>HWox*pd>z8Ha=o6C>^8NX-ZXA=NAlNE9H!|srQxmj*R*8X5rD|Y=R0=SB z41+Q`XU?3u4jipiCLZm9*`N{eDH()ADxw4PY@bC+vVzNiW$ zqs-U8{4X;zWK53oDIGjFTxBE^PLD;T#uozA6RNr?Y&lh~L7PTX$k* zLsLOI5g#0ll?0E`m%+m-VHB;bDO2Pdnvw(+hOm0YV=U^}q;`bVu6Q7v=zmkDGzWZf zPG$+x7vaufQ}tBPP#X0|ZRK0`O#&Z&<+J(b6JiQtURaZ+g>|+WfUJyH!dh`##F-xP z;Dd28A_t6evX7oB^#^hQTae^Ex{V9J_ z4wSN{Pd1K@R;R!tp|0Kiv<)SWL#()-^e0MkpXQ z1mY(UCL2;HD{G+Fbe7Nb=6=^*cP%e|@r$eQ!|1@OgS27o7{K_`H$mH?2fq~j+3x7T z20(`&et6k$zx}Gf$lN(hX$LeUzPLgv8yT;?4jlu|3z!sJ_ZoQ{`FnN zerm2cm)uee`0{91{heU3uYGXgv0zQz^%#et*0AOFtv444`s4; zNCzhArcAL_ed(JXnOL9276(6B#*s7NR*tC^uwpGN98;&p2sW%BXvmME;Eal`wwfAN zk-24hSW`CjV2=y!#tH;?+OBCcel1I*ovoS~A9$Y}ZMC?@@tx&CyK4uFVB{gKIFn*) zZh1|fJf*fT94}gA>-YxSn{UqYv#jQS!womrsSJJGSRxh2Cg#}s+ep5#1#?$GOSl>h zm`@Z4U_qxP3<#9ePzi5BIRj=T-be=dXPId-BhW1WvE3~PE^~B3`Y3=4%zmj9jUA%^ehj8ZuVFceOcLWE;gEg^fOX;-P%$d#TQGc?tDj{S_#CMhg`BD(j zODYfx7Bq^YIIISi@&$?1Kp_BYbAZ#klXQ|&Gu+VK-8n3Os}JNcfK=eMkpU(HXAlKd@YpSM|SmUL<}OMTp_Ad ztrCsGqnwt_PoLIPZjFPx7R(P{74054=?x1Km&AqPGs2QHK_89x+V|$0!ZHzf*1j{s zPeotujlG_=10|9ILSaBZtO*Mi@ROj(S!!({(HjcH)Oa>+xgu8JTvrEY+7!L{rVYi5 zEXP!6scDeSg~14fEJmNofTcSZUU*^o!4G~=zlqZ+L{dO_KhnX-7f~a08^Bm122NhY z)*h#o#P?1`L4De=b_{5H_|?8i18LQf8m`6p;~)Q6PCDtNaCG`n&D48AI@SjG&ye5p zxxEe_2H=~_&bESfxc9Tnl23v-gn)K1nm1w;q!Y&ol$C*!K>;zHLFZevsB;dyS%D-S z>jt4OD93pN$pf-%g2p|#_*M}1%AY#5C#Ear$32A-z1Ts}UJWY+mVD8eG}+ht^peQ0 zNk@s;)Wh7ll={h`kUI*Sa@P2Nz$;wCktr)zZP6%~aL#|EQ5>a7<-a-bED}dxoq~J# zmX>1Z9&O}G8B1@quODSKr$AV=##;NdhoeR`(*6Cxkp3NrbR-#q)^H9?RE7SK)Keu$HK|DiIMg=*LWEu z+#Ak}lcr5C%aL=?cy9>gW>6jiI`F0NuZZM}wrbO;Q2PO|f4@C9-Flqan` z#7#IB#HxqM(T6LnQPzY>F{+L0qR2xz()w1v_thnzV+N(dEI!zKLx?R2OGSWQV%vwZ zEWU3&Yf6c8yQ17aK->bO{vu-RA5aU4_oE&H7W-8s(w0ZPgkWK%H!S{BHVc6sE5VnC z0*3@91_3OOsi6tu!}+_!N|~5Sh@oBoPrJbpT;{jlaF*1rCE7FbCXQ!#&31A>fh8)^ zdYA3(J*bjqZuc#wK30U%*Apj~%CdJ{Oi=}_6Z=+9D0aro@MQ_Blfc?xT1p$Npn-Hz zXzkF0Y>d<>T`~V0VLJVeaJIcH0kG$udsd+=o}WWzHiS?nj07WD4w}{`TT^UWf`9@W zcuZBK2F&u{enY@FNVIoNvQ8W&KH-`oW|JKXglCFaS`)Ox8E2eP6$=%CYdEJ&Xb(Yp ziboq(g#pZy1~4!%G85Y2dk||0d-~>x?{uflVne0h$HqI&SrN-aLJ|VKgtRQ;PgMp2 z_sa-{VA7sP;%a0$p&l9)f+Yk3MNv)Sw*NX3| z_k8G~hgL^JN9|H3Qou-K*Cd(uv%Sf{{OvZ$EjY4VAZQ@FxG$ZVhxlWJ09LO#^050Qt z5M{ddgcjpE(&dj2!4wNALP-rl6>Q`B>;J#KHvzNjsP27NpL2R1G+4H5S(a^Cp0NQ> z*nk}_*d{pG2Fzq0T#{hMKuE%uyj;G-k16+tyuc&e3xtph@eP+R9}sd04q?U^?AYK* zo@WUUlB`)hot{qj`~Oz$I=%b!q}J5kI#tr{+C%NyRjcaM`q!$pR&BL)>()ABf?+M1 zz|iT#=XihU>MF>o-f~I!kR0f->dJQ0Fysy?8#Xm+3Gg7d?`-)q21KcIa2Un%h$JN1 zE`+Wup@??QJlfWtw9=&v=vcTkMPmgLi-i;xR%sRzWo9W013$A#7)1mHoWmS0y67U` zoCqY3avZ<`rVy2@4g`b9GkVHFr>xDF^8D$a{;4lPAn{<|cNiA{B8B~729z=HZEDla$Q_&wil z8_7vQ9TW$i1?fJp)-tld_RUVq6W;Ci?GH0W7zT`hL7z=YVBjzXm@`a-Yrq1tPmY*& z-@Z8Utvmzb3Ew3|CiSULeahX*;`5vx_~QKoSqA@L9sY>`k#?u8BZvSTu%jgozwl#E z`|~^k1xQ7oLEq%L)JK2EnJsJQiz5gPK%^`*h-g?T=%zQ;%9YJ__0?Bu z`R_aS(wDqUPMbQlY#~X5X7aSPTw5HJc8&C%EpFELhtg=#vT6YsMy2>r0$nbkklaY`V`D@H9A}yAh{nJll@9?FnA33&rhvo&$pia5 zu(o|50%d?%!H_5iAp!?{G5q##|F&QM{ont+oqzuM{v0NjO!}M@2kZ~+ONg{5e%HI+ zW%u8If3*+f+lS}qm#{Go?FZ1P1r7(SaEDDs%rF55Izm?;ubGdj83*>eI8%~zrzGb} zV$g&q?;rA=3^#1p;J-;|9sxemAlwNb%n-*3+8Ln9HEqvCg1*35aWKP4XRfekpM6&Q zA)KmN`FRc)qJ)77`>b12%sbzbQzmbC0YZ;sU(mc)zA5~c`o+#|ChN6(ZNrnl_OZBE zirx;|QR&`?~eHZFTv*a5RqF2sjN2DHz`v3B7_=i1T7hQhfXCVduq3p9+4$;8%a zFI2lgLXtG>V?!|L#2FzX=}RxYRJ-E+*mp-`P+%}1#KDOHD$XVk6oVk|ILn0*#q-UZ zH`|9k^dbMzTzB1dRlq=b0SpoF>|@o_YdH!q_{VnTHf>8=yyG43a7hBDRs_;P_jvxc zx4q5V$8ThRB4f$lOXqt}0XK<5cR{aU+DI6Za~}41SbIv7tv&rb-8s)RR(o*8bzzad zzKR%khAzUR1OE4pJ05iD6X5;7f9{jgtq3zsR#ljFqMUdz13skqhFb#wx-t;p9x&yV za>0kO=1}CU+)=Wmg+T{El}w><`H^O?diB{_x;R_oVW=leH}r=2tL1>k1r#z_egNR$ zGzwo2NWn1clyZx7Fl2Je+Eq1RE-xQH8n;QeP(lI?``8kB#tZYtq>(+?*=m|u0}KEL z0$4zM9@9p^1c7J!8{+q2Q1P6Z3jpTRpZ>JZoYt*d=Zq^p=lhh2@}{tV6o`NkdP3KD z9VJT_TyTNwwy-5`lqT?u_F27pwQsu{OAy;9#u5C*=P9nC0E}bOPbet!G7B9Y({hls z1}fqtP1*|phf75P2Y*LVNg``MR~`jT;5xOw?14OE-Um2tXdIM#Y#Id~&2&tm(< zenH!%P)mW?hpI?F2H6HFCb+4cbRCMW>8Skp*~J5#z!9&>{$L)ONe)T8;8Jp$H~Vgp z#cs|I=C{c^eoxHRr470+0i?3~v&PC|UPIua&hP;}&iED4&MtM3D8JY!q)^nMIsqa= zoh13=-od8tZ223gjs1Oa=EgY%tQJy0f=;1KGTuJ-vw$eG7e!&f zXP_d6dB7}Iu3YJ^nqe4m7D~(k#Va}pM`7azjWzRyY|{^t3ZKw$cN=Dz(VXR z`&YWd_3hkn#b8Q^v0v~!m&WhW*8?L7OhlKwbBK>IsqMROBGmT}@wcnrgC6;BbG=vs z%^J&2nK^gmSK60xFar;lGYnBzut!rlXQ)r;GGLsQQ&@O2%MpE)7Iq`Vsb?>x){XgHa z6V@Cr4c>P3SrmArB`;(=ieWaN+K&VKa%|FFjHPd4fBwfG`V-lmkJ>B#)63;5`f@K9 z9~mARP};DDHX^rh1=m8xlm{2JDCeLA7MPt*vH%@8C}bA-DSv{f&F|0CADV@h5jDL% zq%n}Si{A{UWL`GPihfD27;?7`JY+)uY6jsPSAikJ5c7P7qd^=%y8_6wn||?2rzlSVEG7nU;FyncIEkjE*CYZixEo{g_L)D%$7z0e`H;=n`3>O;G&|<4-6>iv-6@P0 zC|_>+Z2rhUTUh6mGKDE9P&*lK)b3~+&fTjU$~XPJ_#tn#!G5*AIwsz}+)+$qAQ~Hv zUA4;o;mcpQt6qJrm3yTnUsx_XUHNS&ODq*T&OXN85A_YyzEA@C>%aP(ZQasi$E`la zKKx%kUj4CHMPX>}qmvMfhjdgk%J#nkc<~^(hXA7Pg9fBlcfp?AS4>A7GkTqvz z)-$xhnc0V5tla;WF>7oSXww$N!=gX_b7?DhrB4C%F4yo#UK4$0K1egefdooS`Gs1t zI_;Em1nQb>Y1b-y%~jWqf0OUl9<_PaK9AR9TClCC9vmYoJ4dxXp8pply|W_5@5T~W<7ppMQ!L1n#U&Rxpd_S|O%*~XjO(L8)EvPg z;^#0J;hX~ogGV>Cv3ulg4O?IWfCF!2GQ}5|Fnj0k{Jvdq&XxAr&wkc+?%e6=cQd)) zj{p;pWLu2U?7;^&+P{7KHaU+u%0B!jpR%F;M%%3kV`LVT2b*O)(!mgMFcXx6Sns0{ zq4!3ydaV2Spj%te>1bD*?qFUsfW!eq&?)`^t8EZApTU27gQNj1GRl|rl1m_5Wq|bJ zktk0=L#d#MaR!<}*P4}|Iah@Cf&S3`-S7HyyZ-v??KM|l>-}Ql41dm9`wOvcx!{~K z3j+!OiYhwi!1EvXJcv~2PK;<>owfb}6{1DnWk!TJkysV9rb_#djs|l;q-P;OXF{Id z;m-X2jpd4^h<$q2Ivplm#RAGKGLsTW z*u1H1ciwroJ^AF5(uhA!``7nqP#)D_+@^QfIyZQXRAz4K7-sM=Bur?ky_v^z4(Lzx z_V#Oyyi579=lg&&1r#t@8|VA1lQa0^WN6c-O}2LJTHiG5=%cme?h;kc>o+KggE)+6 zQK@Ji>RwPN4)mdn;hF#DAdAnIpws-~&9Fp7+lBU<2pZx5650^+HkuWNx`G|jY=NQLMM7|*iMe6)M)ZxJupAE&FSIv#jsK6vg*^mds2UKl8Hq^w}=u+P1CVe({T+__o-m zb{y$ahUk8mI+Lf@Tg747V#2_zXA3LPKq_a5az%@SMoBswq6u{pe|+qbFPjqT=*D zgyjfrxlr0|U&=vSbA&!x%_08&C(Pt)`hxoMe7CNy-U*jD9RjaL`=7* zTV69kV(2x1O7K{P?ABl0C*Kh-vWs4_Occ`6gg)WUagDjP|cVS=^N9< z>q8w^B_R}qGR6Yb^xIg@1sv}m=UTuOo4sf>93!ow*nYW}V7mE$RRV)@pBVFl_dP0G z`^$W0-`mSR0+?MY8|8zPK`t5mx>rjWP2%7<92wLG_RZ2I=+iy)B4GCW!i*vlYtB4} z!7z{}?AaZxBL?L9t-ZaZ87_Jjc!0#DPvwwJc3H}R9UTRG^{ao={{1^Q+1le*Wy=)m zxVidLcj@t~`az`UbcoM2OUkFs)96s1ON}#pE|#nMqg44sT9iY7@}tLW_YUnIcl4>Q z3|i9G#9A1V|9{oDAa?1VV6+gU8Fxj_RM;i4D35|giEKvP^nki5t=D;OF**hPLX)j+ z&GJg9{3Y%v_l;OfYm;B2R6AvGkfI9I#x;qXT9rnD1(fJHisw%cgABO{C*BmwGY^58m9BgbTAfTRj6mvnTnJ@dGN zAAq5^H`KFxPuc6@qG;94r3@-ytg{z(A^)KPI6TC*6LGnx%Wr=DcH6e4Y)2lk#yVx< z$e_baAAJv3_!HfHhAyBGBrM^lz|s@T6lxuRK5Z;y#rxA8=&c0BLphyM17V$jW2Y#! zLOFB%Kw6L;{#~8d#{PW#ox!1gEh%UNG%ZE2{6MAUIl`$CfP@GkE`|O?QDCW5Y|*k> zL9{r+j#z$-{o_CU(1z7VsI^;@@I}^g^nVC*(HQ(R&?eqWaiTz?K>bpnen$glkGZii zb^&JC@iq(>34?}Jf6NSg=hj|ZbL@#4qDQqep0v@$bt^i@YH?W8HjZKwZfmt{pp5V> zKPqo!=o5-h*q}@;xX(^}GaVEzJ|D&uL)Rl(q^AtHA|1m=L{aW?@xLH&GKf~Z%HZBd zi<(b2$|7HDB_l!-rRES1Nnx_7Map@~?buZ(+V*X|o}X7vvr_6xoySA$9+4^QiM}6L z&3LN$I?DqbX4$@jtjO~1Z>^Uu!*T(J6I@)+Y#PaqH>GCqC=C`Q3IJqrz_7Uvl@s&y zr6Vdj!$JSy`7|+73(sFBlI+HZ?g;UiuKAvKGQLeN8DK!6HGs>IH`f&vx>3>(&Upvn zhyy*!NS^Aiaxtb{$Om~r%vzPEo?4@!uOd0YQl_b;(K?rO*s&|mwy)msOW!iu)fyzZ zDfs%EbQ_|V!jP@!y-?abD@=+K1ri13i2{sHDa=5D;Bpn_phP+l^^a(?k!-ln#raHN z@aQmL!p2M!MnCMzb!`$?H@HT6KKzK30Cg{L&vU{6A`v2GN&*)d$tW2;{rpd04(P znr+zfQ54yS%gG!eqe$;+}9&=)P0P-{FE(#K-!7^GN`4Dp&=Y_$v?f8 zme7Zx&ncBss0IQLUMF9kQQ61T)((KPn4}qq@KgU2K_78AI@?#;l8)o-YhV2jzlG9h zSn>f}X}v2}Ydn*5mMD-YkSO3IW+H>Fa1f<;0;rXAj4TOGG@hl9D3B;n?-WQQf%kyO zFvhzBB5e=Im>D&*0kk0JH&rFPEXh<+s}$bQVrLB%||q_xizGnCgy-FMK2`Jvoq z1KP&41cL-8NEfnpb&h!G&(Wg@`+=x@YGfn|^p&#qN$&31qc)M!YG<1*S=y<(3~T%5 z9xp!*nb^f~LhvOLe#-ISX*?%Z14lrc#pt}S9rW50Pqil>-(n9vpj}v%6sZkMD|$hP zXytuFryj^Fg+zfwfqJF@qeBW+3PgsYv9dZ?Murj+HAsffVZMJu^+lbGzsud8o^rOV zfE#D6JdTza}*$9 zkZ8yQEEw!5yYv5SDT4A$2+B*j%bR7s`6+*>^myHQi~E6v5XYiUWF|a0)FG5BTZWfe zSM0Ulhu<>=n|TV`J}5vfkc7&qQL%*e1|cuoN>Y- zo$)dR=Z9g)7VpIvVvL_JS%UK&Mp|W|d|>cet&rKJw#a3Y>>)fKXUXH&L8k$o2$U09 z+eTThL9;{cAtBrU=4QM;$^^7;P)ibx4j53hW7e8|HhyGsOr132jODAAoBJxO*M0I^ z3+j$bN+k6GQEhw9?CGbr+KDHgXx)3Xe3M}zXaU27L30R7IP-Nq$9bC*O0GQ#um9W|SA3W8otRW~N~8!9#42dh<#X>)O-@Fl)O04;4T z_-*Ktv_Tz{tXy(YeS7>Q|C#Z-=`2woQJ{_}kU+z%17RG5i3uVZlQ3666L%u6t}8Q3 zlq_rYLh4`QNK=1^P5+25Z36k5{<6sYL;-}h^62DMDq z6S7>9l|*o~1--R=F^VFgHWM4w=3D{|0Z6Fy`S#nVa{$Y}9{@qDVTPqJ3wUd9XX&X) z&cOEAcfRv&`^!)MSKGC#TlZV_ocCnMlf6FuxdOo{fQ;PV6tuJu>WbmFYr2U(4gFc_GE zwji$ZRh^77V=E5&01g8O6fmfxnAqPx=w^@u14Eh(4v5hQ^EjTDwa{KHwu@0P7>9uv zW&s#LIskKMmuDy@&+L1^J$55xmWj(?(z!coc^M?%3KjvH=$4xFOxpD20on5lI8?;( z0VNgbOpsomRvG13$Sp2{js#_u`zKW%D+|roH3OG72T#lQ8>G=6=%Pn1Nm1-XAa9kQ zG`cmff%a0}wr^E5(k?)@Ty)UvU5<`QTFMDEPy%;qAmAp^{HVW!hfcPB%$VUmRst)A zj?N|<7}~DB(C+{MYlg}&j+EL(b9rNw(&}(!kLVecF)PCYd)Dbvk48CL8jwXyp|QuB zS_j-wQ*(=ST}Jz?*!rwBG;Wn|h27H4fQtvEN$&&l@aS$kCh26*uaq+O`qQE>*pl(6K z{?Zy#N0@n|Y}eQa0>IGG(QMnd@3O!9yT7xa{`9A=9e?447fK?~X#n;_ zdq*r4IFYDbNPuOFJ$rh5_RKrq`qsDXo8SDV$}ylFH&@&H-uFH`{q!@OnFY`iHctx9 zgZ8)f3*ddkA!3lY&xRJLcDjTFUI+4-Ff)ev?H@>}t#OOQ`cFLhpnd-b|7sgHKJJfC zT6>yZ{L)Kp-ASiw394dW`G+rC*HQsDnN;4h{w`V0jMxj#zR0e=>J3(DXbR53M)%mA zciry&Wyg-~cJ!)cT8b(wKbXDB?I3_kQGOl7RbUY7MO+&o09?K{4s?JJKKu zO-(IY9+98(F+f8Ki2{iNi2{=;Fx6-?IaRtdi2^VvOy?ez>|-CZ#h9&uI;Jb*n`6in7zxvg$T+iY3 z(@(eG|NZxC#(1m%h3xxft^reMm9<5qb}-}|$-{umJ`ana`s~=TOF*kt3{ZY8;D=R) zN)02!yAh5D>r=3hDQ4A)mL6&mtTIln?-*4%U`n1TQ|&9gZ9?9zSXY2`f34$TkQuw`WHzMkRlD+ukQGT{qwi}Ns^#WyXw_%vXf6b z%XaOM6v*`~C^J%#Oj_W}3u-(0a;VlP){DALs5_NZ{TyuM;;hcnd~opT*b5vY(7<-x z+qUlTKEm4*LX~b4?xu@Gfkc5iqd<~0>|=wooUmj`tE3H@|7qk!6pgc`0WA}(TD3~g zmN?^o-t(U4UjQ&{*|NpH_r34g+urszmmKu=_S$d!#>MvLH^0TD6Bl0iMf=Bp{70=T zx7p`E|9Jt8E(aw?9C3sL3=}KRI_oS43z+1^HTq|tebyxu01Uo&*=3gnCLjk<;L?tcH8Ygvny9#;fjVURxH=%W>>j|!q}-5d+?#V z?ZHQGv)8}oowi-T@Z10T4LkmXRrZdz|DJ$DhdXcDuI;-wZGOf}8_KTJSZ08O0|dZ? zLbYDuvijG-LUt9;W6W!~wAQ6`7m^Um{cJn+B+cG_vDc^+(Z1>fY`UW@jeC`-)17TDPPFZzIxK!K&90%nWZb+`)~XZoB02c9|8 z)jbmCm}g8s*Z9%L9?VpCaH0)zLvsn^M0zlB-uC;af z2SBfZWdL*t2BqZ2z7VXpbKWeiegFi=%9SfU9vsAX!>}(=aG!G|7#$rQo(G!s15$iM zXMv>%X4QNT32NY*HUI!SOi4sRR0ZE4J+nk45r6YHf8)}M>#x6Fx(uh=_1Awy%Ol4K zoS;}q-pqzk%sfm6{)`twn>{f>M8n?KXF_c)kGIo~vZOSL z0*L~PodRhD*vA$`DSzpbb}@vaW|4tOpa=<51}Lz^fV6=CV}pULSh2#FGPZ8r>PnF? zahS_9&phJ*17JWJzzFy8y$}Hic>);l*+AZa1>98g4ep(E(n+A2w+w`>9X; zzf#KV*0W*%CglLo94^5p+5!pafedSVOG(><N;C} zL<5#_=*OEkpY)=bie|$dZY`% zsnDoP29O4DVD+$W-8u&pEI}YyAOI9@x#bpLf&dtxldyjMde4V6++*ni2?WnNI)ax# zfCBjfMzF9z3c)vC@{)_}?6c3d`|i8XmM!aYP!Yik5{ zVHsdl+hb#0vHG~9<;Ho??z-z9ZBo@C6U;qg&+KxEB4k!3z=L}e<60GrU|_lmPI%5c z*?ayj?}r0W>W9ij+33U2t+T5=(9vH7DCB9GhKuRQp6(|K)C&a;pQ+~|ZZQ}yyh&qk zU{U(zH8Zk-Hvq^09AIiN3Yf&<5SS&eS<__yzI&^kAV)8$Fla&p64#h?-rC7f(L60`0YLW8f%Vci( zV}Xg!d;4zQyxAUo^l@uz?NI5Ol#g^Jdi$)sP1}5{xaul;CZ$iXgaVA7BeZL1SO6{U z?t7g`S`|S7poQQ9Fx~Y1PpFdwV#7lMxjj4W>~qctWC=;r-$a2#fkc4`6iA?90yz%c z#if^C>a+bDZ@ke>CNZ(R?A3plgkGg^*UqAiyb@qu*e8Rr; zr7!tX0}SDcE3UA&yyYzd9vxb`7;xaRVdE2?@52v2Y)7wL!HU1#=GA2 z9xaQ=1Emx#aY7XtMv#zX+~dGiciwVp)ehL5OXNMXe}}w69_5mh;M7VGfo;gy*h`9% z64+=7BQjXQgJNd3=wJa3JP|ao(lQgJD%zt=Dn^YCr1wNXc(O>?GYpk)@ zWxu%PCcE$6hqYf!F>RQ4*nA2`&JLK&1UZ=fv*8pjqgjez#>lLe`#6w-SuxNP zu+-o_QUlhbS(?DgB1%1AG5~~fc}OC0_Nm}IFidUFtxPbtB(OQsP2YLkPCoTmJMD$? zIEZLEV?L2t%;*tPc~oiQIUf2yfFKwqlKY?i_(^+e<8C|d*i*y=CFa-K{E&16Sf1&V zn`G+SB=b}D0KpR``+wM&wx7;P7RJpq19-q`7tf(T9zJPHKm9Mq`txUQp#Q`z?HY@iwYep{+QYs5{NCHvmR67j_Kng%;^QiuE zpNLu!FznKv6T7{AyJXVa+rM4;%Z0SOdIkwfaMH#0=(H{E1rQuVoDy4wB?^@t8kLHG z6ZsI4sHGrx+TM2rD24^5+O>uFo<05E&vtB=MaR%id(;2*{6H3}@4tlrIQCa@RJql4 zxZ{pHWS;vTJNMjk?TcUhVqMcBReo_(AaIezU55o*dbH620X;nd5U@!Rz8vuHfIb9@ zjtt`XPrzIg^Fc5i`zHY9cpqjDxF8<_>0x9519sBn-Eujs=>rb%QVEk`{{^nw+fkGZ zdB8--FXpvi3AYyog9ez60vyh}V6ENpt8d%sr=4ZJ-MHY^_Tw#WV(_7^i1>SY2ITg+ z#h!Y4i~a52eqIc5r+|YpK_O5~G#_)ns2TolZ5YM=4{TRGQW?@Rfq=;JCH9J!UuG}6 z`1MkR9Fl@#qcuqa(Ii@%+(2VWDGZ^VOx2D81O+kn0ku!F)&OY2u~F$@3U0`tc?w!`&y2ASfKms61 zy9WHPk0_wjh_dEI7rn&JKM${@OB^`3m4^BjW~xX~+%@@NukG%B&R+b|7X-in=0I80 zku>>nBZWkPM1e$sG=12QKEMoYst^Wkqyl(ig!!{J4x>iuz`A?z z+t)KX`uZU4`nCFf(mcBKw!?HMt^}4uo4&#)rvY2`da?U6TaXxd)nh)x!lrrdQ4EqC zoP_xp8c|b@s!*$lz!6*hDJPi^AwNzjFNM=kdtQ5OIvyQ2H9_^)4)$s& z_g_MuPd{a5%_VQf*fbJflG2X&a`9ooGaCS;LNv&Ma=PUYIUeFmd(lWO;8#8$8B_0) zXEPOooYx+14UxoL)9AqN?sys9sW{fI^G$(STwS$#xl2)MqFQ1G|VOwRNn(*AC!IL~#CA-f*!F!5;3%}pWqiu|0ugY>AG3h(PfXdIv z((HS1G*>UKL&3`MXS4|hiVZhILF$tulz6giH`@fJWNck!HGA*234e30P=wJW5@49j z5)BmLSkoe0xuk3Hrv$CyyF&D~cXofJ#f4gci&;k5cy| zHkOw&8j{Lrh0#%=-AP7!NIv;7lY;}4f7DNoeF{z`sXsY^sPMFJMCmMQs)}s$5E1m! zBDg5+ldwqri?hrSZ9pBX`$4a)t+KCyObcN_L<}3@PIfgth>7`?UexrjR<|iH zg5n3RR9`-G#HqS_L==ZtTqQ`2kF7V!^T|r#Nbo~!xI|G$njdGJhwBngi z7+Wk+lX+y88jl4GJkBENEh=VKd=?4)gV!0iY)>2Gu~r z?i0tOJicx>+zVT=jesm43?U?#$s{od3*DD0|$KR&s{yi*aV1;Crxm&`4oCE|$&1wZm%V0ej}B&votTuQ9M z^dj6oFM&1mUo&1aV?+}}zb+ar#Y`Kw?|Q~Hs&mGrhVZ_P4{;5N8L!te14)A@f$O2} z1AMPWsDd@h=&%;?9%aS)qSs}&`h|L=dF03d$(J&`98x7fOGEMgMu#QgFN*9Uvp_Iq zvcqIj0p9sNQWY~5mLc*twBhT zuyrcV6M85==0R3I5&s96s^YJnxq8`DB&P46K+0*8h`Fsuz+&*!S*qKoLhRoef~fK6 zUgV<#KGrGA^VtAdb5@HZq}#nD;w@fG9QAdC?M_aD|Ils1x5Zaw#cP#*uXb1}S82J(aX z5;3SX2BV-SGEV%xYqTGIq7qTjQ5IG8;p87)KYc;Wc9!+`MNxTW5>}pNdgtU^PO3_)k_Un)`pw!J*?Ycm_Bw2~M2FkNsU`U~-XgjJS9ykVkxP{Ii~9{G|o z$7{zHik1DxEl4$=Y+-NYl}5e+epGx}2=gA$;!J>YhyF2?e)8#Pm%@Ch>3UvsTo@ke zy5%3NWy`w+A%xKZtGJKB)4vgs;`_=8k^m7A`)-l%`z50lxpMlu^FU5&E&Do%R&Du6 zAjTDU4irq=>9=}-P)A`VhS zKrRkH0A);Imkb#M2=WZrpDJ>{KW=Kf(V5C!k3tCAeVHU?eJ|~u%~(yzs5Y4UUa%Vw zfx;cefq<-r!;rm5su1hO)0N{b}5Ot0CtD%!H1bOpj2e=`0xvyF&dEso+0z=HH4 z?|55Ty(L~uxAddh(KsP++EYtwz#kv=C5kH$RC^UCo2XQ3VMcA1Es7DA6w<009tq!E zt*qpxRt12VOQe0hw!^J+SGEh=@fq)3q_HSsJ=TY+J9W7t*Hb0L ziL1w~YrKC{=Ppy8(dCJ*w4FoR9;X}ipc*JBD0fgB14w>DuLy@QYe{tzJfdZ*R!hl!`mUy_CC@vzi(mNJMPWLCm-dsq?*BI1?DHF@H zA4=V)IQq@8c}}7S&*(LMBXujR1aj={LT+t@2TmNfwyxa<-srj--%dt-J^dBeBTS8Z?=jI0vjP`KmMV3`m<4L%4C@z$e0@$ zQq+CJ7nk8W86s?7lfVeUjQ3jxqVP2VrVt9k~hZ|$CzAUKvUz)e=(Xlt0f3dwt zBU3>zI+dv6suN1()?#0V1el@(0x`$ZoXGjaTLZVzzl)LQ zo+e?8KL=$p#FM@|F{)mdVxKsz1cx@W;G`(ps1ApBhi;=pDF-ABgdGUm_yxCpC|Q=g zN%YVU7mn}TquZUgwPqoh))4HNuDar8@i~7l9@X0OQ`gYkgD06T@{#)at>CyOr2=jo zwea=4`8B+gRw}mH{qvXu$O5z#&qQgtP9|e8RZ}u*Eq1@)%L{jFOeJ2@Pbefd^tZ|c zJaWBy_T3WjWkbkQzO$D~cnEW-6ZzE+UITCtGQ}*C)R(*Rye6G|Bt$E$N#caH)Nrvx zm_#HtGAnPZ;Kius@!s#&Z)xERx^Cc8ggoOkppi> zQQ32^9q4^X*ba+QlL_u7Z&OYd$2$|l?C`iTlDa+$tKjN7`Z}^>be=SMT5l#E{*lZp zq2)GBo|BC9>y&U`e;6A(W-0|Uded;+Hn`227eDOy7B9AObC%4(UPaeVQ8!G3Gn`i2 zHa-XQbl2`?j|Smvp#wi z+^-6++sKzFT6I*m?dYdC9!rs*!dyECO{SDunW=84{~)P^ zNZZ`0evlq^u$$~4RXy=>k48)gT2DMR+k&PT=D}cI*<|Ba_trY6;Nj0Rfx)7(25sWe zxSFpJ-66Ya_ccRzn-Ta13b;9KwFMbTvGacAM@m zK&m~{Ar=kl+@NN2UQwMilWG)f94-KY z+e0^4A0s1CaZAPaUs$mY3j#N(1`wK_q=_3!1ugd|H|B_#=rRY}uvR6yHIL={LeDW{9FZ7px{(|*Q zY)~n$F=sUK+Eh5>`2MfggZsOjc?vV_y1}pCzJOzHSNl?3Cy)k&G$Twu&-X-5(bTkT z#HsOOGJfWZhHD{zaW$4Rw}XF3 zaXOyJTPzU4i9l}w4rm^Fj2ZuK%P6LSbPyw^BH*HX`F?H5wEdAva+F~JJ(J^KV^QgE zQ4T+f*GWO)ihwu{nfkczR=;)R->~~3(9PM2bG>mP!h!E@fjun9)T^@H%C_&a>zskz|7+c<+f0kqvrQindiEBq z5<(k&ElR;lyZB5Y@sOn)+l%FP{wUrKb_;mUpq`=}@X6RToIZ8vypO{)dxw0te4lXA z&vg-)vf*};CF4fYJ?<%D^?h7Rso*+m$dr(P$_|b6P=00LFj&ar2a$ivBsKjM&_7pJ z%NfC_7wlml2pVq|(H^Bac0G`opJ9!={Fm=*L)Nu};IqA5Rg>ZGU6=9Zd6MFxh4~wj zjOBwJm?Q@J*++9M5J!OLBnY^!V+h)FpQM z?mqd53fY)nc?@_nKN`wIbzrk~IP$2wyLkUsgvd-u>MoUSV0lDr*IN|4ot`gDJe^r# zVNOskohZeI{-Doau(Pe}utH-5(dOvEIFY3{CS^7WC;IV;AKhFCw>;@=U*m+==i)G6 z-2ZmP=O}*7^IL_Gv-Z$u*P4fN!e5DEYdK7PH|As`*+!bh)S(eq!D0LdsgyG6)KiBm zKi}!KN1WThO%M#^)-J5Rgjm+aBr+7k((tqPFwK8$;gtRUb$)~Bg=aPU_)T&2G`>ii zvY&O;?yXH7pRM)N2F#CSNpU+0Oh<7x8DmW*9C2lFJ6d$*+S(xY{!~c9O-fLC zn^}Ui|8Bs?P?f-L7k#?Ofbp1ogwM!p<3e|i#psCiZ%rj<&&uU!waE3}+ogKj?}oPa z_hGPXp%L&hJhuD&{va&ksm~3^k#YC?nKH+fZdvxC;C3xG`e3Ez=AFMzSW z`uT}y^pVy78h5B4tBmh>irKw27AR6W2<)v#!JC&9IceDhMckU6mrQ5D0U00+x7qb? zt#F?z0fa-@H?><;gB_;=)U+xdCiC{aF%mjr5PNFr*b7RTKkLTL@$1#XY3NnL8Nt)9 zSAOUgM?%YlCU}SjDDSy^OnWSndbB2-2owuz7MALmNQt;3!gsT1?$I|0&DZMkx3zM= z5w>1(7dAijKOIqCI?zqfqVknB0ftlCNNItT)KAxAMe@R=R<_Wud{B8mx#` zU(5MAN$3T+qWJI2r$nXn2nT4ygD&pN`6ti8@)Dm5QGKo{E*S4O#TP-{+D50cyC*F~)AITby4f?o^ zeFZLN+1mSogfWBR#4+4S-4bXKYA#2a*HO`1gk0E*OpuX$b@{QuK`5u5&)BENt=gFh zVU&e_Bg~PlB~AK%S$4|SNF5#)wA!6$Ex_-d0!)~_+|u-K8b@EOgVpn8!|B2|h8*jd zP|Mi`^?LGu@pdP=(x;zl%I){+?ZxFe4xSB&jGW!&ir5d&LK@ z4Ky>j=1ad6%k#Ub^8oENNc_zxMmHbZ|H`^gEov4aK>7X@!2fMgY?vrV?tEW5ifEd8 z%g2;dKTy?u*A=dx%P|(r{gx!2wX~~+D+r5Pj!2WP2OCEg9|6<-^rZ26mQ4pg)K`w- zjHu*`hRQbw>$1JNGv9q`eW50HBiG*(0b>M{19&5Dr@S7Z>oV+>V(iTWy{`|zja6D^; z1|)HVQug!h;kr{!)y=E&Uoy|l9|o#`R@vZS0(%E$Igi|N5>mnC^18f;q~Ju(zuzgh z2l9aUoJl#fz+RdT5SMc6$A(VhVS_2J&&h;u2Z%kD_ph&U42z60T!yl2v;b*+%*h6(RV@mjnzNx8rcE$k5;8q+; zH`eM}22COR8;p_KToc?Nb!IHc=dX&GxpZH4c4l)!zH#yJ@#&d}y+eHjp~xJ@i6ZXn zGIg_qPOmQ4C0pewrKW;^RD)65*^&{t(BiEwQ4OnS&Km;ma<0?HpF+V&03uyYiG!Ld zPOWrmi*+z|{f7mvv~Qw&IT4a%?NY^XfhmD&LNB|*i#ob-NZTO zvxXf0kS67I_d|zkHeB%GqMyqcI4D)zEwK@YVNfo#*Vw2ZSNQ(>b;6!gR$x!hd%g+_ zFvC~|?EwYW!iOy4Hi}JE`pmyA%Qz`gXS55`OZKb|pIAib>!Kxd-r(OC*$CJ?f`wd- zlG&ub48uHz6#lk+tuZV!7UOerAe^DtHyY{xuFu=jSGHoa+t%&!l<+n#o8Yt4!8 zEn1mvDAdD6ljoY}b*U<(=9X8Id)O17BO6dQ+ZCME><@V%n$}wk`T!BSR-HiQw2!iI9>^VJJPxDW80(k^>l{6 z%~I2Gz4rGn*p6#-&=7h3N%I&P-syDRY{SXxa~_j%IrNCq`&P0F$Ua_$`xcJ<(Hv<` z9rw5a!QPSUNLO1*j^a;D1GFPYRpAe)@EY37$HIojVd#DR0$qi})9cKJ7b&v#DK5i`>p3bO}4AY2Ol^3Zdd!=$ePq1FL^8l_%EFxQ--h~aW|&OZzD+QCOlLlR!K9XCdov|=&6K5^k|-eF>`oj`hgE9@ zZOa-c>gLDk*zFr4=reSL*`n&CV8s{av1N#83&Ncob%46SR9GoCaUn_`W-0@SBt1jn z2QUyp;A4DqM@7YC>;3QvwKwE>3J(PF48vyF)Wc20H!x6~Jsd#f+DzP7f4*BYX&{hg ztr5Fjzqh=aVCx?dZCn2_eGFe_$KAG_oN=5!XTn-E^wGfWh|6d5O&8uM10wE;!E>s} z-z^#tksmL|j5*)aQqzauW!l$yVt06lhu>`Wd$)@IqB=BQWp-AIaD_Oaas6KIQq#;! zAFVzkA~CzD%uiMh%gWY2I(rl-3&VeNS~zl72v-bO3n$-Z;Vx1X=)L_xPzE~)JKtR_ z$+WKQowNSQ=3R4*Iz+>V9x|*dSwKb);GZvKk|QVSZ(nfCH&BTG#l}iB)j98+GV<5W z_gB4Z=n(mR*YgPS2iHQqaRm)TA}7U&mA1le zU8sWK6gjVT@(O&;KYB-SR3)3aAYRgN>^Ips-A1VTP_W5PeyZ*>~H1X+`1GfH3S; zWzc?_jgI3uu1#qZq3V?~K76rM_j?V<*(V(hEd({wwA_3;6k;g9rkgRlEqE3sc+mv- zQ>q|SB<~A{KJjn(OmY@VK@lNje0@7g3Q3+nTVRgkYT8^xPz81z`x-GR)Id?Tpb*7U z=ugNS_g{yrEOJwH$nxiZC_kgQ5}-9|ttFV#J5c@97#MP@-Xlu-2oi+6BTD?;A9p-| z72U~G`pDi&`_rZlURb#9pyYSAyP$*U2u%#q@S|*#r3gFa0A4#G$#k26O@THb?6aRg zVRwk)YAlQ^D5PFLC#Ox>Q%AzW-&Uy4GQf}$)>J$W65x2y8SAzZeFiiZE;Nh7!_o9V zt0B>jwNl&It68oEYAECLOoHj=@mlje)+-nw3IbR}56l!(-rH(>vYw+ohjZ@;jG+I3yyLic<$|htr?( z@|4phxI5(jrUp=9#TA%yEhQ!)#wOhj3%SKmMC22RDY6tanh#>C3Qe#_N=VhM5+kF{ zd?V4aoWk0Sl^j9O6V**0TGli$qj9#)HkHuuMpNh@R>!03oSPp$9!wSMj;H+YtGhnpHn=KZND({R*|Y60RPWwe>g=c= z{*gS>cJpD{1IbmDtu$#o%{r3?wZD4BYxns4*91**#~=_*r68HjsM0bqD_&0Pp=~Do zd~1`^Pdq;m3$+JLTLXj)ME2E1*jGvv^^|#3CHL(H1+60Jmg|z#(2eM4zMDuseQDe) zfkVi*zI`3TgF|`=`4LUcHmuG%D7CO%ED6Q1`_8-rW&883PT`yDz!zjBWOy{&W=d0l zLiBi1IRV^AglHq|UgApAP!m}z2~`l@>sTngVGCWF%;OX)iBQ8Q76O?^17I0{!#`^-TpxX- z_>Xn;MC-=>N)l2ONvX`({f}TPTLR+V;_kd`o2A$n(XZ=;V6`q{?cM7BB{p6%apZA| zD(*?VWiHJ=!SR`l7P7@4m&ekV*w6ClC;dbMy}v86n5Xqabe_mQkudQ2v40J_2^gxN zcmcF>9@f6DJCFlLC|a~kvOVKG&Mq`gA>H-zE>~1kXheUA%P+*|9@H;y@Cq%bdwUe6 z)2SC95>?T4!`60_EqlD>R&u~U&Fi?D|EsD{h?9R$$p;dGtrk>Bz(hJE|7?_=&)P+* zXNAKkIW@Vw$Vf7TE9rJj5!WEgx#y(TW#@DzPF_|c4y4>%P*$A}Xg&Ib3`uJZo@=^O z5&Y(ZR%pa_*ojUHg1ZakXb47#KM@P-cXK78F=cwLx=8BVPCS=qvwX`wpZ?_paLeG} z?&u}5Bk7><2R#^4;41tPcCiYL&G{+%?_;`kId6}40y{<8fyw00Hj*1080?0T$ zTk?~SdQd%^7nFIH6MNp3Pu0p?AbsA zpClzR&%DO%8*?6cnjOiLe^AvO&(50412iooVG9DzO}sj{BV7;ZdZ?FeYyk1X(zI4~ zYnubigh5|ida^p4XkyGEu~t3Hrh@|g;2%z`hZjNJ@+rChVt+^V)X)3dEHoRQk zD$dlaT$LTnV6yl(abFdVz?LUD=s+&KgKKIeG!7j2f(Xu6x2zHFArqF!JlhAv|h>zMF3?@MxxHIPt3?WyN}g zM&wZSg@BT9jvka!0X6P}G%Dz{_jbY9Cj^5RzxW6pBaw$fq zdPE(jyx2m=rIK{`8zx7Sp1>}`VC1@$Aei7rNx7+FU%G3fXxg01+{h;8&N!F0GUQDek?W?#TPeY;+Uw!1tnue+AY+x zz=d!+yx}-l{Nj9?2qfTu28D1SN5%w)-)18Vpj&-bDsaZ0=MxwzY#IST{s|rrFIv!z zt2Y%_snE9^fKq7BE-_@qC{(R@jVoUdGb0db#h`V|dEw;3*x(UbsyK|+HdQ><_3Mkz z&jG))AaR(~do(M0ZA8wu@^fy!Hv{($bFoxHR@?Gs@rprDpdX%`k`KK6y~rVq6hw`x zCoQo(Y5qt6+5sXuO;#O#Da(bRUA?1|VwbtUPa*s}Lds-xM8dHEhJbO{Pp#9Pt_+c! zTTwSgUCm(k-NX$0a(*XQL`ouiqUw_Dj=4&&X~bFj%_s?53tm1NPE=RSdb3Dr9>g5D zjs1VfE_E(I5aM(N;a38Zyrunk7E|I$|Muor)!O^u$6t--rEgA5sMj0q4*BlBg-ClOU+tp00M!skzg%UV9 z7@b|Aj4Drhb;rOhrDwPjl{x^e)I@MVWkwF3o0EHit3|{kMKNA8TJ6AySR_nmS?d9s zWC`%4Xl~U8A6M-%vKQVPISjTmNV(LxJ85|Nr5u2OKDkAjKQJPcYwCV;s?4UQCs2;f5AL^HYEBzTv@p1wf9s3wZz{Cyd~9Vc8bn zsmL4CIU5+o_5E2;Cf%a?)CA~7eAiu%TmLmwqNVJ^CwtUHnSbusoYJ_1uFy)8m+Wwt zLyjFBV}k<4CiceUFC~=la@1-?2A{~( zL=|<>tNqIanL04qYPZ6Rqnk&}Sg4LLA!g1c#;+J_9>)WTEVoF!sF_Xuosl5XQ1+nf zbbose@+*uYa-Zvk5{JV^R`-E4h6QFeA<@zhe&QrANZ_l-ggG=nO@K@;%)TWz3aVka z`;t!|6A~};hdYwF0AGyCeb7G?vLM{Tl&-sIO9&|6%~?AByb^%`Yni?3tH#FjGkt_j`Qiu7^*+wvXfc-pQxb*hGobr8~Ux zQ0(FEP38$$cZz{wT3d z^3l?Zm_<1;#UN6WK4>s7Om0tiOalqYjVMYB(JlML6{hy z94hi9)8)_*5VZ!sCr)Cv?m5Y$r*^}XSsn0nu`M-*UNUZEoKvDVR~sqxz#7wap`s^$);W!~BmQonA9c zA(v4iIx+%8NX+lSl$;9|hh>Q%CfZ2^8vk9tqGRn=;wf4c9&t)4kq4#BX>hqEMkd^5L9)PdcCPbYoGc(;})2J*5s!a{18SR?x4UImZ{u^>qx zw~}};CJ^%`EDjcqLcK$7jriGZ{*?c_<2yFtcd;p`=qQ~2FysTRMA^}(QFi`Zy$2ps zJ0;S9npd({#fK_@IDTme?w79I&-e#fAo(e^QxVGF@B)syT42MqQb3*{bMZRII0{iJ zu?su;*pcTKy*l?FTBa)$eu&pmM0^d9Q`PNS`eIb1XeyuDZEly?t0`CdcptX__D-RX zWzgzPMfOB)?uAj#fKh0kK@SWEi)-<%#z8DHtc0>c4k$cnuyiOms*c0REGgQIW2cG!6Pczx32F literal 0 HcmV?d00001 From 337ccc058fdeaa8a9dbd56c0b47344952badeb33 Mon Sep 17 00:00:00 2001 From: Niklas Martin <55481472+niklasmartin@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:47:05 +0200 Subject: [PATCH 172/287] [docs]: fix type hint for state (#1528) --- examples/reflection/reflection.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/reflection/reflection.ipynb b/examples/reflection/reflection.ipynb index 253dfd075..494bb4aca 100644 --- a/examples/reflection/reflection.ipynb +++ b/examples/reflection/reflection.ipynb @@ -44,7 +44,7 @@ "metadata": {}, "outputs": [ { - "name": "stdin", + "name": "stdout", "output_type": "stream", "text": [ "LANGCHAIN_API_KEY ········\n", @@ -348,7 +348,7 @@ "builder.add_edge(START, \"generate\")\n", "\n", "\n", - "def should_continue(state: List[BaseMessage]):\n", + "def should_continue(state: State):\n", " if len(state[\"messages\"]) > 6:\n", " # End after 3 iterations\n", " return END\n", From 883b39ba9f7270ae2cce9058070f5e622bf94603 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Thu, 29 Aug 2024 06:50:01 -0700 Subject: [PATCH 173/287] docs: better name for react agent w/ structured output how-to (#1527) --- docs/mkdocs.yml | 2 +- examples/react-agent-structured-output.ipynb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 779a804c4..eb6d5d36f 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -171,7 +171,7 @@ nav: - Visualize your graph: how-tos/visualization.ipynb - Add runtime configuration: how-tos/configuration.ipynb - Add node retries: how-tos/node-retries.ipynb - - How to force function calling agent to structure output: how-tos/react-agent-structured-output.ipynb + - How to return structured output from a ReAct agent: how-tos/react-agent-structured-output.ipynb - Prebuilt ReAct Agent: - Create a ReAct agent: how-tos/create-react-agent.ipynb - Add memory to a ReAct agent: how-tos/create-react-agent-memory.ipynb diff --git a/examples/react-agent-structured-output.ipynb b/examples/react-agent-structured-output.ipynb index 6ffd9775f..6bad5ad75 100644 --- a/examples/react-agent-structured-output.ipynb +++ b/examples/react-agent-structured-output.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# How to force function calling agent to structure output\n", + "# How to return structured output with a ReAct style agent\n", "\n", "You might want your agent to return its output in a structured format. For example, if the output of the agent is used by some other downstream software, you may want the output to be in the same structured format every time the agent is invoked to ensure consistency.\n", "\n", @@ -334,7 +334,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -348,9 +348,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.11.1" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } From cd287992159bbed1039d8eccbea0bdf7560bcda4 Mon Sep 17 00:00:00 2001 From: gbaian10 <34255899+gbaian10@users.noreply.github.com> Date: Thu, 29 Aug 2024 21:57:23 +0800 Subject: [PATCH 174/287] fix: ToolNode output isn't a str and contains non-ASCII characters (#1505) --- libs/langgraph/langgraph/prebuilt/tool_node.py | 2 +- libs/langgraph/tests/test_prebuilt.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/prebuilt/tool_node.py b/libs/langgraph/langgraph/prebuilt/tool_node.py index ed7b69553..05ac1d8e0 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_node.py +++ b/libs/langgraph/langgraph/prebuilt/tool_node.py @@ -34,7 +34,7 @@ def str_output(output: Any) -> str: return output else: try: - return json.dumps(output) + return json.dumps(output, ensure_ascii=False) except Exception: return str(output) diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index ba9c88afc..a09adf683 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -1,3 +1,4 @@ +import json from typing import Annotated, Any, Callable, Dict, List, Optional, Sequence, Type, Union import pytest @@ -9,6 +10,7 @@ from langchain_core.messages import ( BaseMessage, HumanMessage, SystemMessage, + ToolCall, ToolMessage, ) from langchain_core.outputs import ChatGeneration, ChatResult @@ -499,3 +501,18 @@ def test_tool_node_inject_state() -> None: result = node.invoke([msg]) tool_message = result[-1] assert tool_message.content == "hi?" + + +def test_tool_node_ensure_utf8() -> None: + @dec_tool + def get_day_list(days: list[str]) -> list[str]: + """choose days""" + return days + + data = ["星期一", "水曜日", "목요일", "Friday"] + tools = [get_day_list] + tool_calls = [ToolCall(name=get_day_list.name, args={"days": data}, id="test_id")] + outputs: list[ToolMessage] = ToolNode(tools).invoke( + [AIMessage(content="", tool_calls=tool_calls)] + ) + assert outputs[0].content == json.dumps(data, ensure_ascii=False) From ae8b1e164b9dbad8ebf7c1c2675e9f9859860589 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Thu, 29 Aug 2024 08:03:08 -0700 Subject: [PATCH 175/287] docs: expand streaming conceptual guides (#1524) * stash * cr * x * spelling * Update docs/docs/cloud/how-tos/stream_events.md Co-authored-by: Vadym Barda * Update docs/docs/cloud/how-tos/stream_updates.md Co-authored-by: Vadym Barda * Update docs/docs/cloud/how-tos/stream_values.md Co-authored-by: Vadym Barda * Update docs/docs/concepts/low_level.md Co-authored-by: Vadym Barda * final nits * spelling --------- Co-authored-by: isaac hershenson Co-authored-by: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Co-authored-by: Vadym Barda --- docs/docs/cloud/concepts/api.md | 98 +++++++++++++++- docs/docs/cloud/how-tos/stream_debug.md | 122 ++++++++++--------- docs/docs/cloud/how-tos/stream_events.md | 3 +- docs/docs/cloud/how-tos/stream_messages.md | 30 ++--- docs/docs/cloud/how-tos/stream_updates.md | 9 +- docs/docs/cloud/how-tos/stream_values.md | 9 +- docs/docs/concepts/low_level.md | 129 +++++++++++++++++++-- docs/docs/static/values_vs_updates.png | Bin 0 -> 37691 bytes 8 files changed, 304 insertions(+), 96 deletions(-) create mode 100644 docs/docs/static/values_vs_updates.png diff --git a/docs/docs/cloud/concepts/api.md b/docs/docs/cloud/concepts/api.md index e18f52c55..e81c672f3 100644 --- a/docs/docs/cloud/concepts/api.md +++ b/docs/docs/cloud/concepts/api.md @@ -51,16 +51,108 @@ The LangGraph Cloud API offers several features to support complex agent archite Streaming is critical for making LLM applications feel responsive to end users. When creating a streaming run, the streaming mode determines what data is streamed back to the API client. The LangGraph Cloud API supports five streaming modes. -- `values`: Stream the full state of the graph after each node is executed. See the [how-to guide](../how-tos/stream_values.md) for streaming values. +- `values`: Stream the full state of the graph after each [super-step](https://langchain-ai.github.io/langgraph/concepts/low_level/#graphs) is executed. See the [how-to guide](../how-tos/stream_values.md) for streaming values. - `messages`: Stream complete messages (at the end of node execution) as well as tokens for any messages generated inside a node. This mode is primarily meant for powering chat applications. This is only an option if your graph contains a `messages` key. See the [how-to guide](../how-tos/stream_messages.md) for streaming messages. - `updates`: Streams updates to the state of the graph after each node is executed. See the [how-to guide](../how-tos/stream_updates.md) for streaming updates. -- `events`: Stream all events (including the state of the graph) after each node is executed. See the [how-to guide](../how-tos/stream_events.md) for streaming events. This can be used to do token-by-token streaming for LLMs. -- `debug`: Stream debug events after each node is executed. See the [how-to guide](../how-tos/stream_debug.md) for streaming debug events. +- `events`: Stream all events (including the state of the graph) that occur during graph execution. See the [how-to guide](../how-tos/stream_events.md) for streaming events. This can be used to do token-by-token streaming for LLMs. +- `debug`: Stream debug events throughout graph execution. See the [how-to guide](../how-tos/stream_debug.md) for streaming debug events. You can also specify multiple streaming modes at the same time. See the [how-to guide](../how-tos/stream_multiple.md) for configuring multiple streaming modes at the same time. See the [API reference](../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/stream) for how to create streaming runs. +Streaming modes `values`, `updates`, and `debug` are very similar to modes available in the LangGraph library - for a deeper conceptual explanation of those, you can see the LangGraph library documentation [here](../../concepts/low_level.md#streaming). + +Streaming mode `events` is the same as using `.astream_events` in the LangGraph library - for a deeper conceptual explanation of this, you can see the LangGraph library documentation [here](../../concepts/low_level.md#streaming). + +#### `mode="messages"` +Streaming mode `messages` is a new streaming mode, currently only available in the API. What does this mode enable? + +This mode is focused on streaming back messages. It currently assumes that you have a `messages` key in your graph that is a list of messages. Assuming we have a simple react agent deployed, what does this stream look like? + +All events emitted have two attributes: + +- `event`: This is the name of the event +- `data`: This is data associated with the event + +Let's run it on a question that should trigger a tool call: + +```python +thread = await client.threads.create() +input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]} + +events = [] +async for event in client.runs.stream( + thread["thread_id"], + assistant_id="agent", # This may need to change depending on the graph you deployed + input=input, + stream_mode="messages", +): + print(event.event) +``` +```shell +metadata +messages/complete +messages/metadata +messages/partial +... +messages/partial +messages/complete +messages/complete +messages/metadata +messages/partial +... +messages/partial +messages/complete +end +``` + +We first get some `metadata` - this is metadata about the run. + +```python +StreamPart(event='metadata', data={'run_id': '1ef657cf-ae55-6f65-97d4-f4ed1dbdabc6'}) +``` + +We then get a `messages/complete` event - this a fully formed message getting emitted. In this case, +this was the just the input message we sent in. + +```python +StreamPart(event='messages/complete', data=[{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '833c09a3-bb19-46c9-81d9-1e5954ec5f92', 'example': False}]) +``` + +We then get a `messages/metadata` - this is just letting us know that a new message is starting. + +```python +StreamPart(event='messages/metadata', data={'run-985c0f14-9f43-40d4-a505-4637fc58e333': {'metadata': {'created_by': 'system', 'run_id': '1ef657de-7594-66df-8eb2-31518e4a1ee2', 'graph_id': 'agent', 'thread_id': 'c178eab5-e293-423c-8e7d-1d113ffe7cd9', 'model_name': 'openai', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'langgraph_step': 1, 'langgraph_node': 'agent', 'langgraph_triggers': ['start:agent'], 'langgraph_task_idx': 0, 'ls_provider': 'openai', 'ls_model_name': 'gpt-4o', 'ls_model_type': 'chat', 'ls_temperature': 0.0}}}) +``` + +We then get a BUNCH of `messages/partial` events - these are the individual tokens from the LLM! In the case below, we can see the START of a tool call. + +```python +StreamPart(event='messages/partial', data=[{'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs', 'function': {'arguments': '', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-985c0f14-9f43-40d4-a505-4637fc58e333', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [{'name': 'tavily_search_results_json', 'args': '', 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs', 'error': None}], 'usage_metadata': None}]) +``` + +After that, we get a `messages/complete` event - this is the AIMessage finishing. It's now a complete tool call: + +```python +StreamPart(event='messages/complete', data=[{'content': '', 'additional_kwargs': {'tool_calls': [{'index': 0, 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs', 'function': {'arguments': '{"query":"current weather in San Francisco"}', 'name': 'tavily_search_results_json'}, 'type': 'function'}]}, 'response_metadata': {'finish_reason': 'tool_calls', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-985c0f14-9f43-40d4-a505-4637fc58e333', 'example': False, 'tool_calls': [{'name': 'tavily_search_results_json', 'args': {'query': 'current weather in San Francisco'}, 'id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs'}], 'invalid_tool_calls': [], 'usage_metadata': None}]) +``` + +After that, we get ANOTHER `messages/complete` event. This is a tool message - our agent has called a tool, gotten a response, and now inserting it into the state in the form of a tool message. + +```python +StreamPart(event='messages/complete', data=[{'content': '[{"url": "https://www.weatherapi.com/", "content": "{\'location\': {\'name\': \'San Francisco\', \'region\': \'California\', \'country\': \'United States of America\', \'lat\': 37.78, \'lon\': -122.42, \'tz_id\': \'America/Los_Angeles\', \'localtime_epoch\': 1724877689, \'localtime\': \'2024-08-28 13:41\'}, \'current\': {\'last_updated_epoch\': 1724877000, \'last_updated\': \'2024-08-28 13:30\', \'temp_c\': 23.3, \'temp_f\': 73.9, \'is_day\': 1, \'condition\': {\'text\': \'Partly cloudy\', \'icon\': \'//cdn.weatherapi.com/weather/64x64/day/116.png\', \'code\': 1003}, \'wind_mph\': 15.0, \'wind_kph\': 24.1, \'wind_degree\': 310, \'wind_dir\': \'NW\', \'pressure_mb\': 1014.0, \'pressure_in\': 29.93, \'precip_mm\': 0.0, \'precip_in\': 0.0, \'humidity\': 57, \'cloud\': 25, \'feelslike_c\': 25.0, \'feelslike_f\': 77.1, \'windchill_c\': 20.9, \'windchill_f\': 69.6, \'heatindex_c\': 23.3, \'heatindex_f\': 74.0, \'dewpoint_c\': 12.9, \'dewpoint_f\': 55.2, \'vis_km\': 16.0, \'vis_miles\': 9.0, \'uv\': 6.0, \'gust_mph\': 19.5, \'gust_kph\': 31.3}}"}]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'tavily_search_results_json', 'id': '0112eba5-7660-4375-9f24-c7a1d6777b97', 'tool_call_id': 'call_w8Hr8dHGuZCPgRfd5FqRBArs'}]) +``` + +After that, we see the agent doing another LLM call and streaming back a response. We then get an `end` event: + +```python +StreamPart(event='end', data=None) +``` + +And that's it! This is more focused streaming mode specifically focused on streaming back messages. See this [how-to guide](../how-tos/stream_messages.md) for more information. + + ### Human-in-the-Loop There are many occasions where the graph cannot run completely autonomously. For instance, the user might need to input some additional arguments to a function call, or select the next edge for the graph to continue on. In these instances, we need to insert some human in the loop interaction, which you can learn about in the [human in the loop how-tos](../how-tos/index.md#human-in-the-loop). diff --git a/docs/docs/cloud/how-tos/stream_debug.md b/docs/docs/cloud/how-tos/stream_debug.md index e580286ca..fe3245947 100644 --- a/docs/docs/cloud/how-tos/stream_debug.md +++ b/docs/docs/cloud/how-tos/stream_debug.md @@ -1,6 +1,10 @@ # How to stream debug events -This guide covers how to stream debug events from your graph (`stream_mode="debug"`). +This guide covers how to stream debug events from your graph (`stream_mode="debug"`). Streaming debug events produces responses containing `type` and `timestamp` keys. Debug events correspond to different steps in the graph's execution, and there are three different types of steps that will get streamed back to you: + +- `checkpoint`: These events will get streamed anytime the graph saves its state, which occurs after every super-step. Read more about checkpoints [here](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer) +- `task`: These events will get streamed before each super-step, and will contain information about a single task. Each super-step works by executing a list of tasks, where each task is scoped to a specific node and input. Below we will discuss the format of these tasks in more detail. +- `task_result`: After each `task` event, you will see a corresponding `task_result` event which as the name suggests contains information on the results of the task executed in the super-step. Scroll more to learn about the exact structure of these events. First let's set up our client and thread: @@ -34,7 +38,6 @@ Output: 'updated_at': '2024-06-21T22:10:27.696862+00:00', 'metadata': {}} -Streaming debug events produces responses containing `type` and `timestamp` keys. Debug events correspond to different steps in the graph's execution (e.g. `task`, `task_result`, `checkpoint`). === "Python" @@ -94,68 +97,79 @@ Streaming debug events produces responses containing `type` and `timestamp` keys Output: Receiving new event of type: metadata... - {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'} - - - + {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'} + + + Receiving new event of type: debug... - {'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.256850+00:00', 'step': -1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a2e-6bb6-bfff-8423bcf47561', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'messages': []}, 'metadata': {'source': 'input', 'step': -1, 'writes': {'messages': [{'role': 'human', 'content': "What's the weather in SF?"}]}}}} - - - + {'type': 'checkpoint', 'timestamp': '2024-08-28T23:16:28.134680+00:00', 'step': -1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'checkpoint_id': '1ef65938-d8f3-6b25-bfff-30a8ed6460bd', 'checkpoint_ns': ''}, 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'}, 'values': {'messages': [], 'search_results': []}, 'metadata': {'source': 'input', 'writes': {'messages': [{'role': 'human', 'content': "What's the weather in SF?"}]}, 'step': -1}, 'next': ['__start__'], 'tasks': [{'id': 'b40d2c90-dc1e-52db-82d6-08751b769c55', 'name': '__start__', 'interrupts': []}]}} + + + Receiving new event of type: debug... - {'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.259723+00:00', 'step': 0, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a35-6c86-8000-f4a85315dbeb', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}]}, 'metadata': {'source': 'loop', 'step': 0, 'writes': None}}} - - - + {'type': 'checkpoint', 'timestamp': '2024-08-28T23:16:28.139821+00:00', 'step': 0, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'checkpoint_id': '1ef65938-d900-63f1-8000-70fe53e0da5c', 'checkpoint_ns': ''}, 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'}, 'values': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}], 'search_results': []}, 'metadata': {'source': 'loop', 'writes': None, 'step': 0}, 'next': ['call_model'], 'tasks': [{'id': '685d89f6-542b-5e11-8cff-2963e7f4ea63', 'name': 'call_model', 'interrupts': []}]}} + + + Receiving new event of type: debug... - {'type': 'task', 'timestamp': '2024-06-21T22:11:09.260021+00:00', 'step': 1, 'payload': {'id': '12ab1026-a551-5f96-9ad3-43424f094774', 'name': 'agent', 'input': {'some_bytes': None, 'some_byte_array': None, 'dict_with_bytes': None, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}], 'sleep': None}, 'triggers': ['start:agent']}} - - - + {'type': 'task', 'timestamp': '2024-08-28T23:16:28.139928+00:00', 'step': 1, 'payload': {'id': '600a6ff3-7ff1-570a-b626-f887e9a70f1c', 'name': 'call_model', 'input': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}], 'search_results': [], 'final_answer': None}, 'triggers': ['start:call_model']}} + + + Receiving new event of type: debug... - {'type': 'task_result', 'timestamp': '2024-06-21T22:11:09.267632+00:00', 'step': 1, 'payload': {'id': '12ab1026-a551-5f96-9ad3-43424f094774', 'name': 'agent', 'result': [['some_bytes', 'c29tZV9ieXRlcw=='], ['some_byte_array', 'c29tZV9ieXRlX2FycmF5'], ['dict_with_bytes', {'more_bytes': 'bW9yZV9ieXRlcw=='}], ['messages', [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]]]}} - - - + {'type': 'task_result', 'timestamp': '2024-08-28T23:16:28.584833+00:00', 'step': 1, 'payload': {'id': '600a6ff3-7ff1-570a-b626-f887e9a70f1c', 'name': 'call_model', 'error': None, 'result': [['messages', {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]], 'interrupts': []}} + + + Receiving new event of type: debug... - {'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.268469+00:00', 'step': 1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a4b-60ae-8001-dd378f965bf7', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}, 'metadata': {'source': 'loop', 'step': 1, 'writes': {'agent': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}}}} - - - + {'type': 'checkpoint', 'timestamp': '2024-08-28T23:16:28.584991+00:00', 'step': 1, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'checkpoint_id': '1ef65938-dd3f-616f-8001-ce1c6f31e130', 'checkpoint_ns': ''}, 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'}, 'values': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': []}, 'metadata': {'source': 'loop', 'writes': {'call_model': {'messages': {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}}}, 'step': 1}, 'next': ['exa_search', 'tavily_search'], 'tasks': [{'id': '43865935-be38-5f6e-8d38-d44ef369c278', 'name': 'exa_search', 'interrupts': []}, {'id': 'dc220677-2720-56c7-a524-caaff60fce2c', 'name': 'tavily_search', 'interrupts': []}]}} + + + Receiving new event of type: debug... - {'type': 'task', 'timestamp': '2024-06-21T22:11:09.268659+00:00', 'step': 2, 'payload': {'id': '494ad427-fe8d-5654-91e6-50495a2699f5', 'name': 'tool', 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'sleep': None}, 'triggers': ['branch:agent:should_continue:tool']}} - - - + {'type': 'task', 'timestamp': '2024-08-28T23:16:28.585219+00:00', 'step': 2, 'payload': {'id': '870b5854-2f84-533d-8e7d-87158ee948fc', 'name': 'exa_search', 'input': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': [], 'final_answer': None}, 'triggers': ['call_model']}} + + + Receiving new event of type: debug... - {'type': 'task_result', 'timestamp': '2024-06-21T22:11:09.272916+00:00', 'step': 2, 'payload': {'id': '494ad427-fe8d-5654-91e6-50495a2699f5', 'name': 'tool', 'result': [['messages', [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}]]]}} - - - + {'type': 'task', 'timestamp': '2024-08-28T23:16:28.585219+00:00', 'step': 2, 'payload': {'id': '7589abfc-04df-58c6-8835-be172f84a7ff', 'name': 'tavily_search', 'input': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': [], 'final_answer': None}, 'triggers': ['call_model']}} + + + Receiving new event of type: debug... - {'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.273113+00:00', 'step': 2, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a56-6832-8002-8ab17e662980', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}]}, 'metadata': {'source': 'loop', 'step': 2, 'writes': {'tool': {'messages': [{'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}]}}}}} - - - + {'type': 'task_result', 'timestamp': '2024-08-28T23:16:32.422243+00:00', 'step': 2, 'payload': {'id': '7589abfc-04df-58c6-8835-be172f84a7ff', 'name': 'tavily_search', 'error': None, 'result': [['search_results', ["{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1724886988, 'localtime': '2024-08-28 16:16'}, 'current': {'last_updated_epoch': 1724886900, 'last_updated': '2024-08-28 16:15', 'temp_c': 22.2, 'temp_f': 72.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 16.1, 'wind_kph': 25.9, 'wind_degree': 300, 'wind_dir': 'WNW', 'pressure_mb': 1013.0, 'pressure_in': 29.91, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 61, 'cloud': 25, 'feelslike_c': 24.6, 'feelslike_f': 76.4, 'windchill_c': 19.6, 'windchill_f': 67.2, 'heatindex_c': 19.7, 'heatindex_f': 67.4, 'dewpoint_c': 13.0, 'dewpoint_f': 55.5, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 18.7, 'gust_kph': 30.0}}"]]], 'interrupts': []}} + + + Receiving new event of type: debug... - {'type': 'task', 'timestamp': '2024-06-21T22:11:09.273192+00:00', 'step': 3, 'payload': {'id': '677de327-99b7-5d97-9bbd-0092abb62d46', 'name': 'agent', 'input': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}], 'sleep': None}, 'triggers': ['tool']}} - - - + {'type': 'task_result', 'timestamp': '2024-08-28T23:16:34.750124+00:00', 'step': 2, 'payload': {'id': '870b5854-2f84-533d-8e7d-87158ee948fc', 'name': 'exa_search', 'error': None, 'result': [['search_results', ['The time period when the sun is no more than 6 degrees below the horizon at either sunrise or sunset. The horizon should be clearly defined and the brightest stars should be visible under good atmospheric conditions (i.e. no moonlight, or other lights). One still should be able to carry on ordinary outdoor activities. The time period when the sun is between 6 and 12 degrees below the horizon at either sunrise or sunset. The horizon is well defined and the outline of objects might be visible without artificial light. Ordinary outdoor activities are not possible at this time without extra illumination. The time period when the sun is between 12 and 18 degrees below the horizon at either sunrise or sunset. The sun does not contribute to the illumination of the sky before this time in the morning, or after this time in the evening. In the beginning of morning astronomical twilight and at the end of astronomical twilight in the evening, sky illumination is very faint, and might be undetectable. The time of Civil Sunset minus the time of Civil Sunrise. The time of Actual Sunset minus the time of Actual Sunrise. The change in length of daylight between today and tomorrow is also listed when available.']]], 'interrupts': []}} + + + Receiving new event of type: debug... - {'type': 'task_result', 'timestamp': '2024-06-21T22:11:09.277262+00:00', 'step': 3, 'payload': {'id': '677de327-99b7-5d97-9bbd-0092abb62d46', 'name': 'agent', 'result': [['some_bytes', 'c29tZV9ieXRlcw=='], ['some_byte_array', 'c29tZV9ieXRlX2FycmF5'], ['dict_with_bytes', {'more_bytes': 'bW9yZV9ieXRlcw=='}], ['messages', [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-006e1758-b1ca-4c90-9ff3-d2e75b9ca9a7', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]]]}} - - - + {'type': 'checkpoint', 'timestamp': '2024-08-28T23:16:34.750266+00:00', 'step': 2, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'checkpoint_id': '1ef65939-180b-6087-8002-f969296f8e3d', 'checkpoint_ns': ''}, 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'}, 'values': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': ['The time period when the sun is no more than 6 degrees below the horizon at either sunrise or sunset. The horizon should be clearly defined and the brightest stars should be visible under good atmospheric conditions (i.e. no moonlight, or other lights). One still should be able to carry on ordinary outdoor activities. The time period when the sun is between 6 and 12 degrees below the horizon at either sunrise or sunset. The horizon is well defined and the outline of objects might be visible without artificial light. Ordinary outdoor activities are not possible at this time without extra illumination. The time period when the sun is between 12 and 18 degrees below the horizon at either sunrise or sunset. The sun does not contribute to the illumination of the sky before this time in the morning, or after this time in the evening. In the beginning of morning astronomical twilight and at the end of astronomical twilight in the evening, sky illumination is very faint, and might be undetectable. The time of Civil Sunset minus the time of Civil Sunrise. The time of Actual Sunset minus the time of Actual Sunrise. The change in length of daylight between today and tomorrow is also listed when available.', "{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1724886988, 'localtime': '2024-08-28 16:16'}, 'current': {'last_updated_epoch': 1724886900, 'last_updated': '2024-08-28 16:15', 'temp_c': 22.2, 'temp_f': 72.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 16.1, 'wind_kph': 25.9, 'wind_degree': 300, 'wind_dir': 'WNW', 'pressure_mb': 1013.0, 'pressure_in': 29.91, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 61, 'cloud': 25, 'feelslike_c': 24.6, 'feelslike_f': 76.4, 'windchill_c': 19.6, 'windchill_f': 67.2, 'heatindex_c': 19.7, 'heatindex_f': 67.4, 'dewpoint_c': 13.0, 'dewpoint_f': 55.5, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 18.7, 'gust_kph': 30.0}}"]}, 'metadata': {'source': 'loop', 'writes': {'exa_search': {'search_results': ['The time period when the sun is no more than 6 degrees below the horizon at either sunrise or sunset. The horizon should be clearly defined and the brightest stars should be visible under good atmospheric conditions (i.e. no moonlight, or other lights). One still should be able to carry on ordinary outdoor activities. The time period when the sun is between 6 and 12 degrees below the horizon at either sunrise or sunset. The horizon is well defined and the outline of objects might be visible without artificial light. Ordinary outdoor activities are not possible at this time without extra illumination. The time period when the sun is between 12 and 18 degrees below the horizon at either sunrise or sunset. The sun does not contribute to the illumination of the sky before this time in the morning, or after this time in the evening. In the beginning of morning astronomical twilight and at the end of astronomical twilight in the evening, sky illumination is very faint, and might be undetectable. The time of Civil Sunset minus the time of Civil Sunrise. The time of Actual Sunset minus the time of Actual Sunrise. The change in length of daylight between today and tomorrow is also listed when available.']}, 'tavily_search': {'search_results': ["{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1724886988, 'localtime': '2024-08-28 16:16'}, 'current': {'last_updated_epoch': 1724886900, 'last_updated': '2024-08-28 16:15', 'temp_c': 22.2, 'temp_f': 72.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 16.1, 'wind_kph': 25.9, 'wind_degree': 300, 'wind_dir': 'WNW', 'pressure_mb': 1013.0, 'pressure_in': 29.91, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 61, 'cloud': 25, 'feelslike_c': 24.6, 'feelslike_f': 76.4, 'windchill_c': 19.6, 'windchill_f': 67.2, 'heatindex_c': 19.7, 'heatindex_f': 67.4, 'dewpoint_c': 13.0, 'dewpoint_f': 55.5, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 18.7, 'gust_kph': 30.0}}"]}}, 'step': 2}, 'next': ['summarize_search_results'], 'tasks': [{'id': '7263c738-516d-5708-b318-2c8ef54d4a33', 'name': 'summarize_search_results', 'interrupts': []}]}} + + + Receiving new event of type: debug... - {'type': 'checkpoint', 'timestamp': '2024-06-21T22:11:09.277519+00:00', 'step': 3, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', 'thread_ts': '1ef301b2-9a61-6462-8003-1316d9875b7f', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'run_id': '1ef301b2-9a0c-68d6-bbb1-0763efc8489a'}, 'values': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '906529f7-fbf2-41c9-a28c-b1fe8f891e4e', 'example': False}, {'content': 'begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-54bd965b-734a-4a0a-8d4d-840865054810', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}, {'content': 'tool_call__begin', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': None, 'id': '222ed3b8-450f-41cb-ac40-905def3c700a', 'tool_call_id': 'tool_call_id'}, {'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-006e1758-b1ca-4c90-9ff3-d2e75b9ca9a7', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}, 'metadata': {'source': 'loop', 'step': 3, 'writes': {'agent': {'some_bytes': 'c29tZV9ieXRlcw==', 'some_byte_array': 'c29tZV9ieXRlX2FycmF5', 'dict_with_bytes': {'more_bytes': 'bW9yZV9ieXRlcw=='}, 'messages': [{'content': 'end', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-006e1758-b1ca-4c90-9ff3-d2e75b9ca9a7', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}}}} - - - - Receiving new event of type: end... - None - - + {'type': 'task', 'timestamp': '2024-08-28T23:16:34.750394+00:00', 'step': 3, 'payload': {'id': '5beaa05d-57d4-5acd-95c1-c7093990910f', 'name': 'summarize_search_results', 'input': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': ['The time period when the sun is no more than 6 degrees below the horizon at either sunrise or sunset. The horizon should be clearly defined and the brightest stars should be visible under good atmospheric conditions (i.e. no moonlight, or other lights). One still should be able to carry on ordinary outdoor activities. The time period when the sun is between 6 and 12 degrees below the horizon at either sunrise or sunset. The horizon is well defined and the outline of objects might be visible without artificial light. Ordinary outdoor activities are not possible at this time without extra illumination. The time period when the sun is between 12 and 18 degrees below the horizon at either sunrise or sunset. The sun does not contribute to the illumination of the sky before this time in the morning, or after this time in the evening. In the beginning of morning astronomical twilight and at the end of astronomical twilight in the evening, sky illumination is very faint, and might be undetectable. The time of Civil Sunset minus the time of Civil Sunrise. The time of Actual Sunset minus the time of Actual Sunrise. The change in length of daylight between today and tomorrow is also listed when available.', "{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1724886988, 'localtime': '2024-08-28 16:16'}, 'current': {'last_updated_epoch': 1724886900, 'last_updated': '2024-08-28 16:15', 'temp_c': 22.2, 'temp_f': 72.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 16.1, 'wind_kph': 25.9, 'wind_degree': 300, 'wind_dir': 'WNW', 'pressure_mb': 1013.0, 'pressure_in': 29.91, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 61, 'cloud': 25, 'feelslike_c': 24.6, 'feelslike_f': 76.4, 'windchill_c': 19.6, 'windchill_f': 67.2, 'heatindex_c': 19.7, 'heatindex_f': 67.4, 'dewpoint_c': 13.0, 'dewpoint_f': 55.5, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 18.7, 'gust_kph': 30.0}}"], 'final_answer': None}, 'triggers': ['exa_search', 'tavily_search']}} + + + + Receiving new event of type: debug... + {'type': 'task_result', 'timestamp': '2024-08-28T23:16:35.851058+00:00', 'step': 3, 'payload': {'id': '5beaa05d-57d4-5acd-95c1-c7093990910f', 'name': 'summarize_search_results', 'error': None, 'result': [['final_answer', {'content': "The provided data details various twilight periods based on the sun's position relative to the horizon, alongside current weather information for San Francisco, California, as of August 28, 2024. The weather is partly cloudy with a temperature of 22.2°C (72.0°F), moderate wind from the WNW at 16.1 mph, and the UV index is 5.", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-928c997b-9d85-4664-bd20-97ade4cc655e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]], 'interrupts': []}} + + + + Receiving new event of type: debug... + {'type': 'checkpoint', 'timestamp': '2024-08-28T23:16:35.851194+00:00', 'step': 3, 'payload': {'config': {'tags': [], 'metadata': {'created_by': 'system', 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, 'callbacks': [None], 'recursion_limit': 25, 'configurable': {'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2', 'user_id': '', 'graph_id': 'agent', 'thread_id': 'be4fd54d-ff22-4e9e-8876-d5cccc0e8048', 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca', 'checkpoint_id': '1ef65939-228a-6d93-8003-8b06d7483024', 'checkpoint_ns': ''}, 'run_id': '1ef65938-d7c7-68db-b786-011aa1cb3cd2'}, 'values': {'messages': [{'content': "What's the weather in SF?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '4123a12c-46cb-4815-bdcc-32537af0cb5b', 'example': False}, {'content': 'Current weather in San Francisco', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_a2ff031fb5'}, 'type': 'ai', 'name': None, 'id': 'run-0407bff9-3692-4ab5-9e57-2e9f396a3ee4', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}], 'search_results': ['The time period when the sun is no more than 6 degrees below the horizon at either sunrise or sunset. The horizon should be clearly defined and the brightest stars should be visible under good atmospheric conditions (i.e. no moonlight, or other lights). One still should be able to carry on ordinary outdoor activities. The time period when the sun is between 6 and 12 degrees below the horizon at either sunrise or sunset. The horizon is well defined and the outline of objects might be visible without artificial light. Ordinary outdoor activities are not possible at this time without extra illumination. The time period when the sun is between 12 and 18 degrees below the horizon at either sunrise or sunset. The sun does not contribute to the illumination of the sky before this time in the morning, or after this time in the evening. In the beginning of morning astronomical twilight and at the end of astronomical twilight in the evening, sky illumination is very faint, and might be undetectable. The time of Civil Sunset minus the time of Civil Sunrise. The time of Actual Sunset minus the time of Actual Sunrise. The change in length of daylight between today and tomorrow is also listed when available.', "{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1724886988, 'localtime': '2024-08-28 16:16'}, 'current': {'last_updated_epoch': 1724886900, 'last_updated': '2024-08-28 16:15', 'temp_c': 22.2, 'temp_f': 72.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 16.1, 'wind_kph': 25.9, 'wind_degree': 300, 'wind_dir': 'WNW', 'pressure_mb': 1013.0, 'pressure_in': 29.91, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 61, 'cloud': 25, 'feelslike_c': 24.6, 'feelslike_f': 76.4, 'windchill_c': 19.6, 'windchill_f': 67.2, 'heatindex_c': 19.7, 'heatindex_f': 67.4, 'dewpoint_c': 13.0, 'dewpoint_f': 55.5, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 18.7, 'gust_kph': 30.0}}"], 'final_answer': {'content': "The provided data details various twilight periods based on the sun's position relative to the horizon, alongside current weather information for San Francisco, California, as of August 28, 2024. The weather is partly cloudy with a temperature of 22.2°C (72.0°F), moderate wind from the WNW at 16.1 mph, and the UV index is 5.", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-928c997b-9d85-4664-bd20-97ade4cc655e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}}, 'metadata': {'source': 'loop', 'writes': {'summarize_search_results': {'final_answer': {'content': "The provided data details various twilight periods based on the sun's position relative to the horizon, alongside current weather information for San Francisco, California, as of August 28, 2024. The weather is partly cloudy with a temperature of 22.2°C (72.0°F), moderate wind from the WNW at 16.1 mph, and the UV index is 5.", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-928c997b-9d85-4664-bd20-97ade4cc655e', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}}}, 'step': 3}, 'next': [], 'tasks': []}} + + + +We see that our debug events start with two `checkpoint` events at step 0 and 1, which represent checkpointing before the graph is created and after it has been created. We then see a single `task` and corresponding `task_result` which corresponds to our first node, `call_model`, being triggered. After it has finished, the entire super-step is over so the graph saves another checkpoint and we see the corresponding `checkpoint` event. + +The next super-step executed two search nodes [in parallel](https://langchain-ai.github.io/langgraph/how-tos/branching/) - specifically one node will execute an Exa search, while the other will use Tavily. Executing these nodes in parallel in the same super-step creates 2 `task` events and two corresponding `task_result` events. After we receive both of those `task_result` events, we see another `checkpoint` event as we would expect. + +Lastly, we see a final `task` and `task_result` pair corresponding to the `summarize_search_results` node, which is the last node in our graph. As soon as this super-step is done we see one final `checkpoint` event corresponding to the final checkpoint of this run. diff --git a/docs/docs/cloud/how-tos/stream_events.md b/docs/docs/cloud/how-tos/stream_events.md index 3b0c58177..bbaaaf7be 100644 --- a/docs/docs/cloud/how-tos/stream_events.md +++ b/docs/docs/cloud/how-tos/stream_events.md @@ -1,5 +1,6 @@ # How to stream events -This guide covers how to stream events from your graph (`stream_mode="events"`). Depending on the use case and user experience of your LangGraph application, your application may process event types differently. + +This guide covers how to stream events from your graph (`stream_mode="events"`). Depending on the use case and user experience of your LangGraph application, your application may process event types differently. Read more about events in this [conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#astream_events-for-streaming-tokens-of-llm-calls). === "Python" diff --git a/docs/docs/cloud/how-tos/stream_messages.md b/docs/docs/cloud/how-tos/stream_messages.md index c25f2ccdd..c305a7d85 100644 --- a/docs/docs/cloud/how-tos/stream_messages.md +++ b/docs/docs/cloud/how-tos/stream_messages.md @@ -1,15 +1,7 @@ # How to stream messages from your graph -LangGraph Cloud supports multiple streaming modes. The main ones are: +This guide covers how to stream messages from your graph. In order to use this mode, the state of the graph you are interacting with MUST have a `messages` key that is a list of messages. -- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called. -- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called. -- `messages`: This streaming mode streams back messages - both complete messages (at the end of a node) as well as **tokens** for any messages generated inside a node. This mode is primarily meant for powering chat applications. - - -This guide covers `stream_mode="messages"`. - -In order to use this mode, the state of the graph you are interacting with MUST have a `messages` key that is a list of messages. E.g., the state should look something like: === "Python" @@ -23,16 +15,28 @@ E.g., the state should look something like: messages: Annotated[list[AnyMessage], add_messages] ``` +=== "Javascript" -Alternatively, you can use an instance or subclass of `from langgraph.graph import MessagesState` (`MessagesState` is equivalent to the implementation above). + ```js + import { type BaseMessage } from "@langchain/core/messages"; + import { Annotation, messagesStateReducer } from "@langchain/langgraph"; -> [!NOTE] -> LangGraph Cloud only supports hosting graphs written in Python at the moment. + export const StateAnnotation = Annotation.Root({ + messages: Annotation({ + reducer: messagesStateReducer, + default: () => [], + }), + }); + ``` + +Alternatively, you can use an instance or subclass of `from langgraph.graph import MessagesState` (`MessagesState` is equivalent to the implementation above). Or in Javascript: `import { MessagesAnnotation } from "@langchain/langgraph";`. With `stream_mode="messages"` two things will be streamed back: - It outputs messages produced by any chat model called inside (unless tagged in a special way) -- It outputs messages returned from nodes (to allow for nodes to return `ToolMessages` and the like +- It outputs messages returned from nodes (to allow for nodes to return `ToolMessages` and the like) + +Read more about how the `messages` streaming mode works [here](https://langchain-ai.github.io/langgraph/cloud/concepts/api/#modemessages) First let's set up our client and thread: diff --git a/docs/docs/cloud/how-tos/stream_updates.md b/docs/docs/cloud/how-tos/stream_updates.md index 38edc3381..52234e5f1 100644 --- a/docs/docs/cloud/how-tos/stream_updates.md +++ b/docs/docs/cloud/how-tos/stream_updates.md @@ -1,13 +1,6 @@ # How to stream state updates of your graph -LangGraph Cloud supports multiple streaming modes. The main ones are: - -- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called. -- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called. -- `messages`: This streaming mode streams back messages - both complete messages (at the end of a node) as well as **tokens** for any messages generated inside a node. This mode is primarily meant for powering chat applications. - - -This guide covers `stream_mode="updates"`. +This guide covers how to use `stream_mode="updates"` for your graph, which will stream the updates to the graph state that are made after each node is executed. This differs from using `stream_mode="values"`: instead of streaming the entire value of the state at each superstep, it only streams the updates from each of the nodes that made an update to the state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more.``` First let's set up our client and thread: diff --git a/docs/docs/cloud/how-tos/stream_values.md b/docs/docs/cloud/how-tos/stream_values.md index bb2c237eb..b5c25d11d 100644 --- a/docs/docs/cloud/how-tos/stream_values.md +++ b/docs/docs/cloud/how-tos/stream_values.md @@ -1,13 +1,6 @@ # How to stream full state of your graph -LangGraph Cloud supports multiple streaming modes. The main ones are: - -- `values`: This streaming mode streams back values of the graph. This is the **full state of the graph** after each node is called. -- `updates`: This streaming mode streams back updates to the graph. This is the **update to the state of the graph** after each node is called. -- `messages`: This streaming mode streams back messages - both complete messages (at the end of a node) as well as **tokens** for any messages generated inside a node. This mode is primarily meant for powering chat applications. - - -This guide covers `stream_mode="values"`. +This guide covers how to use `stream_mode="values"`, which streams the value of the state at each superstep. This differs from using `stream_mode="updates"`: instead of streaming just the updates to the state from each node, it streams the entire graph state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more.``` First let's set up our client and thread: diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index 9338e0ee8..8e3cf236f 100644 --- a/docs/docs/concepts/low_level.md +++ b/docs/docs/concepts/low_level.md @@ -411,7 +411,7 @@ The recursion limit sets the maximum number of [super-steps](#graphs) the graph graph.invoke(inputs, config={"recursion_limit": 5, "configurable":{"llm": "anthropic"}}) ``` -Read [this how-to] to learn more about how the recursion limit works. +Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/) to learn more about how the recursion limit works. ## Breakpoints @@ -437,23 +437,134 @@ It's often nice to be able to visualize graphs, especially as they get more comp ## Streaming -LangGraph is built with first class support for streaming. There are several different streaming modes that LangGraph supports: +LangGraph is built with first class support for streaming. There are several different ways to stream back results + +### `.stream` and `.astream` + +`.stream` and `.astream` are sync and async methods for streaming back results. +There are several different modes you can specify when calling these methods (e.g. `graph.stream(..., mode="...")): - [`"values"`](../how-tos/stream-values.ipynb): This streams the full value of the state after each step of the graph. -- [`"updates`](../how-tos/stream-updates.ipynb): This streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are streamed separately. +- [`"updates"`](../how-tos/stream-updates.ipynb): This streams the updates to the state after each step of the graph. If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are streamed separately. - `"debug"`: This streams as much information as possible throughout the execution of the graph. -In addition, you can use the [`astream_events`](../how-tos/streaming-events-from-within-tools.ipynb) method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb) among other things. +The below visualization shows the difference between the `values` and `updates` modes: -Under the hood, the compiled graph and each of its nodes are turned into [runnables](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface). This means that as the graph is executed, certain events are emitted along the way and can be seen if you run the graph using `.astream_events`: +![values vs updates](../static/values_vs_updates.png) + + +### `.astream_events` (for streaming tokens of LLM calls) + +In addition, you can use the [`astream_events`](../how-tos/streaming-events-from-within-tools.ipynb) method to stream back events that happen _inside_ nodes. This is useful for [streaming tokens of LLM calls](../how-tos/streaming-tokens.ipynb). + +This is a standard method on all [LangChain objects](https://python.langchain.com/v0.2/docs/concepts/#runnable-interface). This means that as the graph is executed, certain events are emitted along the way and can be seen if you run the graph using `.astream_events`. + +All events have (among other things) `event`, `name`, and `data` fields. What do these mean? + +- `event`: This is the type of event that is being emitted. You can find a detailed table of all callback events and triggers [here](https://python.langchain.com/v0.2/docs/concepts/#callback-events). +- `name`: This is the name of event. +- `data`: This is the data associated with the event. + +What types of things cause events to be emitted? * each node (runnable) emits `on_chain_start` when it starts execution, `on_chain_stream` during the node execution and `on_chain_end` when the node finishes. Node events will have the node name in the event's `name` field -* the graph will emit `on_chain_start` in the beginning of the graph execution, `on_chain_stream` during each node execution and `on_chain_end` when the graph finishes. Graph events will have the `LangGraph` in the event's `name` field -* LangGraph writes to state channels emit `on_chain_start` and `on_chain_end` events +* the graph will emit `on_chain_start` in the beginning of the graph execution, `on_chain_stream` after each node execution and `on_chain_end` when the graph finishes. Graph events will have the `LangGraph` in the event's `name` field +* Any writes to state channels (i.e. anytime you update the value of one of your state keys) will emit `on_chain_start` and `on_chain_end` events Additionally, any events that are created inside your nodes (LLM events, tool events, manually emitted events, etc.) will also be visible in the output of `.astream_events`. - -You can find a detailed table of all callback events and triggers [here](https://python.langchain.com/v0.2/docs/concepts/#callback-events). + +To make this more concrete and to see what this looks like, let's see what events are returned when we run a simple graph: + +```python +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, MessagesState, START, END + +model = ChatOpenAI(model="gpt-3.5-turbo") + + +def call_model(state: MessagesState): + response = model.invoke(state['messages']) + return {"messages": response} + +workflow = StateGraph(MessagesState) +workflow.add_node(call_model) +workflow.add_edge(START, "call_model") +workflow.add_edge("call_model", END) +app = workflow.compile() + +inputs = [{"role": "user", "content": "hi!"}] +async for event in app.astream_events({"messages": inputs}, version="v1"): + kind = event["event"] + print(f"{kind}: {event['name']}") +``` +```shell +on_chain_start: LangGraph +on_chain_start: __start__ +on_chain_end: __start__ +on_chain_start: call_model +on_chat_model_start: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_stream: ChatOpenAI +on_chat_model_end: ChatOpenAI +on_chain_start: ChannelWrite +on_chain_end: ChannelWrite +on_chain_stream: call_model +on_chain_end: call_model +on_chain_stream: LangGraph +on_chain_end: LangGraph +``` + +We start with the overall graph start (`on_chain_start: LangGraph`). We then write to the `__start__` node (this is special node to handle input). +We then start the `call_model` node (`on_chain_start: call_model`). We then start the chat model invocation (`on_chat_model_start: ChatOpenAI`), +stream back token by token (`on_chat_model_stream: ChatOpenAI`) and then finish the chat model (`on_chat_model_end: ChatOpenAI`). From there, +we write the results back to the channel (`ChannelWrite`) and then finish the `call_model` node and then the graph as a whole. + +This should hopefully give you a good sense of what events are emitted in a simple graph. But what data do these events contain? +Each type of event contains data in a different format. Let's look at what `on_chat_model_stream` events look like. This is an important type of event +since it is needed for streaming tokens from an LLM response. + +These events look like: + +```shell +{'event': 'on_chat_model_stream', + 'name': 'ChatOpenAI', + 'run_id': '3fdbf494-acce-402e-9b50-4eab46403859', + 'tags': ['seq:step:1'], + 'metadata': {'langgraph_step': 1, + 'langgraph_node': 'call_model', + 'langgraph_triggers': ['start:call_model'], + 'langgraph_task_idx': 0, + 'checkpoint_id': '1ef657a0-0f9d-61b8-bffe-0c39e4f9ad6c', + 'checkpoint_ns': 'call_model', + 'ls_provider': 'openai', + 'ls_model_name': 'gpt-3.5-turbo', + 'ls_model_type': 'chat', + 'ls_temperature': 0.7}, + 'data': {'chunk': AIMessageChunk(content='Hello', id='run-3fdbf494-acce-402e-9b50-4eab46403859')}, + 'parent_ids': []} +``` +We can see that we have the event type and name (which we knew from before). + +We also have a bunch of stuff in metadata. Noticeably, `'langgraph_node': 'call_model',` is some really helpful information +which tells us which node this model was invoked inside of. + +Finally, `data` is a really important field. This contains the actual data for this event! Which in this case +is an AIMessageChunk. This contains the `content` for the message, as well as an `id`. +This is the ID of the overall AIMessage (not just this chunk) and is super helpful - it helps +us track which chunks are part of the same message (so we can show them together in the UI). + +This information contains all that is needed for creating a UI for streaming LLM tokens. You can see a +guide for that [here](../how-tos/streaming-tokens.ipynb). + !!! warning "ASYNC IN PYTHON<=3.10" You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case. Please see examples [here](../how-tos/streaming-content.ipynb) and [here](../how-tos/streaming-events-from-within-tools.ipynb). \ No newline at end of file diff --git a/docs/docs/static/values_vs_updates.png b/docs/docs/static/values_vs_updates.png new file mode 100644 index 0000000000000000000000000000000000000000..dba7a4dc959025711f7293fd99e810c41cac45dd GIT binary patch literal 37691 zcmdqJbyOVR(l&|=9^8Ti28R&b-Q6X)ySr;}cXx*nEVx4;xVyW%yMB}3dCz-t?sxyc zYhBiAx@SvOb#=||y`OsagvrZ_A;ROrgMon|N{9<9f`LJVfv$UCp+Wzzqgz`*CvZna zF+s4(G5kZ&2Lod@2@@F^u+N}tSTOJqb1=Z4CZGcsbbx^Y;)B6pKxZ`2A)gBl1_?TY z|EbS~_;*W)uw2N0UxW34>cIq*gd`+DXC*@iV`CdfGh3(EjwMr2$2oIlH77M0X--31 zYg&CHTLWWSH*32;UBI~AI6;@z#!mV`H)|^!M@}~$;(uCjg0BA*(-8yzY2swbL#!qv z4-~R>Fb1;H($mrt^TGpxKyC*k6HY~8(f{cV`ou$Q=Hz6@Nk`}E>PqX%L~H9{O2@## z!9hpQNXN)X18PCz=x*br??z+eNb)Zs|CS?c>}cpZFh z{ri`l#%|{S(PZQJKeq+CLApORbPTlgbpItJJs=&t3>>W7 z|Fr+VYW}0~ubyiE>B-8>@psR^YW|-+zd9N_2-#YLR66ng=gIs}=f5lerz1DrpF97{ z6aRAaKcyf)^TKn}{qHm5g=hO(JqQNI4<;cjpzH>Iq79X)JbTZ_v@GGuCeHS;P!bLz z{F`{FfgilVJo0Uofq@1rq@$6kl}Yowq2Yid~VU9Hm`j0t(1jGw_ zBmcLNg#!E%2DsvNg8yX@zeIiq%->ea1Ezqvqzs6p8hQV|y?@CeRal4oZ>!BwPt1jg3$ctTo#A&ZLWqeRf~=Z+^|!o|liz+7m?Ymfz%A+iHWnvDPv!YwDwfD*l+? zWHbGftk~&PO}4aMpa}2h+(~5*Dt5nv%k%WJjX6N${Sx*z?RCpp^Y_s9@%OcbXY-|w zW+(D1e3qArPl%egBGEC8=Tk&Drrl%a9BZyBoNWrnqeGc>r%@vO5?fKdnqsXoD6r4u ziQWCm9X4fQ>pXsyK8G^Z$9#E(!_S9Du)5)ybDJ$uDLVc0o)w2tS!?wxfeIeiGnQ;f zeo_po2M4q0^VYw9tHwyJ_y3kk>RFTmZ;~(4xn!JQ!7PT!`#_R

a3nhP@ooMdywEmdq=KfH}SdX-d5BZAOXLPa%6P?t3$Vkt`lV9**#h%cX>5VU5$% zGxaO1uxbzyLmFQMLieWfbseD2*Q{mC<*?psElU49+3fH|Ub1&wPs2}W`R zL`y>B$5cn?}_|M=!PR#Qw%LioF>$E^hyZ|_G0v-8J$t(hR^TlLZb!dTVx zeM1A-EdWDws+5&M=1h@Km_%q%U?HHyq8#$7tm(AUUWEQV+mMzAHgCN!CFUAH&J|mtAWNQsc|d%lEKHGKr9=_ zQs*S8eS~(O;B*VCex1BeojjvD>ZmEmtf2K#9U>yt*#_Yw+Y7t;b)w@kX#qkFmQCPg zWDQGhu6qWe<0YJf4xMg&uMSxRAv4~f@T@)&IhAz z%p)2Pr=8akCa-He6k;rpry_213fWY>q@`kBsZEyj)`7T_79Ux@sPtz}K7Nn5xeJ#w zTRo&v4DrOnC>0@O_Dq6@DQj<^(^o14& z+xxEajlNyJlTV#SeL4Z-Vw^%d;A8N`3(;f3pg~FLE2aPXu#~4P5n;%^IjOH~{Zj!R z3#CRzN9#Mul8+6xccU?W1y{ZLS-%v)8{f10uxsW|dOA*w4O{n(D}9Y+Q>=GnxyLpS zwQ&kmn$alO|}@lUkE?%gg!v7mkg0$OqhHSk!_AaldC{wqH%v#(hHH; zQMwdB4=(&kg2B>`jzccBa{M)y6UybSKWv38dtFw6i6Fm_8fHgFN>3l&Y;#G0=W+!r zJpGl@a{O+wf0x(${6Gf$KE{QZZI5({O3hzt;45_kg%l%4pb9F)6b36Jp*!Xek*@Yr z-C{*T@9gdlI_WjC3P$8{)>`WPNG!c-2d;qE9-E zt#gHCAygidOo?_@aHt9LsBellWzXD?jSul#4e!E}2szdB*OoIQVkgFZBsXbFf?nG$ zFOt<|>kRt=rWn8OpXPk#+UPcW9Qr!&7TvUi-7rh#{7(B5SbV=qB9j!f6WD%%FeM&? zL&a;^;rS8U_=V+%TF4&0h9K-OH7}ziv$CaRO9=`RS#(&Chixa**O-FJ1%&2|pU^Q- zu_fx^*9Ar;S&Khm-%ZpQS{_WML7EQ*6j0J-c;<1OFdd6X zJ$R@bODA0=6}X3_Of9^>B{l@$aZA9CJvhDIo1ZzCn<*U066GabtLDz9f57O5iN8~* zoiEjyOrNTgHiIK-ymo8^MXQ#+qzA+!;p71#c6WdFm*<*B1v7o}`L23J*hOCs0{V0^kr?zQ`{btER%o?G7{gwFK;gs_7hVwDv zp@KE&W}P~nMsr3z{H37O?8~QRkK3g7BIU?pCx`!OiuAC|`kr}nD3T81vS_iTqI$A{rR#lb65(gu$w{h#jToj73&>wQ3cHrh9Ut%BBs?V=DOPQqp^dM3#=M zM~*M({;)Ep8;~v)$dlCm4DOE$&qMl=Cs{|9vcL5a*@pgR!mw8!SvQ8_`T5U<*muz)!hXD6M zu0@(%w+HhdU%%se&vNd*r!P`Q1oibEE5zjr*5!nsd-G$dP#6ig-6~G&ou;DX!Ji=(4TjOqPur7%0*UTY;&KYfyBM2PiTV3aoY50I&-ATr zme%QudXmHYPfF$st3thXk)m>QN9GM#?t0*eny*D>mm@KvL?*d}nX-ii ze;K{qfv02jEy3>DJnM)&YQOsGWU-}`S5S@#P82&*iD})dYUd$V(;KusBVmO@S|`i> zSk=zMhmfL1_N=~{7S>5+Nnyz5P2~$ro|t-S#E-Mth?)+(*+~`vGcgAgT5-2Wp@bR? znBO@DVi@K_Q;ipI-YiLw_*@#F5 z+*=x-d*^3DrY-EZbS`0`U>nrf;DQ4udh7Jv~?`Fvmr|g{Eni^pQ|w?&Ly2> z%4rp=TB7#Fl+=4~2e3vFKm104W8XwC>s*br7}fMcn>IIHfjP|!swbnDk);Z#wlq~13gncM%ho8F5~l9hEo!s4o#69y91UcU5R<$ z^TkI8HlGgSYkC`*iIdq2F*$4RZrv8WT$Mr8PD?Kxau8CDrTf&HUe#sABj9oFL=KpI zY1I(D{KV7y%BJyx`+EMkuxhR08$tXHEH+3?MLc&{=qqPx*@NNzx?<(nWa}`n2`_hE^>2c<_%&)c!2C*H&zeg2DI-dIkpV~dD7X64jrc}vilxL(E>kk zQ>)LZEnQ2}y3z+{;?tZ1z7abk?R}u3d(&e)XRP4k3rUhhQwliB+m}S+x#{w}s%e*O z(6e5U4BrNWf0)NXYrPYqB8G+D8hx`1js4dkrG{XS!LSb*xq3(Pxc->^n=HfZaKrMT zKX9m3`PzvkOa+E`eaU~gKfuf)n?h=f<0GXsy7}K2^45V=Gaj>WhlkUkz#cu`YN}zIBM|5wiIBrY(VD- zL_=3)gu$7zGyL9M1hJt zFnk!}QEiO*(jc|#0vOdiauIJ&(8#WO2UHjf-VQFEn^B0Jsn970y@FNQT?|_>h*TZ0 zXfr!F!>h`9TQ-b8&m54GqAD!WuQyPy51k;GSQd`>*VO@y4AV?DU{KJT2sB+(wywLDc(5f(KN6|ysj^*Ig!fVFM#T_h~ z`{LjMk0CLo#0ul0!i}wCu}2gJUXan;dmH$3LuA>@Lu2V3(qRxW{*jQ17WEPsY+P4dX7F&l3S0xgbx`c zx$3{!FVI9BTe6$1`RGe+x{fW!So8gWQVqPNa$BppF}4thj|lO z=?{}(sU&slyi`L~H~Cx1z^nxt$X#Bd*R*e&3)9k^?NEP+B;0ql>M+VQD5eR~4bX)i zoI!=8O#kwTDS-pv-2a-tS@CBhO~*kU1qM_xd#H@eL^~2r*@3m!UNONnfvZDHc(s`z zGav@2$*wU)KzKvIntR2X!1~b#8=0)qM-g^7NQ-}d3IOZ(IZ<6KYvDoD{CwM6fckSm zjT;wS`-LV93>Nw2Sukh6aB2{)USO!(=L@Yc<@aR+N9{=|o!lmxb+jY?c%T^`M@9P% zbWgG?%HXF3_uvFL4F{OF)v#BL)3Gbqg$vs^j)csT!J$IB9rIQ>!ZVo5@bufz)lRx* zxslPT!R35sp^Qwl5k3PiA*S)%poEtvws%3rVg75Z6~m+tL2Y3JOrd6{sb6ht_9oBP zgX#ppGbtupNiA6u$Z9Qv=!l8Cf|R#)$miZ%eMw&fW;hH8Yfs3+S4Vi&2Hk*3$i(&A zZ_hyn@7}8MS$Nwdm~y|`*AWgymhumvAWsy=1`)rt%RR!7`e(wh*i0MJ3WD*v4!FWz zZBbsNU7I2@D*j*}nq9EF^mQy3TC_ppm4iy<=OizdT}5hb?YVLr$bs!*4>fXej+r!t zeZImGU@Cnc9&~iIWa@*BiHOZjg8+Xl9Qx|H!^l0lw=4gK@0XKHM2|^gN-WL+FfkN& znTyKO9jnE`AMdySa44_KO#oVv5WHhGiGXh)i)cc)Xu&yad|S>EPsJxfbx{~OP zu1-%cQbC0Ost4*ZFlU|ke*a%s*9Zz6QEL!hV@UimG+e97FqwT>*5cR?J*EAXZ-mu*gF;zRB8f6V_o)_u5TeA z@OV;)ppI7QFPPb{{12o|ZDy|dw=QnLpD?=0bx!-+iy+^Vg{ccjE&r`+g9zlroBxko z3&eX@YW9I$G4pb?Xv;j>elpqsb$eb{e-%SfZ!`GqmQ_al?isj>cG;8*v%j(a; z(#y*F9K@2wDG-87IhgFUI``v~E*`|%yi4m^79PsOQCZc9`3D@vo-gYN8wMuHBcZiXB9%=Ws1KoE)f*rtUQJC8yc45HQr`EBVXJ>rk~ zoao>zplz9tcPdcZa5LpJ!R*A>cj}4P(d;_yosxkn&H#P8W|n$jx6a!!$St^E^nxM2 z91G#LY%z;d*^I_}Z^ljia9?W%ss+V4Y-o>4#E%zT`hzvI-WoMy&Xb7cB@ z&zi`uoNI-0JDmQW1uZs9b)_G6#j$K>7C2aByC+33+Aqw*AGT@3)h*`bd(K}Itl~90t<}|-$;*M+c zO~NrSTg_tMOx%Fa9L3lu`)X;nypLz!9E|wQ*jyPhBBRWMU?WnxGHh$k+N?F5$5IYH znjHebO^M!_I~D<3Lg7zCjM^y^eb$f3Rt%~GScI||{l4Y5x7H8vkF%Z% zPa~z5HB)j^v%eEDToT*DXGkJ!DXSRb^4|`>Tvt~4VJGEoZVS*+dOxSqpsF>N@fv6F$+|7fc69C01pp7Z+ntoWulaTUJ^C4?m=xu z=7-g@JNvs*+g#D4C~qk1z&FA)gD_C+uG~US-QT~t)9w@IVA#&xm(QhXq%>!=yuQ|< zEXNb|9yGUjj~;6@J%YZ>ZI9!>u1&tt9XG$K6)>%7wSuPok1LZQUS;>2bhpq~LD3Nn z-)j?{%&Y2ZvA)|h%y-ul=@~j=0n(cb%ft##zP`|MnB0=NyxK`}*Y`cLvf`+@kGTDBu3x)W{tZf zLLdF5sp`^KGcun-xEBtDsdGsO3vSm}Wn-G_-(LxG^QGmFF-m0xUVJay7N|U&7vB}W z+lEdj!oy${o}WF(5@g(RM=fz?Kh2+P7T!8plvA+bg9a5ge1}i;CX4b3K#c$?dH&_< z9qm0GQ@wa^B`b!M)E@kJ%Q=X#CB%Kan=jO`-x7ta6iiRZz5AAN6&b0uQ?1+KSM({u z%99Yzs>93OhZD$hfx2>&T01jtXHQ|E`~e!$IK%#q+E0$u<8Uaplk0(`k- zc&L8HW=8PkPh%2S39^jhre?Q?voenJRQ%is)s8SLe_l_(_@-5xb_-i{8v zT6M9({RNRXv9~PTW3lqY(E_xRvWOV0koFM_I7!GEuzA}2iS@Lq>8mE!>x@2TZ z`u<>cwQ0JU0QJ7;_2wegmdK3a zs{=9wff(9(>pAad-45<3T$a!M+1+DVT!$5nYx&;KcawXg8H4NVeEm-ypSZ%;`9`%= zwwfz28l6cWS-fV;9lwejM!U?4AJ+tZBg+MIzg(|}4gDx?FjeG@Mj5Z8%fUeKH2$4X z^WQ_;8!==TR>7_7G1eTZ+pHHi!_qB>C?rS zN}D)b6T99&S*meADCV5O-*3ZK*5b(?A{JArce=PGb)aNwdW$V94Oc^-IbzfM;8INb zX{9MRsmoP%THSRw?~kudxb@WY@mJ&t%scOzt`-p^_NS$zvZX1scl!?{Oh0l?u z-<`zap3dH47~4OjW)RsgN*5#?`qfNaaF=xYVQdg42IC|1iHy^@{g%EDXbW?dI~ZB& zL$UX?+gCDQsp>zD`PLuT^ZIG;$oA}Km5KXZJBS}L$6FPEUm3RTr{{PyBd3m`DBI`MiQ3L9b+txHvPdZ z>vKFiF#*JMZGs)CcejG)uW)P_Apx$#!K=}B!ccXzrHc28Cu!LE=xfg!s6q0v%l&>0 zx`3C+HP*NSO9aLK@cWEcebwlD)tKDF%;E7^Do;~|sv~XC`ysIYHE}JT8f89!gRxQK z(nC4acc6~aTJApR$+-Wms4L<Vbwa)BxsFCr<=yrAl3~4Vitnr6V82IZp zQHNi`X%iT}?6vj|;Or+&KT~iqDo&^5kVxy-GrIuRghpq1WH%fcve>TgN}*d6#~lTH zt|f)ZKL!hb62m0?+R>=^wi-q5PMH#-WJN3b3uZ0})35hKymr~*RzVplr#lb2R^fL6 zPJ$fkE-Jw2b=2vj`L!KV>;T7LA%&aV`6t%z;*R#m&l5q}X3 zZNv53`X^nO68d74Hq)vYowo2~BALt~G0QC3f&}Z<*%aANDF$plROW^sWH4#Tg9uB$ zB~$TXjo?)Zfs?YjtiCI39Xbbw8g+7x$CG_e^(JLq?IBE+tmLzHZo^~g0K8AJ%r4D~MXvh2vv#9jJFkw1VHw|0 z#8Wbn<7qMxqv@02^MjFyu}%;K?uRFLA;QRmy?=ak8*OAZzvsJAKJ8y9_nIONjqeL$ zv8kLo{ROaw^=I+%+^`JGXF0-;5r<*U?;?ulN%^iGOW;&ixWzugPfGDAD3T?{R7=MqOd#%yyO8egV>-*nD2R7FA& zq!&{`6Wupvd<5kl660dNbCm9MFH*LeurCDR$sIM6j)R7K^w$o@aOLf^qz_cIRL-1o z=W=W_MfL z+cJn+!omP|cXtK;Z=R!yT3Yyd+HWsZw(P3%`MPHJfgwChfyRuhIg>**z&?3HH5xyS`^t>WK-a4 z62GPM?k>09BtS|;Jw!;z*5JUM#!Zpk9HI+i#6gq$yerr8S?Z8`;a?aU(?q%sdX9Lw z1{F$qveS&Y*IcyJkV3mG&M*Tn$ZVEl5(lWP)S@KjxOb@v&t2FAWYRL!^W!+F*vY<1 z-T0RndCT(9PgY;*zN~d}CzBeqw)58NVnlrupE;I+<`CF915DP^%gC+srLbnNnXqPb z6&nNq^U-?X0wKvwgaQ^SEX01O|NPw5hL{hP52gs)$sCDHgMP#%cTAT)fQ`q!DNLVT zHXjrF8JgUZG}4B(nwn>rww}~F&3nB16@}$EF#g3!TFW5s9nNZ!YyCM zaZHN2Da$CB@bNzYIc zt95<|fk)^M)hgMqWhzNmkJnTkj7^63XRC_3rHW)fG#hDG9mjc?y&mi&#l^)BW2Go$ zGjqWqq2+tSF-uKG(`PC)*)(d&ztT4DaH&*ih{W*RrADfksn8rc4#tzozBY6EL8D&Z z-i={uEze>&T4%B78VlNRkxVTD-Em|--UoW4cY`v9@gF!Ho*>bR!n4%#v3dH_yKP~q zTyl+>dPqZGh+??i-$vPM`BDVnV+*-1A$;A^NJFNZZnQ{BwIB!|5=KXnaaQHGE_Y29 zhk8f~-w6h5L%?EZ$Pb%7bt>)@f?{b{D?0vyq{^SA=gTLC4qnTLskc;q?FH9IW14Eq zwSrK%sI)J|ks*GYoj8{imTnh8L!_*rRHzH_pks`V2}N(T_p|Z=rN%@qEfd*j>@lT- zqlDtmiR;FbN z?KL`MiX0&lv#7A|uBBLagphg0yD)yXrzNAFJf+`0sXmSP2r!2MxwpA|j#WtZk+2IV zz}1-n0AvGF(cne2tHl<=9auqjpMB-QC^nmZqf*bdJ`$4_tk_E97}+9$+h>H$jKM;5 zuI@GvygGf4;)j3kuyqc#`kp(Mv;=>N*KGcQC1WDUXUofT!a0GHFPE$!f1yNGLI_AoA>hx@7Sf@ZM+1+^F0 z60ggkzQGGH7^pW-Aaq7OFCD(HhZ?u=xf$6{O8`wSi&F%f zPN#6K-BU?g`hy>{|2F|9HYc;8l-WF?knh&Z4T|ekx_nO?0m$Et2IFsjBlArhS9QvU z(gp_x1xe^4V13DDwO$f$g~$9nfiA}#w+9ErZBnh$aoQWfF%PLYg~z6qj^TS_w_Y@O z*OPKoqAfF9X|kuyQ0SWmFNM^H*!1JWgHC$6^+m%lFjE(XX zw$3Hd5)igH0en(EBkuu%Vw30srW7{AFqX5o>hL{~J)d{$z6FD${?qxK{m2^mBg@7b zA#4vqJcfNZh>1h03ETa0ObN7@yndGY%Adi2!FhoOb#HLZiH?4A{*l>F)Qpg!BM;yM z{P?@XLXMS$ZFi3szS3pc)D8#1UTe zC*0A6ueK%)Wz45ND`WLo@J1D;TSr8iPb;O zNNA>Da`Q*W3NOaKGf;W{7&FW#;vOP5GF#165aLQL;4?tK>pi65j0yt z=oI1ev5~8p`^=&q*twNl>r#OMAru?w4oVHn)P&mm=rK+kFQEe*Y7)HRiHg`!*#U1c zgfGuYiBaN9uBzTcgXJWJV=n~yG`kFU1(27&qnUMWacSoDp|w4i-d5@BCa0O3uPKi- zuThEofFVqTds&hAiVJ$t7n95b#CKGpIkuG_$Mzmw-H{ES?~|RfkD%VtrYvQ5$8 zE38e7o;utGQzskd%LQ&$_o@w```0EGTqZvaLhhwv!+N3iG1D^Wc!@>fr%zZI$Q|CR z_)B-!#?-Wll|6p)=4sB74a-lf2;z9E2*LYd zGsOQF(>OOtdi(Bhic~J#$as#=5H2uzobO$WIe;=?!}(ysk6bSM%hE4S=c9>{G$yz} zv?ZZB7Jx#3=i5`6{pWsF9rqxAe}A*?Kse<>anw@F*&mqtd^>R@VpDXTc5iG+nIhj@ zAz%qrGgvHD$=HdT?&s>dpl~A13MUxSdu<-1!#kW|;QXZ7Thzv?3F~0d>mGP~2Xfl| zNlR<0sYBnhehYhIwWho7EIfT8!%yo6L^+-@sKcA_#G?zV#x_Gswxs^X7zJPsW^6u( z+;!Av@3ln}I(`#}?X7&|59ZGnBeetkxtCLcvNZv6K}Byz&J?+@#@*;?J)n?hlClMyfub(aKVm=74e z^~P;yCGg zLI&zgA5Q5MSBymXF9aBgB;rg*G`%f(sc69_fWLdYl*01hV6iu(%*5ZUY@kA(*E<~P ziL#I&gqKv!te+-{4#0&GJ4}KB)e*2u8{kUUE19C7K-F&PSozajyi zplch& zF4$Y>{s2(~5cgPt5uvy+_P8xK-f(E?-ga?HdNJGWq8o7eMXzn%pXi!t&RbHqS|FJ? zE*vmkD8Dsx*XyZf4LuG=4B{8N*}i=M+k`G9oV`=(LQ?Z%)`@~Ekkjm-aV~uZuMRr) zH|%LM#CwS8Zr|LlUzc2mp(9KhwiA&b^oIRuL2^F;8g8v*q2EG53-|bI1Z>&?`&SRo zpO6EEM8KE{EWrlbby_i!&_YwW5IV&U{~NYwh|}F_!?sI~g#H4t7__JUb}BUIqm~DS$(QyHlnmkPZmo4FGzf1SE=%9$Oml_hGr)tM=X?GEptz^~|?IJS$zj z4WM+2Rw-Q+SPrH%(hI-yer>5;%y{KmIo_?vv8rv9qv}H`0P2n_>6>?5@M_ z<^8dT4EE+A*s1V-@$UMjA6I$dOyr}A1S=5zv@cNW-p%lg46Q7FAv9L_ue+e9ujB)y@t6iHi9_cy?EP`(hp5_%C}E;^b5DIWWQaq zitbt5UvUCS_`Gzf9d?Ne7M56i6$B5Gj7+aD$3W_Ipsow0`oqwADGiVx`yn?)N9F|m)|gku&qmIVD0MWO+yj=vT_Oi`k@$bcihF_UL+|G^PR zgx(yC;{gjmV_ChW_83WQ8FN??HCKk`)!ryIb?_4*I2q6xG{u$LhE(hH@iq^+VU2}^ zK^mBx_%H<5`72~nA@c43TcaPu_|Qw4xj%64gI(An85Nz6_vz_eG`0G%qdS=C0p*Sn zr4=zZQB#vdNF0qQfQw-^xne$*TOF=_%bJn3zU!ldRde7v4H9#FNh+5l~74wVleyF3%X$c7CpfQM1z;M;Y7G(9+{j7ePr=S>K0Q)5ey)K zP=Col-1iRTqR(3$Eezv?cd`dwu4+bvGNzy=fff^bm?+&6vX1Nz)u%R zBYC#yv(Z1z1dSPs^(d^F`YG`D^J&TAHt8eq z_S|AUgmErrsn0v>kiVMbX zMEoG0{9(A{O%>My)E%dF)9#q@wYkShs87r#RFx-6-CplENzH%-&J(@i7nz4W)m5Ks z!M%K3dy(<*8be8Ubt^?_f>BFQ`=*qC((WejuSw4fJNhC_X5{k*rh|}RPk-j;Oa>LZtn<1E(+b*=OQ~pURYXoQif@h`M4->=!PFb!l z7)hxJ#?eOgBHly}Q)Wj=6{i5rb$!BkOt_`2hwAm>mA<~{{UcJKs5QU`q_#fkUuhrBQV0T<;G# zngF6XoU|AwpsI>4iB?;K*|$V42dl=cE`*N=06_EC!!!fLE#UG-1ZVObpL~ocTta!^ z8_VWdHgl08M9oZCcGdE`qYkcTe^BgC@#SA;0Qh!|g!XoVl?Aj7Mob?(%o0lLv{@78 zM_0es5&^Y=&RCWFXM&OF97R#FA6(>_(g#s@z~|eqRMAJ(N5WO9_SRj=TIRZ~_0NKS zAx~j>`@Mpt8PQWgl?J@$vV3Q!ZrzgE@;g}hHRh|-orWk)?i$95F1&DlhCJBlp?gSO zc;RAP{OBFJFJOg@@MU)jl9370{u3nXa5?{U zQrtIbL4IJI!``g%JB3edLF4uk4$p)u_F{knB_Mis{eq|HoN_na5|qa zuk+E2#L)$#@eoFBBm{&qKev8%aC5VC8pbpkbBcn3f{__hmd#9pr68C@Z2+P6@1-13 z!H&QQ`wL0=n52AgsYb`QtD$RRm~c|xLm=9G_Bs77Ey@^ofFZ|^1y#irVOdK#5b;U zI-vLo38Qa!0pq|+yiV*dmy7C%kr~44n4X|j>r#};LkX2Wq4&lF@MCAdul_L24&aTk z&*;SL6c(0|GNOfP{PqBvz>dE;yU=*>AD_FOe8OnAf_>JNt=ac-8hD09I~)gRw+ZTO z6U^aAG8wa3zJ@|k0eox1!c%Rf$N;h7tAp=8qmKl{k5AC|<9QQJ04gEiO6KC_)LS*f z>_^!pKa-q{k9htTbUZThkh1|K@DO+RwvFN4{uI~k(upVr+gywkp!#{JzCMmoo#M-LqB{(qjjM=)>zi>j*yfIXm(IT@qzKY(H z$h&Yd@~>}&a8j#f!ld(~{#Nop%kkIaDhh~;VlFS$-CYI25yJn?od)esZxogPLFM8y>In) z-p%oRjE?v1*XzgJ*M(|BW4>MU>0;EkjhN8abX}iEh(5HN&saNsnmG|m48O4?C{LPV|!)9a^Ud0!!hx<+*C^0#HdvRVv5FOGjqKC zJE?Ji=W(TqX(jBA51_B%J$%jVs;$to%L;Yj%5FBg*>{UmNCX%Hdf0n=_&NzLC9o@s zus}6Pa57k40f5cJ!4;!#RX(C^sq=vmrr$CYI3NvZ@w8TRKY-;cKAWJ^bADQ5qa8vV zi|cnYYgejy+zfKC7B(RE0k^e7ypliiS6qU4X2l=285^=6L>uI2hD=V1@C2JJ7H18y z-SwYj`1Q~EI7~kb0Q&#{b(#n=+9AKz)a#_)T#|~VD8>t2H4Xr{*?Hj zBTxTCM-?H&Tx>kXg89)c9>1r`zo}u2Vc#s!nBWpwmHu4<;=uh%04X|CNOwGs+{x=QpA1<7q)*sH9))mkR_diW@VRy(ua`uW#@*EZZG8&ljhe?*g z9h#Zmm(F78@PqnP>_0>^?vH;MWiJ=bhkr|I`a|~RGOE-0PeTlFWKmEd44;>F%Rl!y zAO0=MYJMqbEe8pL4R<{gm#*XqoOym~zJ4WR%GrJ0E`ZoqUl3*5 z&}@}~p7al2%dq@VW6f~h!PY>Lc$BLx(M5Z~RJykyW{sfT14L~N2V=%g)^s{+?5?wA z?wDr;ZMXHL=8ni<>PMJNIY_bgofru~pdzN7%IpD=KRxnQka3$fWR8@!^>LM}_&yFcHgffJy;ZiTnrdlP$1@2KLSEvi}#9#GAh4Yss?zk>R&2>GPSdIeuC zTuP8IHKcO>u`fVE;5lXWUQOjpriN_t@Mig`2ER(%9svS)8L`;UCEyU}3Q`~gQr*xBVN_o+BxTBOe?VPqyAL&YSZ@>_? z-=Me^hPk8Ej;#K2rIT{II+R7eXmvB&r|^f-w8t0@z|0RRbIMgQKHH+3stII`l`0pU z$NBef0ZghMDEHt{sZz@;L_ay4!jdkc6``ZFCJa8PjOOroQJiGTO2t&Tq+^7}f;eC5 zDN5VB%6zG6SDGWco+ryPhJh3Ha^%f>g7FB4tDya}ITg=-(IHv+e^^sV?;iww30XX| zEU+p?t%vI|thbQM1?UL0QaM%H*3I_p#XzBXKj66UpU@qx0GdiuzHNNS5B%sD->^Wv zGv4{R;QwZUF|^DH{x=J3OYVTw2YPbx2y2|qX1LKPqFJ!v!7iWu&9I`%_P6Q)Xk*pX zetl$0lBf0L{$og=Y|UPUIA};`hPq!CnVVo-)Yw1um`;5sPXkmd zb~xOOX$yT<$+M6hHWo>Q?Jyb_)sA&o4U2u~w>CZ6BwIe9+vieNSy zc1u|tci{GXkMt8~ZA<1zqlkxxgD2$U-<%Btsgu5HJvLbiBNQ+F=EsWUg0T@>7Fp~TV0HbL>t1YeCG-9@&b z?QJXvGAevF&+qnW(lw)h3j5f%u#@0ra5c8kxjnvT`QoxtyEQqT+1x>@T)JZ+0WP1V z2>@01Novjb%sHDY;X^3R9$G@GkYWRt-JFehQHK!%c&tMx&C!QNV!3OIy4jgF<9eYg z2-gJ(VoQ$z&vFAA9rqdl#*U35Gxk|uK!iYdH5tZnN({tmtg!37L3VXUhZC2!rdOcW zHSM~eAdHTQ|H8NW$oD~pK$h^rbn~wLyrEO`uFYLVg1UqM!`EE~#kIBpy2fcja00;_ z3GVLhE(vbI-QC^Y-Q5Wm+}+*X-Q7;_eZHCVP0gA4M|A;Bb*+BswchKw7tKrA{VNp6 zk{zozXR6(=LT<|6LnMyd(`e#AJip)0P(@}X%5Jp28gk;8E-n}qtNevUN1&v}9!?2R zC1a|a){%`&R(k(_Yj=;&)enWF$PUP+xagm6H_?h>DJNj_?mZTLA`+zkoYO--^;d~1 z>eypA#1}!eJgd-UaG2(J3enfo8v;QhSogb(DpU|N!^8nGGjFxmltT(=_~%WgjEd*- z7#5u$r4J)IEc<_ws^Gvuvv~C2o#LQN$Z>n|FyO%=x02Fy5K^ zZCBg*Jb&lBipBLUgC*4D^Li!LP9xU`;=-!){FEn*WW#yW^Jj;LD>jSS91Gu)_vd@E z*UK^4O3iOb5m8aaTD|@fulJX`39d`|={%9@W^cLUuBHw6gf3sG7|_lrdC8y|s|-w$ zT8B>l;#dm^OvX8v9vn3P^L8__f#h0JN{4dH>RwqvkJndr4{`_d3jEENVs^vT;i`*KtFY0gN@HruH|sTOCbpU3E2jGAGp8&KU<0kabCDH9 z?)ASKZQPmHyR~qO8?7QjT(5}K77YiBq}E3FC9SVQ`dZSClI+Qha|GmsRKwl1%UV=U z_gseaM?i430J`Q~00GVY zm@K9Y76syI7Ewd9e=CWHLDN=7ndO4m_VwkW>d7=v)NkR-r`X;sy@N<3497FP4-}Qj z7u>jmQeo^AmX)p}MefnI)7RShwBsnHLVa3<{cDk|wi=z7ej$HY^vn=Dm75dUfJ{*Yg;Q4sW0_? zRHcA4()6>?R`Qh=FR%2~XcjaopRwTcnqBc;vV7r&!LLVlnL2Fx^Fe_oD$a6H;g4&) z1hyaTLgosQy{5ZaoswDZHIj!E%NIinykp*7EJ{2@KTt{`Uz!f`z(`z^b@s@>EeB_6 z3K$9vadWt2V*LCLZu=F{55x0n$9bGV)av6Ggwa+o*S2pgt*7JoK$BW`zPO$&Pb=s` z!_;mxpX?;>EA=-DMd211aq*ms_14*eNE|rzfhat#gE^ZfN4|gXS|#1~6!lHzE4Nx3 zWX6g00vrS`Y7}iwK{%Jls=>I<`2J7ipJ?7T7|VKe@z&$u6@`C3-JF-7NPmG^A~M&( zhY~qK*5hQ1EbvgisudNHw-aq#(9EGADINE(T*D)aMYoldwWr-#A}umfNi&TRMj?&q zV0l>{eUaQ;d-YBEk-Pq~SvDD9bI#sLrZHr(gX`^8anCq3d1Q&k(bAZs#EG(3@9upM zNSkPW`=H*C_t9 zG~7f48uiwj$}zWmFq&zC*3YE-{+c2zUMu3u*O-xjmHe$przo+l8h`tve?k_N2UMpn z5+lIU_k*vby&fEoQ>ZT)koXw?#nvY2FU$YK*2dKSVQT|zM|oi0+yz$MEuhnkIzNNO zd9&;3Uw0s}U7rO@HYxJxiV&83|Ap!e-k#Lpd z9(?~R44+BFql4@?A={(_Fnf@e(v7enYz$#noV*9@i5kzeYMS{@3~o)(TC;_a<}hL!&iI#4MK+ zov`(%!h*=!er=Xh5KL6!AFUq0Jyin>jnsPdPH=Is-;-ed+)p2cmbQ88JDEN3|-IU=gFdlo6)C8Cs%N+&U^CA`79bO20SfM+TZ)) zXRJSKI#sSkNqFN1<^S+E!o#$1nFtn%)tM}%qe&j7qd}TZ3BF&c$XMXuApT}EwiN_P z?&BTd(RQGbR{u)iR`#H8q0DQFDlD$#ca5-E5;FfU}2y`^P1P zIrx9_H*Y=}Z-R7 z;`$@D&kg!AfjA1__BwTrqM=MhFKwq zWJ@bYEby0kWN$PS;GP|oCcLW*RmZ#~ZX%qb6VgI*f$)omn(Ro1EYu$ssc61jWfrtE zE=}Nh7aK#(odbh_Jq3~*;(=ejF=k=LFbm0MunSza zKjPD<*6^2s{{IQnweP+3de4{rVw@OBq$;F&>1(GaHvTn{QR_XNNPYaVXrlbuw!RAc zs6N;Y`ASl2ySJGcaJI$GMWimVz>mF0=0@QSu*c^4Co{e@O460H?tQZ@K$Dk(kPBUdZ$BC!y)V@zP(s}p(=G5Yh-~Wr4Gc-F6$qF=ed--Ut zzrJI=#V`2I03f0i$lzzsiDh68Sub-cNP{j9bH&kln^N5#?IbKF?0zCu7 z7VcQug0q`@<5sE8_K58@Ku}z?&{OHL!fG$)+e6Rv49WkA;K!bKPj9C7LQ9zYD1AnB z`ffAMCR^lqcMqDd`GfJqm&fA)SKB+5&ypu^k1wUn{EU09tN>#Jqi(jRVMF6$n^D^rwVQRlPqNK!X>$tE2L#7;}POlPU?6+X! z5F$ydKtoZ+Zd!X!4NrUZ%3atHJqM$gXu4s1F$-fmp|7!uuzN@lI&wm0m@WP_4LYkg zG{^G;lUVckuF}7i*-W27Ygk@5CY|EtMtiAdvmMmTUz2GfLO-Ek#KKLly3ZGGha=<$ z%T;BgWe`v*lh1`1&*Vz>dfJ99 z$D*C%5HthvNMa!=Ks!8iUY;dUTDSPn zjyeNRS!>`|CTb;VQwr0-*D6yAv8bs(f!e^WLdbo#%gdk=I=((Jhz>F8<)vx~5j znTIP_BU?v=519$ey0_@EC4vAbvJ4Sm@+oSwWqzo+cSJl_UlX?5pL?(X!JWU?0?y7i zlMV4T^_JgtPsXD&9UAShBmmvG7fgb>p28$y{@<%yyTt6~_ zbjD-)V5qAD$>o^u!@DcB1tEau$BPeek{Cb$rGbP^x+8;o2EpX|p2xC}J=Py%RG2mu zqTDNuM&(qn+)wgI)M>37v=LL`7${*k)zh?x-{y0~>n4WDG59Zc(P1P7vQ$&Q^DEA^ z$#-_H?oAmPYpM=*L|`oN5(VoRk@)fvMAS1p{}D&^);k00$HNhg@L9d%!J=?WapO2+ zYvixrv-iwzi)|=ED5R>bWo!9M>zEcBVj7lFx$Ae?kB2{laq&#N~ZwdeU=-&_wV4+_;w&ZTrTcD$|M4)Kug&4w>>hVBDpcE*S z$HB`?l68J6V-sXgN@uhgUrkq|8ADQ%f988Si$3e(DL#B6pW3TZ$ZClr@}bsVA+0rx zAGreQw3Z$N$&HS63Cp9-`RBg+eFqh|4v$)eyW5xA5Sh>(30Rt{_3c^VtqrKLzX%BH z2FcB5S%_MVjC+#59onHY)P~ka|Lg4D`VT>Ah3jqUmOnaGSpPaZQq^CDv>#j2UtVcD zEm{wmbvelZ{BS3n!EK`}-Wi3nbH|E`f7b^GQB(AwZOpbV0wi+9Z=$%gp`XLk#=8X5 z$eJ)B>smFm=E-n%jH(al%!JxTH;mCKj$#Yns23E(KRwZ3);Kn%i<4c)mEeJdQ>X%> z|M7Ox{86~$cAxr+59pV^@PaN0GDA~Pz9hMLW7A8B=UIx8hNpacwK#nR!^lrGFW0YI z5~S{<_?eW8HUuF=Z{M0S_6eM*0bd2?PHS3&XZ$X_ZC4P=8%NoS2B2NP+@Fd_g=`@w5CZLjx|~r~1XFSa zP*3Mdg}9uS_~_jp&PQ3CZIS(4BsyPCU$iCvHIJk!Dc;-5QWn{XpHfmOOl;28crRHPzt-P?C6VNAS(}~lkS`!L=wR|{%EU^giP(yFAUf~P}TTHaYjKEX#W&SM*`lwU00da^CU%HWZcQ6x=ytJv+8jVe+-Q`!GxgQ z1t^AdnR^r{r!Y3MIMjJeFZhxhAv`vZK}R~7$z*2MIid40)C{PF`PQ&#cs*Y|ecBfE zcy2BiR$P-UIiegvfBm}ou0ZNucWtjjZfQBptHvIS&Qrk?gu!X`DrcdMA2!>aAyF=sq+L~pR|NP(7M5=Ik`@bN#RLG3~@y!K`^*(cJt%_JG2+9KRG=0+RI6P zVJrr3TgMjnsdc_Imw{1CmOL+c<(s4r$;bTi#^`RyL>6-prsm)|y+A=IA2lNB`hB;IgP<>@` zb@4iG^`>WVu6n5l58M3$B!_kkBkP=xG*_H`)k-hcX*6{o_++m_uDj3I9LnMc7ok9Q@XhBW4 zhcAlhXD1E2b|*yzSnoVr&Q#`3hZ|;hr_5hU;x(4}lQNrW>k!&snUm;;xBV^GixW); z>Hio3k9MjWz8_@XK|a#HZNDSoaNl-+l{98%Z8CC&Cu%BIof6FFh9w(GwoB%rcOv@?+ixM6lf+ z#C_l$p1f}z(t8Ar-(d@2B{CTZhE#CGlt4&nCWg?YC5MM*;3E_86TG&EU;qXsICMq) z{LlWGY!e9s0jvT)+G^>hLeKfoSPR6U?fYZdRZ`1{A^BG^q>wV+PSyL;8x?5c_Cg9)VnC;eA*mX&f1V)D z3FD0Q&qC424${^_%oi$}4IB64d7R>HVTVVnp4eU^LJ2-454K?U6qxO3)P+Vzl)XaO zS^IX9Rq9PHm`!Wbm*G6;S1p{pl3!hyNx~UMga7}4pEUMrx&ecksLDh2+K4aBdtU(_ zXy*S?VofcCEH9SOOJ?JD+#_XRv=PKNN7*QV!L>DO22rtN3{M%cSGBS9GWW}EhTfv5 zMON=3W7qfusR{anzQPTqd?v)j>#!`w>pA*(-dHSgo_>?ZF0U#isVzphT+G|B1PmSA zS~1}gJZQC@94N(&l_gJc=Cb_(v;bOnQGHx#N)RVFEPw1G3C}lWaaCqV5_y&P25|Rl z?k#n)41Fwv^@QW8eSL&*7jB76H@bjdtesqK_&WH>@b)7@T+0L8e~Qp$|IjI$L5(Ww z;t$H$^`$;j9~&-0>?yK$6hu$+|Hk`FZ18n0^?bYD>XTX609M^kSG$l3uiGEnXM>ON z$dF=nz<+Gt)R&F{aF``Mg#V^}JH0UHcrOond8vbReI`U=8IwnxNsSq+-6Rtqc)oaC z9TqHO7O+f@TvUAVKiNx!?;Z_UHF`ak^#|(aiZtP6m6U$fYm~D;{dTw8V;LZr|^1v~G(hD+uM?v*U1s*jy!Hc`6Dk>x)`)DTStt{@j}L3I7la}mG9 zcMlOiEI-?3eiwLT_4mu!Az0=Lx3e<@0)8Jb`C`Se5&t8P{?uPJJCuF!H7Hs97FX*Y z8o^pfRtL&XwaErywsWqQhNz(FIvVcjU}vzatL429p{F*oIMEs|E(A0w@HM9Ihg6by z4crN6BmE;JM!mr-RSdcD`Sy^ju#~}1vH+zMomNvdglue;yC$EC!ZWOr2{nSEy@@LHuo=vpYZF|BA;3P8C0aQKCq4H?NJv)l<2ct-qOa$~- zh}G!iJHDvcjU!Cek^&Jhg|@3g$kf!{<`4cpMye?XLlR&-GO~CzglshV;VQHw0P>7?SUh-#>x} zns-s&ftYz*40iV%s9V`99z-k5F`duB$-~*Ud~c62H$|?cMX216ity(r60+@fvEvtj z5^!*_)(9vA_F@tqKa&Vw)?-UwSZ}GPUqxiFRk*RELvX3_en(D&fX9fJf!B$t@2%$W zM6X=0VPyqC-EHCYCkoHmz1(2@IB_FkAaO_^GL#Q65P>Krx0K7{sivT{8^Z;3>8&k> zTbPa5mL)GVlH5d?&G~%g*VrVxpC%~mB2gds7ZY0;^l?P3(E{uKd{u?nN6zNX{(7h1 zFk?DjEL_xXAOdT^0d$@jVY>XMaBjXzE8T3N;@AO?H&MLVKU38zA>uoOU&qS{6&51O z4gjp>7nDK?$}SGvb590J8|s{Xp-C5`Cghvwo)j4#YHtTKJr!Av6Q!NH`!TBi5Pqlh z3hJcA-W)TPfd{e#a>J8nd}bd=70f@X}K-P_^}+M zJq_V2IO#IefLGsxu~yIq-y%u~S})2dKo9PWUcoB*yGAc_KG6wgHx1AWRM{Q%>N7!~ zdf~b$eWMql!|6G=lqkYl`c44#r5oqi)Fzf>jrY*uiiLCRDkM5>?4CIG;*7wUW{At0 zeUHO|4a!dggdq&R;-q(Tmad9Udiyn$oMn$dLB7K@zG{W)nZ;PmJa0(BE`Ofqq$Eq# zxMHPcjl~qDGpr{sd)rLMCe{}m?lJG7k*-%-Fp3H@9O(GpKCYi=Y->>sI7eNQsLzY= ze2k%%wl3J-e`E-36`7;fPBIMTW)qm>;PzcnFnd6bCQ^wh70V`y8(M@L17^0_VZ#4K zYX9aIKum3z;0>i#hv|lbQxw0+WZKW`^W9k@Utd@*e^WVwcv?$aL=y?A$^5tcMx$l= zTkaV-C_K=aOEPVHrcoK8B`ui6IU;OQLgLNp$HLeUWw6=cuu5t!7&GysjlynEBF@waE>& zRh@*!Yrd!tO%+{D9J2%p^@hl8c8`knwVg}igl2?<%qU9;bY;`PT?LMXEzs>-(X3lQ zS+$U&km{#6E19n&*Z674<2HoDsQ!foCljxaTno7%Atepc)7Kx@`Z<*y9W9r(g}D~Z z8*5#w+wDhN-HJopG$*thW(iK%lty}Oq?_Vm*8t^?Y`$4xQS|ZoPz51dW*44)uHAwn zEJy+^LGjHW6%Iuh2$$3eU-#6iOPvE!DnZgV~L zw~@J1uo`>t=2-hZm86dGuDy>}!x%!x2glBeQrg(aq(+HrD5{PonI<8(@tyRK z+6_TX$bYxegoUZN#He?71U@uTgfoOZ^G*9|x!@2G8j@axGBc81gtDj1$~3V7P(Qb! zR^P5g1i%pU5_>t@h>IMAv7{7D$<9d*Xq(YlI2J%D2NVS&qlP`RaxH6!dKiFH_+D#*2oU$xw$i{f%6c_m8gI5Zel; z)jwKRj#izn0a!G*UsP^nheVYCG{K-~|B_xqXx1T?0-mTL05kmjaEhHk370b$4eThN zuyIeP2~Gx)Ej>}NXB-p;&)xlVfYdywk|~SQiOGcFW|@o+Q6|xLchRg}8%^xfx2Sp# zZK^Nq65hA`PbAJ}8=1T}wEA;=xZt-J$S%_UlSA*th`m;2$$uL_tACPp2!uS<|Mg(k zj}Q>kle#K8{XzBL6*wHwv)T)ZPyY9VHgKSv>@{lJvD@%}|Al)&px>~DOc`_tn!nZh zYFw!V5{kY%5&Wcv7>CENZ)@$5*1scFRNuE?TxXKWEVbU*xsNB~8>p+D)*z?_YS1T8E21A7rfZa-hCo=4VWK{dP+M- zhyLcEzVU^Iz<9@*G)~mJvH==Z#r2nR0{h&ow*hWb^8q#uxvJ6l#_!(m(=_!rTk0c2 zWAwAvE6U0Hx7c%>G5-?5et6&b&|=dbYn|CO9zI1)$u^c}`{7y#?|d(2i|fwgESRR* ztD?v0j}#&Fqv~jMcoHOMQj1Jc9gB3?(~D^+(L5x%!#X5@W1$J(mA@yOjw>uiZO8+TO6LNMEi)>$X|L7gcTnwSheRwgXY{J!?d zMR@yZ#uBli&6dQ9zbZLdjVn^!UaAKUabS$-#4 zi2UM|HKBgYXh8B^pVM%FAhA5;xt^$Bwn8JKYN)9=E!8DV(OEW5qR8SsVxk>Kywv>w zeYt=K3v@vJ&6(@$D4x?&3SpcHJ9`PUAI!E+H$v%KDKwi>S?A3Wah5LZj=V(m3o+Lh zNf=t*`_4JBf&{21pbYfq7nTojaEQ9*0o@|PcPQ~N$c~S-Gxgdn!@hbkVNxxQ| zNpBbQT&AT+05>b zf-Ck;1er#00L$9gxrm>2QqJ1*{upYx}ryHZT*NRcbsuUK(D@Bf= z(hYpn!Kq|%z@_^T?%QIAtioeWcJfCQ?!|3(@uTYCY>PJ~R6pg=%JuH+vtBw`iP_H| zW$0?!l62u!XPPq6@FD!!E$ie#pPQQ6d1?P(IDta@ah$$r=1A{X5QZzL8KA282g7yN zKFvWj2QcPdNYu49R#oxP8D?oFw>CPe-Fm$yu9py*A6Hk&XA}AL9P5suxVb%-qvPT3 z(^uYX8y@ebYv;Ef@(AuWN*BFiKiE$}as8z2gZvR12q<#e2ZwO6q5Iyqk~yqQ=0*5} zykvt1UGo#)tBWl@m`m1(6t_5H)?1wClkvXtrDYcjB2~@0c522jW0fou`lpprj%@s5 zyTq|rM&8+S^Z*_C%*kZ@^;*ODUNEIL38Sn{QG^e31Y--wr^7dZON z3ACj4E4E?Aio`V&mr~z3pZe_#Ufzad%hXOgA3S<*GPjsM=SOl*pk0!-Bd3PQ6zc+= z(jeyo-Xt#M=jom8Qu}hnbhDMC`HoiPVUYiL?{7jryT=>677sKN537Zqi z#or8C0)>U2_RWs-3x@sN=x%me7b||~yxx`Ph0D_2F}-oQZ-S>J^O8RsBoX`Z3)Ks0 zo1((a#3b|-2+Q9O;C&1BiD~?f2-E_5I6Vb?=dB&ih)31fn2wp_Q#$uEyi?0@PmdIQ zQNSpI4M=h8Qtz;=KTFQ|B%hDjbEJ82Bn%L=3_5@9>D+v|f$t@_vmj8E8?awwb)3|c zTz&N)Ej3z6O%hstOZLNAld{!e1@0~&JI#J-HnyU}tKg|yYdxIw;=qwsWjO^*ahf*G zmSlYB)cfblJLmfm!x6|q6+jxD=1bR@8Kh>f->ZDz&Ase7862B^8$gza_JyIX)>}Df z@RGgfZmwEe8QASlg60jKxIujVflYy@JadL|tSq4y+f$x-_|5mun^ogiKFp=ztxk+% zM*FT5<_jLw|lUNWlE7NS26QelE6 zns7L#d}-mR7dO_V)@m1}C3(q$mI?1w>e@D?|g+aW-19+8j9Zc zNJLIxg<2?RN#{$JqPs7OYLC5&Ie`@iKs)OvWl*z%{vn7vK3@73*LrnnXxBVWr6n^Z z>n21i5tlOjTh42sya8E3R#Gx=YHCXG?0V*y^6znef|E(0rp;Oc2Kw|Rrk8GTHY~q* zxVM@iJqnEWLifK1s}BrYTRF|AmT)vjMdkQ|)Ts5O4bqjqckWmL(^>rY!#EFD#;n#x zuiQe4x5j0}*>OOVDNY~3^!8T;{^lTMIO$37ZyKtD$u|>;h30=;Zv&t~S+?$4FTq3S zia&Wv%Z_Gj#b(a}dFrh-1X}T55c=p4^fSfTMK(VhQZTOl?H;kbV#nf&zG2TCYHhFt z-yI%z9=Gxx&fY+l<5hv56PT=RmZ>Cg0c$ruc$1xMbOrNoR1^-e7nO#o|U{g}le|l^|f$D8buNwFfJc3AcUJH`G0d1j{#do@v z`U!vi{@yZ;ua4PQIZDL!rCYUxR{h=ig3V^7IdQbNO|8s^#pOg@sneMSib0i1r!oeR z?_%nB#8Un&i3!FJD;fa}K9qo3JtDKND0gWs?nU$FFMJy0hNEKWGjFElWAhWP2PP_s z^%eYpr@+Kqun!j^pdm06tN1e^CpP2d#-MF&#fUUJJ~uLS#S0$}9!5RbOFHhC1;E2* zmr5@w!0D8)ToitLWx|y63lR>-pbo&i{p&N<=jzY0t`38yg_h+LM2_>1*HL%oaQkPh z)aya+mU1t;>ru$x=ZyM3-`=sXa0MzU*$VC6aN4WXos@(3VZ91b_}&K{1ONr5Lq;|x z)0{fC{=cl8y!h@hJ=@J2HFZ#7N4f{ki4hS?@Xt5KA=oYWo?%(amzE0UzKfsa5Qrkz zd&WR}(h;JeSH6~3-)Aeg<=T2_O^W~8X-?&##<>gGA{J!|&xdUxD{E>J! zZu#u|o1-rPS&@}H9jvp-D`U<6wKf-T(0D%A(aJye>=e`CGma#*)%mjM?(R(f*3a+p zxHx+z-MWKf-1_e}OjuZ09_TvVRGaH9U=QWBP|A4o$S+Ds_1q(sz3821bDMGYBTKAX z2A{CAi$3@U-Z5r)+QIY{(>|vc@k`lyxXeb?CqHosspBL!Vj8;;sC`z~JZrSmEPw07 zLOWJn>W(Q{U~SX;s`ZPbnmS%)((9vJ_}PUih%}bHjGg#_zl!9#4II7}=%2quNYBFs zi1(|1Z}dq7L-g@;Qv8AAD;fJu7_BL)0V5{e++m0}wHM#6)A8L2tbL$)Ep9fRW~T9M z0*m?mk%QT(j>b|#i&R(?QV&4nr!UG(K#iTkrtB>~>*Ohi#M+TQJ`!9BOMk+@dqBC4 zC_N>LTyg!Y>G6l&T*9(-EZfy}wsstm$?{2JC5=S+WeQI~$HfPnRyW7f8MLWn5RI4Y z+l%z`oCe>4lj9iSIhS(`jLjVdbEsu`V@z9|xL&)TitMw+=}*G82m7;ra6y3PkeuCK zoHVI*xu;YWZ{9+|AEzj?CFb5(T59GA$IR$WwMV|Rdi@~u-K<}cWz?>i?qe;{9j6Q% zr&a*KiVt_^4$U)|^&9O;a(kScJ*`|kP?Pf8HKvEtc(_FV&Hd2|<+uqEnc(0k#2TLF zGsXdPA>32ob25;*$x%Iv8q?lC=8-v3$ghRt;f@AA#zP=*Or z_=LSs5-o&o2;$~CjTCN*9x2|RL}kt64P>CbTz%&!7Hd?${c+M8T#N%I4S}a;$Tjmk zgd13XH4mhq(h6J+vywH)cn^>BdPg$MSnoC3IzV4}a3pPv^5V2g<3u;U@$`8TF?g*k zWLl2S_G(WgLm52WKtk*656QY6&Lk)l8pM1CPg-p?{?N}ufedvkn!n=KV#qW8otD+$ z4sOZ*l0I*Qm>z~8AWRs727r|w6ZKyzzNnn5vC#5!#X<7oM!&uaM#2~f7KPsDG1?$Y z8yZYozr3RN{Cf=aq8|(!#xAIOpVPjRkN0KH@v;s+I_sRgXaelGdg-DTg_m;h10qnuI*m^0t2^fpzU%puf#fV$u} zyo>G+hipqow&`Q`{kbf6)k(d5#)_1CH-W#{kXAr#ZDU%q8!H( z6ZA$xyUXT?XaHzFfNQ;Ar0`O9_8XW1pi;xmpObmH0!eGL>w;q zHim{1B$)sCQq#M~5zklZ?O{J0FI@50Zt_h`Bp%Si^t^zyZnlR@U(ZEP;UJ9yz~B2# zYN-l|Z6p;FK5fpIXouW0%+(PejefP*j&M;k)8OME;LmD!Nc|FV+c$1F6N)sp(3^^@ zC-NQ2u5IbS7JJ12%=6E&6hpLEAC-O1hbK2i5^_NH_y0l1q>!-D7?c}*Qs2_T5cYAE zTYc&~E55Zm_ev#&exsT>Gl+8!lVBK|y87nd^_Be%1Rps0UerfBBD^v@bD-l7+r+WsDdSm-e)3A}e17K|!c>DodDU-{ z?h%E0M16!g)@-A(VR31HuT$oqIj!*g2x^$WG9Qm5>FEb=M(2tBu8jaIjMce*sLp5R z7zpYx@2P;An;$?`!+{|gmX+Bf$Lx`rqt+478@-Hre573iB zpw{YS=~bo^)ACVxf_$@5Osiu`?Ra-oAS zKBnZ=D>=g*0A>ri)1O7Q`1S0HZCDaCE(3}nxFJr68mH9_OU3r8?!CT~JjSi9X>-!v zJe|^7LXtE_-JlT9ll6d%F%u9?IA*Cv7R^#P?l9}`nrPVhD`J^zxrqR=+e9L>P*wiW z_*V-Jz7ARwRiU&<2kzKW1*5j}XuJ>`FQ`X_5H=miyd{4eK9Wb6G_{o5TPiGLCn+@8 zY$3G214t`fqg8e7V8S}Ibl}~Q!F$dj;Z?liA+D;Ct%eEyjkJ82@ODr%(XpTO6z*3i zt^2tx+5e$r_+!(mRu$`pG=|5WY@FOuy{XJt-9kJ8pPSrW2G$&ODna%puz3LQ{_#Oe z?C(%Pz?1vJMq*Rj&FWIIb6LiTVre6{iMH%3;6^uZV9e7Oe(TGSrI8ov{jr+u|>fh8<~*|WJg&nVcS4MaEq z5Xi)cq)gP3(!IPrhOsR>JQLBmn&8NBI=uLKAH;}!=82^Q2%4v3rrsY4j%s=@wr$^C z7ty7N)5J97ZpWL(DRF^3*|-_qx9_tFcR#Q3Sm?k&5x2ycWN+6+^`hx!Bzun}mRTM( z$Ti~!|DuvEjd+49f(bm+Bn&7cfUKWx7RPXsx08JOa1(>Xr;hBr9S`O(7TOgK6b!KF z`CD6rz-?p>W*c)`m(&H8nzu#xa^hy4?PCquk#Zo>+BKHU4hTXX&6O%K>vn;aDdY#G z{M?>x`rQkI&0$C4c|WfL0a|vPF5nNNFrLO@+U*Ao_1P6qCO1=}kPldCOG){D z41aIkypLhF+DLQ0)~qDmJK^GIv+lU41vT?LLRB4Ny}dUae+*jA3qiO69>vj0-#eoD za;erIRELooO{T-l?nA`moC5uTu}-D@<6s2Vu44L;8L~UV9%v%xqAW}<0krE&?|api zjSxUB*E3xgk|VSHh7wR$rS)1#0>}T&1It#7{2<;Ay{99AkUnyy zUCqDb2kjr8s~)RoO}jf3x>kn^NLGRCdm^8njryn;+xYZRcEA{*!i%XI`%m;cgg^O1 z9d-p_1%K@OqWlh$^XkF)N^Fdg+#i|w=peY7Lx2$n11dVs`r`WBvUYo)oj2%n9PMML&Ta+Dg<^f{&_}L_U9EF6K6!nY zpI*63bL5L`f2P6iNgHmEh7h3TcgWb6{U;e0*%k5O!u0srY4vWE^E4T`#rbb7PJ;gm z(hvXOQf&u$N&lsP;UAjDNkYH$k+AxnS^TGxkv<-h&b?a-d~AOLLPjHVMkNFki+kdi z6-LrS(qpcgCI2@Tmn9XJtFXv-xd3ZTP)xNl%A5W`hcL0bfB$`W14ZI1%|+zwi(kACWPmKg~H<9odg3y{+Bnj!AsQt3O_NTV<%xH`fQw(nB`? z64oF_{z2q_*q->EEz;G2?_sumb3KrT1bxY4{4i?z*VskLBZwp|{9?I|to>od+>YJh zNPhb49W?f_bH{O_&}Bjb-Jf;wp@)R)-Y>6>QdO&xhs^7JHA;HC}M=r<69Ix|4tUS4@c1 z#!oVi0kQdegn^s{1{wbeQwt5p?nC*!V_2TLhoCmLhRMMI6Gp=Z7<)jAYAcd`>^81KB5MMBkri^<6*U@ ze@*zr+jwqA${YIIeLVvu{1 z&-2=m>kXhL{dfZYsC>f=Z1f6^@giqKv1j1}`kCDiRhFG|^NGjUqwH88!kcg%f8ZEd zdZZvIxjb5+xgIspq47R{qrN47x9yF1(pSFS3PM^ECo_-SUI3$Mlw^fiR5qnO(BSUc zvJVC(Ec|+crbR;SKQN5}uwQSgpOUI6B6j?YW}939B3Qf8@zg(IEhrhP?{W_c0IKUA zkdd_AyZuqS>8D=07q1I{U5eM!&)oUcpK&zllEhf#FfM3NK#5RO0~V9n=& znob`kJ+>rss+Djphr;xkLonwIAug;=?2u#H;RFLij+77<_*vs7{n?BZzPVjP@JVy3 zoPQQ>ISbd}De(5mkojr!8#e}@#Bsm_sLvWveDJNBH~Oo~w?@vab%*nsD@|6;(|-sZ<*CiZ7CXYS2WAHP)_7LwjcpNf@;E^w}+wYG^tF0p`QH9i4PsHB#RyQZdWY3vc%B;SLHl#a2ufN!;dKd}dU>b3yq~x+V zEUfwNRcbWJ@`_W;)|)C#vxDAT!4Qyj9JC%6Ia>at$iBotQ;;6`cH=b8vh2O)^}LUH zvf0^L&;>Fdsds_Au`MvA7H80Dk})GP>3-so7=o5FrK*SxDrwONgM>|;Ycqe+HHP&fHWISy15P-yD zYW!148BZ)+s#mEkZt~hhk~q=q44LAPq?_F+WRVtyjQWk|4p4n_!%}#b(RZ+^`^Csx zSoo(g{O95p-HSoAPr1{E z)fYY+aA|Q5*i;?th#0Qog|=YF`W`>7{@w*Kw9KE08?wI-5)?m(+rSS56Kv*j6@`%^ zlM23ahnfHszc5|mNe9tCMDQJiZ{|-Urw4!8aCT5b zZ=FSIAa+1a2e_!u*^#0D$3Rns`EY0yPUn@^eq}P;8vjIBjbB)~btbBtx-%WpAxEH> z$MHvqaMmF949YlujsU`IOPOL0Bl4-f?WavkJ|*pCe7UT3#)gXc<=yIl`rgK`_P2y? zgU*CmsP1G(9P_}(e%u82SIO4meZ%2jIxZ3tdX+pO+3j3@rSNsnXi_?;kwiBdf}^}O z0tAH5MY0OkTASl}_$T!T#9)I|OY<$vKN5pV>mT}!}_}XMV zy4(BrFQ&I&elT2%FOS5UOPl2ra7bO(S;x<@|7P>*0o`C3E$W$jL0d9qRtg}w(<7ip zdQL~N{fe&_B+ASP^$`O)W{Z}trE^rv18E@C1XnwU&A&JsR&EkloLHF^Sy&3W$_|@v z)HW7m8Z5nLl$@8mzz(YYy+s0*sIm`R7QWZ zjZb`{6(G1Q!_UBVYkFl}L2{KGSfZN5_bnA$yk_{F+$C|q{xnS|7JyC-Z1wXDX!iX4 z>(wR?jDcM-6P6QV-1087!-tsi>jG8w4qzwlVTqDde2b{q7z`lb>Ohsf~K-X2?a~-bwFU`o8Yh`?PC&;hp%20sR+Zlw`Pv zn4(>P@Ljhu9lvkpaj*CSW&3+-P(`DB0Q2l@yp0DlDWx7&?7njE`=&_$n_XN}KB(04 z)5lKPNI(3ao+ef*(`3ByNWNr7(u{rm-JofRo`z?+kW-N;^so@>rMT+8($y0nCWOhO zLGoQd^i&2JB3G?i*DRoHP^ZiY13`z1jm%BX5js~lKWPG0pu3_*l)x=@U7XCDopcHD zpq-e1@{MO`C0&y3u4&0k#sIr9s^&;13s-v*o(gBH1>=i`7e#MXlu%x7pG!MkD3-<` z8(K@Fb%l4?32SJ3`-G76wm!x%P1LhQb?(6jSImMo+fuKaXV_;YRTkUS(yNmTngX~4 zEu1Ein0I|?P8zC>SRM|K>#G*~+m14H4SjIDGNwb}DHIg3x%8swHVnt0PxjWsZ=i6r zU8UZ9m!s*(kXcv?cM?1+Ty=h~|ACax>jLH1q*ohqpPT0jqTs0xfyo2aZ+v~OM9W8a zo}zk3NAGVoNY*KrX-<*`aWjmb-`j(-mL0Eq*u+FTP5G{}_7ZUAY(UVr61FDj`@SaV z%fO3_l&iYGpRP(8$i!xoF)Jz!i#rF|NFr9AxB~6KjfZRR$$o~}I8&G#WqkgvE6Je& zFxGy25~WHwDAfW<)GaH~I=h=?jI!dre>DEUgN_w~=qcN&tJ6l(z!+D@9d_0m?X+;+S3SU!hXdcD&iqB%C8^JMmrWO5BB^_aeHYpfEw269W;8%aU#N>Tq} zygabsm%y>z8N9J}7>3g>ChYqd3h z;}W|wN2mG`dA6tM=ICi*F}FZXzn)~9FKu8E^5s7WX8G0JP)Q4#c8-Po4~Jt`$opqj zOTZbKfUh6wH7b4AR%-T&$LG;%R^J-$@M9OGOYA9{TEctSXXDQODCDNApRcqiK!iwZ5xxe zhrtdL45e{S!6@UCe$&wZ()CNN3Jb9K?~|td035I%x!ylipVLKES2`Tbx!3?EBHrdkcIxh{ADm<%VS9U!nq76d_HEP|Pw}i^ zy8>l&gGJCxFmdK8dN+IbZI*s+v$T{8N4`BCMyFN6+y!>tOF}t#Eik3p?N8re zUpAX!wNje3mt^3OM=uvgl5{1EgEac+^BLUFaTu4;?x4-}o_zz6BN1M6os`uQz%%Fw z$_Ole$_z0;9Hcjk3w~n_o4=63mK}QWzMuET;6&+sJss03wz2bJr+ zAN9#iJic{|5ub;2ynz@G+8Be)!|pgQ9_|mqAf3_6wW2N9JkD6&8Qx#w@f_LlN zwi?`Yne20P8}Va(?a*x_OB^|8yGz!GNKHVB)|KHc*!`_b!Ju~y(`?9922&d~QN}-g zLrB5@GWbvpA7QosuW$)82T%c?Y*b8`sjJ_jRcz5aJunt-^d7*ScPfB@Z(25>SqwRK>VfEe zut$r|_H*z-2384lGfUUqNqu~x`^A)&rlw1Hc=f#tl8Z?(0~Y3k{kDvD7Pu_`?o4ss zjKW!UXY~)48PUAu(Xj!nSrN3B3M)FAjN}K%R84habPdSM=F-BT;e_UMWT$=Ey~M}W zp>W$t8?<6P?x@}xURP)>M5WOo^%5rLa1dNZMnETW|?pJ#W*9dtoOc(qVfJyavX z!%Plai&YeJ2g!`}Y~Xj6WM;zC)0Yj-c>L3?Oy3s+$g_v?PMS(BMsA9lERXZeQpz+~ zG)V55{S7+7M^d81buX>|5()b{5TL>yp}k;)=9tuh2Xb^*kNuHpGBBV zo;_PeGISnV7oHmmu$X+)sr?j^UxlNqBY^_%xB0iVdPYjZA_8{|ErvBuf)Cz)T-UzS z1+`A8*pL92A5jUV`~DqJpzVQIYx&3IdZIagy$|w#zI)l@)#q%U`+EN(3h+4cA@EPI zU!~-OfMV+eqw&Wf~~C;uF?wVpqM0+aM;^3r(hZ&Ae}0FL@ls9LIE k9lB~s1`Lpd|I>0;#jku~%X*jQ76}25y|ojz+{!2MKSEr=hyVZp literal 0 HcmV?d00001 From ca8614f87335a34264ba806c12a5fe77bb6cbc9a Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 09:43:42 -0700 Subject: [PATCH 176/287] Implement get_state and get_state_history for nested graphs --- .../langgraph/checkpoint/base/__init__.py | 6 +- libs/langgraph/langgraph/constants.py | 6 +- libs/langgraph/langgraph/graph/graph.py | 9 +- libs/langgraph/langgraph/graph/state.py | 11 +- libs/langgraph/langgraph/pregel/__init__.py | 556 ++-- libs/langgraph/langgraph/pregel/algo.py | 40 +- libs/langgraph/langgraph/pregel/debug.py | 6 +- libs/langgraph/langgraph/pregel/get_state.py | 12 +- libs/langgraph/langgraph/pregel/loop.py | 15 +- libs/langgraph/langgraph/pregel/metadata.py | 0 libs/langgraph/langgraph/pregel/types.py | 3 +- .../tests/__snapshots__/test_pregel.ambr | 190 +- libs/langgraph/tests/any_str.py | 23 +- libs/langgraph/tests/test_prebuilt.py | 11 +- libs/langgraph/tests/test_pregel.py | 2369 +++++++++++------ libs/langgraph/tests/test_pregel_async.py | 1179 +++++--- 16 files changed, 2833 insertions(+), 1603 deletions(-) create mode 100644 libs/langgraph/langgraph/pregel/metadata.py diff --git a/libs/checkpoint/langgraph/checkpoint/base/__init__.py b/libs/checkpoint/langgraph/checkpoint/base/__init__.py index 1afe69ed4..94d34622c 100644 --- a/libs/checkpoint/langgraph/checkpoint/base/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/base/__init__.py @@ -51,10 +51,10 @@ class CheckpointMetadata(TypedDict, total=False): Mapping from node name to writes emitted by that node. """ - score: Optional[int] - """The score of the checkpoint. + parents: dict[str, str] + """The IDs of the parent checkpoints. - The score can be used to mark a checkpoint as "good". + Mapping from checkpoint namespace to checkpoint ID. """ diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index c1024ea9c..eeb320ed3 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -5,6 +5,7 @@ INPUT = "__input__" CONFIG_KEY_SEND = "__pregel_send" CONFIG_KEY_READ = "__pregel_read" CONFIG_KEY_CHECKPOINTER = "__pregel_checkpointer" +CONFIG_KEY_CHECKPOINT_MAP = "checkpoint_map" CONFIG_KEY_STORE = "__pregel_store" CONFIG_KEY_RESUMING = "__pregel_resuming" CONFIG_KEY_TASK_ID = "__pregel_task_id" @@ -19,6 +20,7 @@ RESERVED = { CONFIG_KEY_SEND, CONFIG_KEY_READ, CONFIG_KEY_CHECKPOINTER, + CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_STORE, CONFIG_KEY_RESUMING, CONFIG_KEY_TASK_ID, @@ -30,8 +32,8 @@ TAG_HIDDEN = "langsmith:hidden" START = "__start__" END = "__end__" -CHECKPOINT_NAMESPACE_SEPARATOR = "|" -SEND_CHECKPOINT_NAMESPACE_SEPARATOR = ":" +NS_SEP = "|" +NS_END = ":" class Send: diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index 2eb732274..04c27ebaa 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -26,9 +26,9 @@ from langchain_core.runnables.graph import Node as DrawableNode from langgraph.channels.ephemeral_value import EphemeralValue from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.constants import ( - CHECKPOINT_NAMESPACE_SEPARATOR, END, - SEND_CHECKPOINT_NAMESPACE_SEPARATOR, + NS_END, + NS_SEP, START, TAG_HIDDEN, Send, @@ -160,10 +160,7 @@ class Graph: metadata: Optional[dict[str, Any]] = None, ) -> None: if isinstance(node, str): - for character in ( - CHECKPOINT_NAMESPACE_SEPARATOR, - SEND_CHECKPOINT_NAMESPACE_SEPARATOR, - ): + for character in (NS_SEP, NS_END): if character in node: raise ValueError( f"'{character}' is a reserved character and is not allowed in the node names." diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index a12796548..1416d14d0 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -31,11 +31,7 @@ from langgraph.channels.ephemeral_value import EphemeralValue from langgraph.channels.last_value import LastValue from langgraph.channels.named_barrier_value import NamedBarrierValue from langgraph.checkpoint.base import BaseCheckpointSaver -from langgraph.constants import ( - CHECKPOINT_NAMESPACE_SEPARATOR, - SEND_CHECKPOINT_NAMESPACE_SEPARATOR, - TAG_HIDDEN, -) +from langgraph.constants import NS_END, NS_SEP, TAG_HIDDEN from langgraph.errors import InvalidUpdateError from langgraph.graph.graph import ( END, @@ -321,10 +317,7 @@ class StateGraph(Graph): if node == END or node == START: raise ValueError(f"Node `{node}` is reserved.") - for character in ( - CHECKPOINT_NAMESPACE_SEPARATOR, - SEND_CHECKPOINT_NAMESPACE_SEPARATOR, - ): + for character in (NS_SEP, NS_END): if character in node: raise ValueError( f"'{character}' is a reserved character and is not allowed in the node names." diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 63995068e..352cae7e2 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -5,7 +5,6 @@ import concurrent.futures import time from collections import deque from functools import partial -from inspect import signature from typing import ( Any, AsyncIterator, @@ -60,14 +59,14 @@ from langgraph.checkpoint.base import ( empty_checkpoint, ) from langgraph.constants import ( - CHECKPOINT_NAMESPACE_SEPARATOR, CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_READ, CONFIG_KEY_RESUMING, CONFIG_KEY_SEND, ERROR, INTERRUPT, - SEND_CHECKPOINT_NAMESPACE_SEPARATOR, + NS_END, + NS_SEP, ) from langgraph.errors import GraphInterrupt, GraphRecursionError, InvalidUpdateError from langgraph.managed.base import ManagedValueSpec @@ -83,7 +82,6 @@ from langgraph.pregel.debug import ( print_step_writes, tasks_w_writes, ) -from langgraph.pregel.get_state import assemble_state_snapshot_hierarchy from langgraph.pregel.io import read_channels from langgraph.pregel.loop import AsyncPregelLoop, SyncPregelLoop from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager @@ -182,181 +180,6 @@ class Channel: ) -def _get_checkpoint_ns_to_graph( - graph: Pregel, - checkpoint_ns_to_graph: Optional[dict[str, Pregel]] = None, - checkpoint_ns: str = "", - max_depth: int = 10, -) -> Pregel: - if checkpoint_ns_to_graph is None: - checkpoint_ns_to_graph = {} - - if max_depth <= 0: - raise RecursionError( - "Reached maximum recursion depth while building checkpoint NS -> graph mapping." - ) - - for node_name, node in graph.nodes.items(): - new_checkpoint_ns = ( - f"{checkpoint_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{node_name}" - if checkpoint_ns - else node_name - ) - if isinstance(node.bound, Pregel): - _get_checkpoint_ns_to_graph( - node.bound, checkpoint_ns_to_graph, new_checkpoint_ns, max_depth - 1 - ) - elif isinstance(node.bound, RunnableSequence): - for runnable in node.bound.steps: - if isinstance(runnable, Pregel): - _get_checkpoint_ns_to_graph( - runnable, - checkpoint_ns_to_graph, - new_checkpoint_ns, - max_depth - 1, - ) - - checkpoint_ns_to_graph[checkpoint_ns] = graph - return checkpoint_ns_to_graph - - -def _prepare_state_snapshot( - config: RunnableConfig, - checkpoint_ns_to_graph: dict[str, Pregel], - checkpoint_tuples: Iterator[CheckpointTuple], -) -> StateSnapshot: - checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - checkpoint_id = config["configurable"].get("checkpoint_id") - checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} - for saved in checkpoint_tuples: - saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] - saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] - if checkpoint_id and saved_checkpoint_id != checkpoint_id: - continue - - graph_checkpoint_ns = saved_checkpoint_ns.split( - SEND_CHECKPOINT_NAMESPACE_SEPARATOR - )[0] - graph = checkpoint_ns_to_graph.get(graph_checkpoint_ns) - if graph is None: - continue - - with ChannelsManager( - graph.channels, saved.checkpoint, saved.config, skip_context=True - ) as ( - channels, - managed, - ): - next_tasks = prepare_next_tasks( - saved.checkpoint, - graph.nodes, - channels, - managed, - saved.config, - saved.metadata.get("step", -1) + 1, - for_execution=False, - ) - state_snapshot = StateSnapshot( - read_channels(channels, graph.stream_channels_asis), - tuple(t.name for t in next_tasks), - saved.config, - saved.metadata, - saved.checkpoint["ts"], - saved.parent_config, - tasks_w_writes(next_tasks, saved.pending_writes), - ) - - checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot - - if not checkpoint_ns_to_state_snapshots: - return StateSnapshot( - values={}, - next=(), - config=config, - metadata=None, - created_at=None, - parent_config=None, - tasks=(), - ) - - state_snapshot = assemble_state_snapshot_hierarchy( - checkpoint_ns, checkpoint_ns_to_state_snapshots - ) - return state_snapshot - - -async def _prepare_state_snapshot_async( - config: RunnableConfig, - checkpoint_ns_to_graph: dict[str, Pregel], - checkpoint_tuples: AsyncIterator[CheckpointTuple], -) -> StateSnapshot: - checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - checkpoint_id = config["configurable"].get("checkpoint_id") - checkpoint_ns_to_state_snapshots: dict[str, StateSnapshot] = {} - async for saved in checkpoint_tuples: - saved_checkpoint_ns = saved.config["configurable"]["checkpoint_ns"] - saved_checkpoint_id = saved.config["configurable"]["checkpoint_id"] - if checkpoint_id and saved_checkpoint_id != checkpoint_id: - continue - - graph_checkpoint_ns = saved_checkpoint_ns.split( - SEND_CHECKPOINT_NAMESPACE_SEPARATOR - )[0] - graph = checkpoint_ns_to_graph.get(graph_checkpoint_ns) - if graph is None: - continue - - async with AsyncChannelsManager( - graph.channels, saved.checkpoint, saved.config, skip_context=True - ) as (channels, managed): - next_tasks = prepare_next_tasks( - saved.checkpoint, - graph.nodes, - channels, - managed, - saved.config, - saved.metadata.get("step", -1) + 1, - for_execution=False, - ) - state_snapshot = StateSnapshot( - read_channels(channels, graph.stream_channels_asis), - tuple(t.name for t in next_tasks), - saved.config, - saved.metadata, - saved.checkpoint["ts"], - saved.parent_config, - tasks_w_writes(next_tasks, saved.pending_writes), - ) - - checkpoint_ns_to_state_snapshots[saved_checkpoint_ns] = state_snapshot - - if not checkpoint_ns_to_state_snapshots: - return StateSnapshot( - values={}, - next=(), - config=config, - metadata=None, - created_at=None, - parent_config=None, - tasks=(), - ) - - state_snapshot = assemble_state_snapshot_hierarchy( - checkpoint_ns, checkpoint_ns_to_state_snapshots - ) - return state_snapshot - - -def _has_nested_interrupts( - graph: Pregel, -) -> bool: - for child in graph.subgraphs: - if child.interrupt_after_nodes or child.interrupt_before_nodes: - return True - else: - return False - - class Pregel( RunnableSerializable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]] ): @@ -526,60 +349,211 @@ class Pregel( k for k in self.channels if isinstance(self.channels[k], BaseChannel) ] - @property - def subgraphs(self) -> Iterator[Pregel]: - for node in self.nodes.values(): + def get_subgraphs(self, recurse: bool = False) -> Iterator[tuple[str, Pregel]]: + for name, node in self.nodes.items(): + # find the subgraph, if any + graph: Optional[Pregel] = None if isinstance(node.bound, Pregel): - yield node.bound - yield from node.bound.subgraphs + graph = node.bound elif isinstance(node.bound, RunnableSequence): for runnable in node.bound.steps: if isinstance(runnable, Pregel): - yield runnable - yield from runnable.subgraphs + graph = runnable + break + # if found, yield recursively + if graph: + yield name, graph + if recurse: + yield from ( + (f"{name}{NS_SEP}{n}", s) + for n, s in graph.get_subgraphs(recurse=recurse) + ) - def get_state(self, config: RunnableConfig) -> StateSnapshot: + async def aget_subgraphs( + self, recursive: bool = False + ) -> AsyncIterator[tuple[str, Pregel]]: + for name, node in self.get_subgraphs(recurse=recursive): + yield name, node + + def _prepare_state_snapshot( + self, + config: RunnableConfig, + saved: Optional[CheckpointTuple], + recurse: Optional[BaseCheckpointSaver] = False, + ) -> StateSnapshot: + if not saved: + return StateSnapshot( + values={}, + next=(), + config=config, + metadata=None, + created_at=None, + parent_config=None, + tasks=(), + ) + + with ChannelsManager( + self.channels, saved.checkpoint, saved.config, skip_context=True + ) as (channels, managed): + # tasks for this checkpoint + next_tasks = prepare_next_tasks( + saved.checkpoint, + self.nodes, + channels, + managed, + saved.config, + saved.metadata.get("step", -1) + 1, + for_execution=False, + ) + # get the subgraphs + subgraphs = dict(self.get_subgraphs()) + parent_ns = saved.config["configurable"].get("checkpoint_ns", "") + task_states: dict[str, Union[RunnableConfig, StateSnapshot]] = {} + for task in next_tasks: + if task.name not in subgraphs: + continue + # assemble checkpoint_ns for this task + task_ns = f"{task.name}{NS_END}{task.id}" + if parent_ns: + task_ns = f"{parent_ns}{NS_SEP}{task_ns}" + if not recurse: + # set config as signal that subgraph checkpoints exist + config = { + "configurable": { + "thread_id": saved.config["configurable"]["thread_id"], + "checkpoint_ns": task_ns, + } + } + task_states[task.id] = config + else: + # get the state of the subgraph + config = { + "configurable": { + CONFIG_KEY_CHECKPOINTER: recurse, + "thread_id": saved.config["configurable"]["thread_id"], + "checkpoint_ns": task_ns, + } + } + task_states[task.id] = subgraphs[task.name].get_state( + config, subgraphs=True + ) + # assemble the state snapshot + return StateSnapshot( + read_channels(channels, self.stream_channels_asis), + tuple(t.name for t in next_tasks), + saved.config, + saved.metadata, + saved.checkpoint["ts"], + saved.parent_config, + tasks_w_writes(next_tasks, saved.pending_writes, task_states), + ) + + async def _aprepare_state_snapshot( + self, + config: RunnableConfig, + saved: Optional[CheckpointTuple], + recurse: Optional[BaseCheckpointSaver] = False, + ) -> StateSnapshot: + if not saved: + return StateSnapshot( + values={}, + next=(), + config=config, + metadata=None, + created_at=None, + parent_config=None, + tasks=(), + ) + + async with AsyncChannelsManager( + self.channels, saved.checkpoint, saved.config, skip_context=True + ) as ( + channels, + managed, + ): + # tasks for this checkpoint + next_tasks = prepare_next_tasks( + saved.checkpoint, + self.nodes, + channels, + managed, + saved.config, + saved.metadata.get("step", -1) + 1, + for_execution=False, + ) + # get the subgraphs + subgraphs = dict(self.get_subgraphs()) + parent_ns = saved.config["configurable"].get("checkpoint_ns", "") + task_states: dict[str, Union[RunnableConfig, StateSnapshot]] = {} + for task in next_tasks: + if task.name not in subgraphs: + continue + # assemble checkpoint_ns for this task + task_ns = f"{task.name}{NS_END}{task.id}" + if parent_ns: + task_ns = f"{parent_ns}{NS_SEP}{task_ns}" + if not recurse: + # set config as signal that subgraph checkpoints exist + config = { + "configurable": { + "thread_id": saved.config["configurable"]["thread_id"], + "checkpoint_ns": task_ns, + } + } + task_states[task.id] = config + else: + # get the state of the subgraph + config = { + "configurable": { + CONFIG_KEY_CHECKPOINTER: recurse, + "thread_id": saved.config["configurable"]["thread_id"], + "checkpoint_ns": task_ns, + } + } + task_states[task.id] = await subgraphs[task.name].aget_state( + config, subgraphs=recurse + ) + # assemble the state snapshot + return StateSnapshot( + read_channels(channels, self.stream_channels_asis), + tuple(t.name for t in next_tasks), + saved.config, + saved.metadata, + saved.checkpoint["ts"], + saved.parent_config, + tasks_w_writes(next_tasks, saved.pending_writes, task_states), + ) + + def get_state( + self, config: RunnableConfig, *, subgraphs: bool = False + ) -> StateSnapshot: """Get the current state of the graph.""" - if not self.checkpointer: + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: raise ValueError("No checkpointer set") config = merge_configs(self.config, config) if self.config else config - saved = self.checkpointer.get_tuple(config) - checkpoint_config = saved.config if saved else config - checkpoint_ns_to_graph: dict[str, Pregel] = _get_checkpoint_ns_to_graph(self) - - # we only lookup subgraph checkpoints if we actually have subgraphs - if len(set(checkpoint_ns_to_graph)) == 1: - checkpoint_tuples = (saved,) - else: - checkpoint_tuples = self.checkpointer.list(saved.config) - - return _prepare_state_snapshot( - checkpoint_config, checkpoint_ns_to_graph, checkpoint_tuples + saved = checkpointer.get_tuple(config) + return self._prepare_state_snapshot( + config, saved, recurse=checkpointer if subgraphs else None ) - async def aget_state(self, config: RunnableConfig) -> StateSnapshot: + async def aget_state( + self, config: RunnableConfig, *, subgraphs: bool = False + ) -> StateSnapshot: """Get the current state of the graph.""" - if not self.checkpointer: + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: raise ValueError("No checkpointer set") config = merge_configs(self.config, config) if self.config else config - saved = await self.checkpointer.aget_tuple(config) - checkpoint_config = saved.config if saved else config - checkpoint_ns_to_graph: dict[str, Pregel] = _get_checkpoint_ns_to_graph(self) - - # we only lookup subgraph checkpoints if we actually have subgraphs - if len(set(checkpoint_ns_to_graph)) == 1: - - async def alist_checkpoints(): - yield saved - - checkpoint_tuples = alist_checkpoints() - else: - checkpoint_tuples = self.checkpointer.alist(saved.config) - - return await _prepare_state_snapshot_async( - checkpoint_config, checkpoint_ns_to_graph, checkpoint_tuples + saved = await checkpointer.aget_tuple(config) + return await self._aprepare_state_snapshot( + config, saved, recurse=checkpointer if subgraphs else None ) def get_state_history( @@ -591,37 +565,47 @@ class Pregel( limit: Optional[int] = None, ) -> Iterator[StateSnapshot]: """Get the history of the state of the graph.""" - if not self.checkpointer: + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: raise ValueError("No checkpointer set") + if ( - filter is not None - and signature(self.checkpointer.list).parameters.get("filter") is None + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) + ) + # find the subgraph with the matching name + for name, pregel in self.get_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + yield from pregel.get_state_history( + { + "configurable": { + **config["configurable"], + CONFIG_KEY_CHECKPOINTER: checkpointer, + } + }, + filter=filter, + before=before, + limit=limit, + ) + return + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") + + config = merge_configs( + self.config, config, {"configurable": {"checkpoint_ns": checkpoint_ns}} + ) + # eagerly consume list() to avoid holding up the db cursor + for checkpoint_tuple in list( + checkpointer.list(config, before=before, limit=limit, filter=filter) ): - raise ValueError("Checkpointer does not support filtering") - - checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - checkpoint_ns_to_graph = _get_checkpoint_ns_to_graph(self) - # find all matching checkpoint tuples for parent and subgraphs - checkpoint_tuples = [ - checkpoint_tuple - for checkpoint_tuple in self.checkpointer.list( - merge_configs(self.config, config) if self.config else config, - before=before, - limit=limit, - filter=filter, + yield self._prepare_state_snapshot( + checkpoint_tuple.config, checkpoint_tuple ) - ] - for checkpoint_tuple in checkpoint_tuples: - if ( - checkpoint_tuple.config["configurable"]["checkpoint_ns"] - != checkpoint_ns - ): - continue - - state_snapshot = _prepare_state_snapshot( - checkpoint_tuple.config, checkpoint_ns_to_graph, iter(checkpoint_tuples) - ) - yield state_snapshot async def aget_state_history( self, @@ -632,43 +616,51 @@ class Pregel( limit: Optional[int] = None, ) -> AsyncIterator[StateSnapshot]: """Get the history of the state of the graph.""" - if not self.checkpointer: + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: raise ValueError("No checkpointer set") + if ( - filter is not None - and signature(self.checkpointer.list).parameters.get("filter") is None - ): - raise ValueError("Checkpointer does not support filtering") - - checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - checkpoint_ns_to_graph = _get_checkpoint_ns_to_graph(self) - # find all matching checkpoint tuples for parent and subgraphs - checkpoint_tuples = [ - checkpoint_tuple - async for checkpoint_tuple in self.checkpointer.alist( - merge_configs(self.config, config) if self.config else config, - before=before, - limit=limit, - filter=filter, + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) ) - ] + # find the subgraph with the matching name + for name, pregel in self.get_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + async for state in pregel.aget_state_history( + { + "configurable": { + **config["configurable"], + CONFIG_KEY_CHECKPOINTER: checkpointer, + } + }, + filter=filter, + before=before, + limit=limit, + ): + yield state + return + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") - # turn matching checkpoint tuples into an async iterator - async def alist_checkpoints() -> AsyncIterator[CheckpointTuple]: - for checkpoint_tuple in checkpoint_tuples: - yield checkpoint_tuple - - for checkpoint_tuple in checkpoint_tuples: - if ( - checkpoint_tuple.config["configurable"]["checkpoint_ns"] - != checkpoint_ns - ): - continue - - state_snapshot = await _prepare_state_snapshot_async( - checkpoint_tuple.config, checkpoint_ns_to_graph, alist_checkpoints() + config = merge_configs( + self.config, config, {"configurable": {"checkpoint_ns": checkpoint_ns}} + ) + # eagerly consume list() to avoid holding up the db cursor + for checkpoint_tuple in [ + c + async for c in checkpointer.alist( + config, before=before, limit=limit, filter=filter + ) + ]: + yield await self._aprepare_state_snapshot( + checkpoint_tuple.config, checkpoint_tuple ) - yield state_snapshot def update_state( self, @@ -716,6 +708,7 @@ class Pregel( "source": "update", "step": step + 1, "writes": {}, + "parents": saved.metadata.get("parents", {}) if saved else {}, }, {}, ) @@ -805,6 +798,7 @@ class Pregel( "source": "update", "step": step + 1, "writes": {as_node: values}, + "parents": saved.metadata.get("parents", {}) if saved else {}, }, get_new_channel_versions( checkpoint_previous_versions, checkpoint["channel_versions"] @@ -853,6 +847,7 @@ class Pregel( "source": "update", "step": step + 1, "writes": {}, + "parents": saved.metadata.get("parents", {}) if saved else {}, }, {}, ) @@ -942,6 +937,7 @@ class Pregel( "source": "update", "step": step + 1, "writes": {as_node: values}, + "parents": saved.metadata.get("parents", {}) if saved else {}, }, get_new_channel_versions( checkpoint_previous_versions, checkpoint["channel_versions"] @@ -979,10 +975,8 @@ class Pregel( if config and config.get("configurable", {}).get(CONFIG_KEY_READ) is not None: # if being called as a node in another graph, always use values mode stream_mode = ["values"] - if ( - config is not None - and config.get("configurable", {}).get(CONFIG_KEY_CHECKPOINTER) - and (interrupt_after or interrupt_before or _has_nested_interrupts(self)) + if config is not None and config.get("configurable", {}).get( + CONFIG_KEY_CHECKPOINTER ): checkpointer: Optional[BaseCheckpointSaver] = config["configurable"][ CONFIG_KEY_CHECKPOINTER diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 2f1589573..936c609a4 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -31,13 +31,14 @@ from langgraph.checkpoint.base import ( create_checkpoint, ) from langgraph.constants import ( - CHECKPOINT_NAMESPACE_SEPARATOR, + CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_READ, CONFIG_KEY_RESUMING, CONFIG_KEY_SEND, CONFIG_KEY_TASK_ID, INTERRUPT, + NS_SEP, RESERVED, TAG_HIDDEN, TASKS, @@ -272,7 +273,8 @@ def prepare_next_tasks( checkpointer: Optional[BaseCheckpointSaver] = None, manager: Union[None, ParentRunManager, AsyncParentRunManager] = None, ) -> Union[list[PregelTask], list[PregelExecutableTask]]: - parent_ns = config.get("configurable", {}).get("checkpoint_ns", "") + configurable = config.get("configurable", {}) + parent_ns = configurable.get("checkpoint_ns", "") tasks: Union[list[PregelTask], list[PregelExecutableTask]] = [] # Consume pending packets for packet in checkpoint["pending_sends"]: @@ -291,9 +293,7 @@ def prepare_next_tasks( "langgraph_task_idx": len(tasks), } checkpoint_ns = ( - f"{parent_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{packet.node}" - if parent_ns - else packet.node + f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node ) task_id = str( uuid5(UUID(checkpoint["id"]), json.dumps((checkpoint_ns, metadata))) @@ -341,9 +341,16 @@ def prepare_next_tasks( PregelTaskWrites(packet.node, writes, triggers), config, ), - CONFIG_KEY_CHECKPOINTER: checkpointer, + CONFIG_KEY_CHECKPOINTER: ( + checkpointer + or configurable.get(CONFIG_KEY_CHECKPOINTER) + ), + CONFIG_KEY_CHECKPOINT_MAP: { + **configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}), + parent_ns: checkpoint["id"], + }, CONFIG_KEY_RESUMING: is_resuming, - "checkpoint_id": checkpoint["id"], + "checkpoint_id": None, "checkpoint_ns": f"{checkpoint_ns}:{task_id}", }, ), @@ -388,11 +395,7 @@ def prepare_next_tasks( "langgraph_triggers": triggers, "langgraph_task_idx": len(tasks), } - checkpoint_ns = ( - f"{parent_ns}{CHECKPOINT_NAMESPACE_SEPARATOR}{name}" - if parent_ns - else name - ) + checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name task_id = str( uuid5( UUID(checkpoint["id"]), @@ -443,13 +446,16 @@ def prepare_next_tasks( ), CONFIG_KEY_CHECKPOINTER: ( checkpointer - or config["configurable"].get( - CONFIG_KEY_CHECKPOINTER - ) + or configurable.get(CONFIG_KEY_CHECKPOINTER) ), + CONFIG_KEY_CHECKPOINT_MAP: { + **configurable.get( + CONFIG_KEY_CHECKPOINT_MAP, {} + ), + parent_ns: checkpoint["id"], + }, CONFIG_KEY_RESUMING: is_resuming, - "checkpoint_id": checkpoint["id"], - "checkpoint_ns": checkpoint_ns, + "checkpoint_ns": f"{checkpoint_ns}:{task_id}", }, ), triggers, diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index 72b6cdbe5..fe32fe600 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -13,7 +13,7 @@ from langgraph.channels.base import BaseChannel from langgraph.checkpoint.base import Checkpoint, CheckpointMetadata, PendingWrite from langgraph.constants import ERROR, INTERRUPT, TAG_HIDDEN from langgraph.pregel.io import read_channels -from langgraph.pregel.types import PregelExecutableTask, PregelTask +from langgraph.pregel.types import PregelExecutableTask, PregelTask, StateSnapshot class TaskPayload(TypedDict): @@ -160,7 +160,7 @@ def map_debug_checkpoint( "name": t.name, "interrupts": tuple(asdict(i) for i in t.interrupts), } - for t in tasks_w_writes(tasks, pending_writes) + for t in tasks_w_writes(tasks, pending_writes, None) ], }, } @@ -215,6 +215,7 @@ def print_step_checkpoint( def tasks_w_writes( tasks: list[PregelExecutableTask], pending_writes: Optional[list[PendingWrite]], + states: Optional[dict[str, Union[RunnableConfig, StateSnapshot]]], ) -> tuple[PregelTask, ...]: pending_writes = pending_writes or [] return tuple( @@ -232,6 +233,7 @@ def tasks_w_writes( tuple( v for tid, n, v in pending_writes if tid == task.id and n == INTERRUPT ), + states.get(task.id) if states else None, ) for task in tasks ) diff --git a/libs/langgraph/langgraph/pregel/get_state.py b/libs/langgraph/langgraph/pregel/get_state.py index 457ab6b4f..79e5f0bf8 100644 --- a/libs/langgraph/langgraph/pregel/get_state.py +++ b/libs/langgraph/langgraph/pregel/get_state.py @@ -1,4 +1,4 @@ -from langgraph.constants import CHECKPOINT_NAMESPACE_SEPARATOR +from langgraph.constants import NS_SEP from langgraph.pregel.types import StateSnapshot @@ -8,26 +8,26 @@ def assemble_state_snapshot_hierarchy( ) -> StateSnapshot: checkpoint_ns_list_to_visit = sorted( checkpoint_ns_to_state_snapshots.keys(), - key=lambda x: len(x.split(CHECKPOINT_NAMESPACE_SEPARATOR)), + key=lambda x: len(x.split(NS_SEP)), ) while checkpoint_ns_list_to_visit: checkpoint_ns = checkpoint_ns_list_to_visit.pop() state_snapshot = checkpoint_ns_to_state_snapshots[checkpoint_ns] - *path, subgraph_node = checkpoint_ns.split(CHECKPOINT_NAMESPACE_SEPARATOR) - parent_checkpoint_ns = CHECKPOINT_NAMESPACE_SEPARATOR.join(path) + *path, subgraph_node = checkpoint_ns.split(NS_SEP) + parent_checkpoint_ns = NS_SEP.join(path) if subgraph_node and ( parent_state_snapshot := checkpoint_ns_to_state_snapshots.get( parent_checkpoint_ns ) ): parent_subgraph_snapshots = { - **(parent_state_snapshot.subgraph_state_snapshots or {}), + **(parent_state_snapshot.subgraphs or {}), subgraph_node: state_snapshot, } checkpoint_ns_to_state_snapshots[parent_checkpoint_ns] = ( checkpoint_ns_to_state_snapshots[ parent_checkpoint_ns - ]._replace(subgraph_state_snapshots=parent_subgraph_snapshots) + ]._replace(subgraphs=parent_subgraph_snapshots) ) state_snapshot = checkpoint_ns_to_state_snapshots.pop(root_checkpoint_ns, None) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 509f6eabc..58441f831 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -36,6 +36,7 @@ from langgraph.checkpoint.base import ( empty_checkpoint, ) from langgraph.constants import ( + CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_READ, CONFIG_KEY_RESUMING, ERROR, @@ -361,21 +362,15 @@ class PregelLoop: def _put_checkpoint(self, metadata: CheckpointMetadata) -> None: # assign step metadata["step"] = self.step + metadata["parents"] = self.config["configurable"].get( + CONFIG_KEY_CHECKPOINT_MAP, {} + ) # bail if no checkpointer if self._checkpointer_put_after_previous is not None: # create new checkpoint self.checkpoint_metadata = metadata self.checkpoint = create_checkpoint( - self.checkpoint, - self.channels, - self.step, - # child graphs keep at most one checkpoint per parent checkpoint - # this is achieved by writing child checkpoints as progress is made - # (so that error recovery / resuming from interrupt don't lose work) - # but doing so always with an id equal to that of the parent checkpoint - id=self.config["configurable"]["checkpoint_id"] - if self.is_nested - else None, + self.checkpoint, self.channels, self.step ) self.checkpoint_config = { diff --git a/libs/langgraph/langgraph/pregel/metadata.py b/libs/langgraph/langgraph/pregel/metadata.py new file mode 100644 index 000000000..e69de29bb diff --git a/libs/langgraph/langgraph/pregel/types.py b/libs/langgraph/langgraph/pregel/types.py index d4881452b..d1ac00fd1 100644 --- a/libs/langgraph/langgraph/pregel/types.py +++ b/libs/langgraph/langgraph/pregel/types.py @@ -62,6 +62,7 @@ class PregelTask(NamedTuple): name: str error: Optional[Exception] = None interrupts: tuple[Interrupt, ...] = () + state: Union[None, RunnableConfig, "StateSnapshot"] = None class PregelExecutableTask(NamedTuple): @@ -92,8 +93,6 @@ class StateSnapshot(NamedTuple): """Config used to fetch the parent snapshot, if any""" tasks: tuple[PregelTask, ...] """Tasks to execute in this step. If already attempted, may contain an error.""" - subgraph_state_snapshots: Optional[dict[str, "StateSnapshot"]] = None - """State snapshots of subgraphs represented as a mapping from checkpoint namespace (`checkpoint_ns`) to snapshot.""" All = Literal["*"] diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index d273ea9a7..ba58cda8c 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -805,6 +805,7 @@ "name": "tools" }, "metadata": { + "parents": {}, "version": 2, "variant": "b" } @@ -856,7 +857,8 @@ graph TD; __start__([__start__]):::first agent(agent) - tools(tools


version = 2 + tools(tools
parents = {} + version = 2 variant = b) __end__([__end__]):::last __start__ --> agent; @@ -952,6 +954,7 @@ "name": "tools" }, "metadata": { + "parents": {}, "version": 2, "variant": "b" } @@ -1084,6 +1087,8 @@ }), 'id': 'tools', 'metadata': dict({ + 'parents': dict({ + }), 'variant': 'b', 'version': 2, }), @@ -1103,7 +1108,8 @@ graph TD; __start__([__start__]):::first agent(agent
__interrupt = after) - tools(tools
version = 2 + tools(tools
parents = {} + version = 2 variant = b) __end__([__end__]):::last __start__ --> agent; @@ -1150,6 +1156,7 @@ "name": "tools" }, "metadata": { + "parents": {}, "version": 2, "variant": "b" } @@ -1201,7 +1208,8 @@ graph TD; __start__([__start__]):::first agent(agent) - tools(tools
version = 2 + tools(tools
parents = {} + version = 2 variant = b) __end__([__end__]):::last __start__ --> agent; @@ -1297,6 +1305,7 @@ "name": "tools" }, "metadata": { + "parents": {}, "version": 2, "variant": "b" } @@ -1429,6 +1438,8 @@ }), 'id': 'tools', 'metadata': dict({ + 'parents': dict({ + }), 'variant': 'b', 'version': 2, }), @@ -1448,7 +1459,8 @@ graph TD; __start__([__start__]):::first agent(agent
__interrupt = after) - tools(tools
version = 2 + tools(tools
parents = {} + version = 2 variant = b) __end__([__end__]):::last __start__ --> agent; @@ -1495,6 +1507,7 @@ "name": "tools" }, "metadata": { + "parents": {}, "version": 2, "variant": "b" } @@ -1546,7 +1559,8 @@ graph TD; __start__([__start__]):::first agent(agent) - tools(tools
version = 2 + tools(tools
parents = {} + version = 2 variant = b) __end__([__end__]):::last __start__ --> agent; @@ -1642,6 +1656,7 @@ "name": "tools" }, "metadata": { + "parents": {}, "version": 2, "variant": "b" } @@ -1774,6 +1789,8 @@ }), 'id': 'tools', 'metadata': dict({ + 'parents': dict({ + }), 'variant': 'b', 'version': 2, }), @@ -1793,7 +1810,8 @@ graph TD; __start__([__start__]):::first agent(agent
__interrupt = after) - tools(tools
version = 2 + tools(tools
parents = {} + version = 2 variant = b) __end__([__end__]):::last __start__ --> agent; @@ -1840,6 +1858,7 @@ "name": "tools" }, "metadata": { + "parents": {}, "version": 2, "variant": "b" } @@ -1891,7 +1910,8 @@ graph TD; __start__([__start__]):::first agent(agent) - tools(tools
version = 2 + tools(tools
parents = {} + version = 2 variant = b) __end__([__end__]):::last __start__ --> agent; @@ -1987,6 +2007,7 @@ "name": "tools" }, "metadata": { + "parents": {}, "version": 2, "variant": "b" } @@ -2119,6 +2140,8 @@ }), 'id': 'tools', 'metadata': dict({ + 'parents': dict({ + }), 'variant': 'b', 'version': 2, }), @@ -2138,7 +2161,8 @@ graph TD; __start__([__start__]):::first agent(agent
__interrupt = after) - tools(tools
version = 2 + tools(tools
parents = {} + version = 2 variant = b) __end__([__end__]):::last __start__ --> agent; @@ -2185,6 +2209,7 @@ "name": "tools" }, "metadata": { + "parents": {}, "version": 2, "variant": "b" } @@ -2236,7 +2261,8 @@ graph TD; __start__([__start__]):::first agent(agent) - tools(tools
version = 2 + tools(tools
parents = {} + version = 2 variant = b) __end__([__end__]):::last __start__ --> agent; @@ -2332,6 +2358,7 @@ "name": "tools" }, "metadata": { + "parents": {}, "version": 2, "variant": "b" } @@ -2464,6 +2491,8 @@ }), 'id': 'tools', 'metadata': dict({ + 'parents': dict({ + }), 'variant': 'b', 'version': 2, }), @@ -2483,7 +2512,8 @@ graph TD; __start__([__start__]):::first agent(agent
__interrupt = after) - tools(tools
version = 2 + tools(tools
parents = {} + version = 2 variant = b) __end__([__end__]):::last __start__ --> agent; @@ -2658,76 +2688,6 @@ ''' # --- -# name: test_conditional_state_graph_with_list_edge_inputs - ''' - { - "nodes": [ - { - "id": "__start__", - "type": "schema", - "data": "__start__" - }, - { - "id": "A", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "A" - } - }, - { - "id": "B", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "B" - } - }, - { - "id": "__end__", - "type": "schema", - "data": "__end__" - } - ], - "edges": [ - { - "source": "A", - "target": "__end__" - }, - { - "source": "B", - "target": "__end__" - }, - { - "source": "__start__", - "target": "A" - }, - { - "source": "__start__", - "target": "B" - } - ] - } - ''' -# --- -# name: test_conditional_state_graph_with_list_edge_inputs.1 - ''' - graph TD; - A --> __end__; - B --> __end__; - __start__ --> A; - __start__ --> B; - - ''' -# --- # name: test_conditional_state_graph[postgres] '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' # --- @@ -3052,6 +3012,76 @@ ''' # --- +# name: test_conditional_state_graph_with_list_edge_inputs + ''' + { + "nodes": [ + { + "id": "__start__", + "type": "schema", + "data": "__start__" + }, + { + "id": "A", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "A" + } + }, + { + "id": "B", + "type": "runnable", + "data": { + "id": [ + "langgraph", + "utils", + "RunnableCallable" + ], + "name": "B" + } + }, + { + "id": "__end__", + "type": "schema", + "data": "__end__" + } + ], + "edges": [ + { + "source": "A", + "target": "__end__" + }, + { + "source": "B", + "target": "__end__" + }, + { + "source": "__start__", + "target": "A" + }, + { + "source": "__start__", + "target": "B" + } + ] + } + ''' +# --- +# name: test_conditional_state_graph_with_list_edge_inputs.1 + ''' + graph TD; + A --> __end__; + B --> __end__; + __start__ --> A; + __start__ --> B; + + ''' +# --- # name: test_dynamic_interrupt ''' %%{init: {'flowchart': {'curve': 'linear'}}}%% diff --git a/libs/langgraph/tests/any_str.py b/libs/langgraph/tests/any_str.py index 836cf9371..28a67ddf6 100644 --- a/libs/langgraph/tests/any_str.py +++ b/libs/langgraph/tests/any_str.py @@ -2,16 +2,35 @@ from typing import Any, Sequence class AnyStr(str): - def __init__(self) -> None: + def __init__(self, prefix: str = "") -> None: super().__init__() + self.prefix = prefix def __eq__(self, other: object) -> bool: - return isinstance(other, str) + return isinstance(other, str) and other.startswith(self.prefix) def __hash__(self) -> int: return hash(str(self)) +class AnyDict(dict): + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + + def __eq__(self, other: object) -> bool: + print("did we get here") + if not isinstance(other, dict) or len(self) != len(other): + return False + for k, v in self.items(): + if kk := next((kk for kk in other if kk == k), None): + if v == other[kk]: + continue + else: + return False + else: + return True + + class AnyVersion: def __init__(self) -> None: super().__init__() diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index ba9c88afc..453a87b08 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -18,6 +18,7 @@ from langchain_core.tools import BaseTool from langchain_core.tools import tool as dec_tool from pydantic import BaseModel as BaseModelV2 +from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.prebuilt import ToolNode, ValidationNode, create_react_agent from langgraph.prebuilt.tool_node import InjectedState from tests.messages import _AnyIdHumanMessage @@ -55,7 +56,9 @@ class FakeToolCallingModel(BaseChatModel): ["memory", "sqlite", "postgres", "postgres_pipe"], ) def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + "checkpointer_" + checkpointer_name + ) model = FakeToolCallingModel() agent = create_react_agent(model, [], checkpointer=checkpointer) @@ -76,6 +79,7 @@ def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) -> "agent": "agent", } assert saved.metadata == { + "parents": {}, "source": "loop", "writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}}, "step": 1, @@ -90,7 +94,9 @@ def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) -> async def test_no_modifier_async( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") + checkpointer: BaseCheckpointSaver = request.getfixturevalue( + f"checkpointer_{checkpointer_name}" + ) model = FakeToolCallingModel() @@ -112,6 +118,7 @@ async def test_no_modifier_async( "agent": "agent", } assert saved.metadata == { + "parents": {}, "source": "loop", "writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}}, "step": 1, diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 492d87570..e5d263121 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -69,7 +69,7 @@ from langgraph.pregel import ( from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore -from tests.any_str import AnyStr, AnyVersion, UnsortedSequence +from tests.any_str import AnyDict, AnyStr, AnyVersion, UnsortedSequence from tests.conftest import ALL_CHECKPOINTERS_SYNC from tests.fake_tracer import FakeTracer from tests.memory_assert import ( @@ -670,7 +670,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 6, "writes": {"two": 5}}, + metadata={"parents": {}, "source": "loop", "step": 6, "writes": {"two": 5}}, created_at=AnyStr(), parent_config=history[1].config, ), @@ -685,7 +685,12 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 5, "writes": {"one": None}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 5, + "writes": {"one": None}, + }, created_at=AnyStr(), parent_config=history[2].config, ), @@ -700,7 +705,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": 4, "writes": 3}, + metadata={"parents": {}, "source": "input", "step": 4, "writes": 3}, created_at=AnyStr(), parent_config=history[3].config, ), @@ -715,7 +720,12 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 3, "writes": {"one": None}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"one": None}, + }, created_at=AnyStr(), parent_config=history[4].config, ), @@ -730,7 +740,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": 2, "writes": 20}, + metadata={"parents": {}, "source": "input", "step": 2, "writes": 20}, created_at=AnyStr(), parent_config=history[5].config, ), @@ -745,7 +755,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 1, "writes": {"two": 4}}, + metadata={"parents": {}, "source": "loop", "step": 1, "writes": {"two": 4}}, created_at=AnyStr(), parent_config=history[6].config, ), @@ -760,7 +770,12 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 0, "writes": {"one": None}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 0, + "writes": {"one": None}, + }, created_at=AnyStr(), parent_config=history[7].config, ), @@ -775,7 +790,7 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": -1, "writes": 2}, + metadata={"parents": {}, "source": "input", "step": -1, "writes": 2}, created_at=AnyStr(), parent_config=None, ), @@ -845,7 +860,12 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 5, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 5, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[1].config, ), @@ -860,7 +880,12 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 4, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 4, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[2].config, ), @@ -875,7 +900,12 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 3, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[3].config, ), @@ -890,7 +920,12 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 2, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 2, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[4].config, ), @@ -905,7 +940,12 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 1, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[5].config, ), @@ -920,7 +960,7 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, created_at=AnyStr(), parent_config=history[6].config, ), @@ -935,7 +975,7 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": -1, "writes": 1}, + metadata={"parents": {}, "source": "input", "step": -1, "writes": 1}, created_at=AnyStr(), parent_config=None, ), @@ -1369,7 +1409,12 @@ def test_pending_writes_resume( PregelTask(AnyStr(), "one"), PregelTask(AnyStr(), "two", 'ConnectionError("I\'m not good")'), ) - assert state.metadata == {"source": "loop", "step": 0, "writes": None} + assert state.metadata == { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + } # should contain pending write of "one" checkpoint = checkpointer.get_tuple(thread1) assert checkpoint is not None @@ -1453,6 +1498,7 @@ def test_pending_writes_resume( "channel_values": {"one": "one", "two": "two", "value": 6}, }, metadata={ + "parents": {}, "step": 1, "source": "loop", "writes": {"one": {"value": 2}, "two": {"value": 3}}, @@ -1500,7 +1546,7 @@ def test_pending_writes_resume( "start:two": "__start__", }, }, - metadata={"step": 0, "source": "loop", "writes": None}, + metadata={"parents": {}, "step": 0, "source": "loop", "writes": None}, parent_config={ "configurable": { "thread_id": "1", @@ -1535,7 +1581,7 @@ def test_pending_writes_resume( }, "channel_values": {"__start__": {"value": 1}}, }, - metadata={"step": -1, "source": "input", "writes": {"value": 1}}, + metadata={"parents": {}, "step": -1, "source": "input", "writes": {"value": 1}}, parent_config=None, pending_writes=UnsortedSequence( (AnyStr(), "value", 1), @@ -1992,7 +2038,9 @@ def test_conditional_graph( workflow = Graph() workflow.add_node("agent", agent) - workflow.add_node("tools", execute_tools, metadata={"version": 2, "variant": "b"}) + workflow.add_node( + "tools", execute_tools, metadata={"parents": {}, "version": 2, "variant": "b"} + ) workflow.set_entry_point("agent") @@ -2169,6 +2217,7 @@ def test_conditional_graph( created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], config=app_w_interrupt.checkpointer.get_tuple(config).config, metadata={ + "parents": {}, "source": "loop", "step": 0, "writes": { @@ -2221,6 +2270,7 @@ def test_conditional_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 1, "writes": { @@ -2321,6 +2371,7 @@ def test_conditional_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 4, "writes": { @@ -2382,6 +2433,7 @@ def test_conditional_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 0, "writes": { @@ -2428,6 +2480,7 @@ def test_conditional_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 1, "writes": { @@ -2528,6 +2581,7 @@ def test_conditional_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 4, "writes": { @@ -2589,6 +2643,7 @@ def test_conditional_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 0, "writes": { @@ -3033,6 +3088,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -3074,6 +3130,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -3149,6 +3206,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": { @@ -3197,6 +3255,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -3237,6 +3296,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -3310,6 +3370,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": { @@ -3345,7 +3406,7 @@ def test_conditional_state_graph( next=("agent",), config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[*app_w_interrupt.checkpointer.list(config, limit=2)][-1].config, ) @@ -3371,6 +3432,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -3424,6 +3486,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 2, "writes": { @@ -3488,6 +3551,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -3541,6 +3605,7 @@ def test_conditional_state_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 2, "writes": { @@ -4341,6 +4406,7 @@ def test_state_graph_packets( config=(app_w_interrupt.checkpointer.get_tuple(config)).config, created_at=(app_w_interrupt.checkpointer.get_tuple(config)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -4392,6 +4458,7 @@ def test_state_graph_packets( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=(app_w_interrupt.checkpointer.get_tuple(config)).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -4491,6 +4558,7 @@ def test_state_graph_packets( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=(app_w_interrupt.checkpointer.get_tuple(config)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -4555,6 +4623,7 @@ def test_state_graph_packets( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=(app_w_interrupt.checkpointer.get_tuple(config)).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": { @@ -4840,6 +4909,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -4885,6 +4955,7 @@ def test_message_graph( config=next_config, created_at=AnyStr(), metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -4967,6 +5038,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -5019,6 +5091,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": {"agent": AIMessage(content="answer", id="ai2")}, @@ -5069,6 +5142,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -5114,6 +5188,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -5196,6 +5271,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -5248,6 +5324,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": {"agent": AIMessage(content="answer", id="ai2")}, @@ -5288,6 +5365,7 @@ def test_message_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 6, "writes": {"tools": UnsortedSequence("ai", "an extra message")}, @@ -5565,6 +5643,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -5610,6 +5689,7 @@ def test_root_graph( config=next_config, created_at=AnyStr(), metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -5692,6 +5772,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -5744,6 +5825,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": {"agent": AIMessage(content="answer", id="ai2")}, @@ -5794,6 +5876,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -5839,6 +5922,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -5921,6 +6005,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -5973,6 +6058,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": {"agent": AIMessage(content="answer", id="ai2")}, @@ -6013,6 +6099,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 6, "writes": {"tools": UnsortedSequence("ai", "an extra message")}, @@ -6085,6 +6172,7 @@ def test_root_graph( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 6, "writes": {"tools": UnsortedSequence("ai", "an extra message")}, @@ -6427,11 +6515,13 @@ def test_dynamic_interrupt( } assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ { + "parents": {}, "source": "loop", "step": 0, "writes": None, }, { + "parents": {}, "source": "input", "step": -1, "writes": {"my_key": "value ⛰️", "market": "DE"}, @@ -6449,7 +6539,7 @@ def test_dynamic_interrupt( ), config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, ) @@ -6522,11 +6612,13 @@ def test_start_branch_then( } assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ { + "parents": {}, "source": "loop", "step": 0, "writes": None, }, { + "parents": {}, "source": "input", "step": -1, "writes": {"my_key": "value ⛰️", "market": "DE"}, @@ -6538,7 +6630,7 @@ def test_start_branch_then( next=("tool_two_slow",), config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[*tool_two.checkpointer.list(thread1, limit=2)][-1].config, ) # resume, for same result as above @@ -6553,6 +6645,7 @@ def test_start_branch_then( config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"tool_two_slow": {"my_key": " slow"}}, @@ -6572,7 +6665,7 @@ def test_start_branch_then( next=("tool_two_fast",), config=tool_two.checkpointer.get_tuple(thread2).config, created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[*tool_two.checkpointer.list(thread2, limit=2)][-1].config, ) # resume, for same result as above @@ -6587,6 +6680,7 @@ def test_start_branch_then( config=tool_two.checkpointer.get_tuple(thread2).config, created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"tool_two_fast": {"my_key": " fast"}}, @@ -6606,7 +6700,7 @@ def test_start_branch_then( next=("tool_two_fast",), config=tool_two.checkpointer.get_tuple(thread3).config, created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[*tool_two.checkpointer.list(thread3, limit=2)][-1].config, ) # update state @@ -6618,6 +6712,7 @@ def test_start_branch_then( config=tool_two.checkpointer.get_tuple(thread3).config, created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 1, "writes": {START: {"my_key": "key"}}, @@ -6636,6 +6731,7 @@ def test_start_branch_then( config=tool_two.checkpointer.get_tuple(thread3).config, created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 2, "writes": {"tool_two_fast": {"my_key": " fast"}}, @@ -6705,6 +6801,7 @@ def test_branch_then( }, "values": {"my_key": ""}, "metadata": { + "parents": {}, "source": "input", "step": -1, "writes": {"my_key": "value", "market": "DE"}, @@ -6734,6 +6831,7 @@ def test_branch_then( "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 0, "writes": None, @@ -6786,6 +6884,7 @@ def test_branch_then( "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -6838,6 +6937,7 @@ def test_branch_then( "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 2, "writes": {"tool_two_slow": {"my_key": " slow"}}, @@ -6890,6 +6990,7 @@ def test_branch_then( "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -6921,6 +7022,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -6939,6 +7041,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -6959,6 +7062,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread2).config, created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -6977,6 +7081,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread2).config, created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -7005,6 +7110,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 2, "writes": {"tool_two_slow": {"my_key": " slow"}}, @@ -7024,6 +7130,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 3, "writes": {"tool_two_slow": {"my_key": "er"}}, @@ -7052,6 +7159,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -7070,6 +7178,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread1).config, created_at=tool_two.checkpointer.get_tuple(thread1).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -7090,6 +7199,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread2).config, created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -7108,6 +7218,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread2).config, created_at=tool_two.checkpointer.get_tuple(thread2).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -7126,6 +7237,7 @@ def test_branch_then( config=uconfig, created_at=AnyStr(), metadata={ + "parents": {}, "source": "update", "step": 0, "writes": {START: {"my_key": "key", "market": "DE"}}, @@ -7145,6 +7257,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread3).config, created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -7163,6 +7276,7 @@ def test_branch_then( config=tool_two.checkpointer.get_tuple(thread3).config, created_at=tool_two.checkpointer.get_tuple(thread3).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -7287,6 +7401,7 @@ def test_in_one_fan_out_state_graph_waiting_edge( config=app_w_interrupt.checkpointer.get_tuple(config).config, created_at=app_w_interrupt.checkpointer.get_tuple(config).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 4, "writes": {"retriever_one": {"docs": ["doc5"]}}, @@ -8199,7 +8314,7 @@ def test_nested_graph(snapshot: SnapshotAssertion) -> None: ] -@pytest.mark.repeat(10) +@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str @@ -8264,6 +8379,45 @@ def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + tasks=( + PregelTask( + AnyStr(), + "inner_2", + ), + ), + next=("inner_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), ), ), next=("inner",), @@ -8275,6 +8429,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -8287,47 +8442,6 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=( - PregelTask( - AnyStr(), - "inner_2", - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8340,7 +8454,7 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -8362,6 +8476,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -8386,6 +8501,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -8413,6 +8529,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -8428,7 +8545,46 @@ def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), next=("inner",), config={ "configurable": { @@ -8438,6 +8594,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -8450,42 +8607,6 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - next=(), - tasks=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8498,7 +8619,7 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -8520,6 +8641,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -8569,78 +8691,81 @@ def test_nested_graph_interrupts( "my_key": "hi my value", }, ] - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - ), + history = list(app.get_state_history(config)) + assert history[0] == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state=None, # no state because we haven't entered this node yet ), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] + next=("inner",), + config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + assert history[1] == StateSnapshot( + values={"my_key": "my value"}, + tasks=(PregelTask(AnyStr(), "outer_1"),), + next=("outer_1",), + config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + assert history[2] == StateSnapshot( + values={}, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + ) + # while we're waiting for the node w/ interrupt inside to finish assert [*app.stream(None, config, stream_mode="values")] == [] assert list(app.get_state_history(config)) == [ @@ -8650,6 +8775,45 @@ def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + tasks=( + PregelTask( + AnyStr(), + "inner_2", + ), + ), + next=("inner_2",), + config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), ), ), next=("inner",), @@ -8661,6 +8825,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -8673,47 +8838,6 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=( - PregelTask( - AnyStr(), - "inner_2", - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8726,7 +8850,7 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -8748,6 +8872,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -8777,6 +8902,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -8804,6 +8930,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -8819,7 +8946,46 @@ def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "4", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), next=("inner",), config={ "configurable": { @@ -8829,6 +8995,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -8841,42 +9008,6 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ), StateSnapshot( values={"my_key": "my value"}, @@ -8889,7 +9020,7 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -8911,6 +9042,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -8938,6 +9070,45 @@ def test_nested_graph_interrupts( PregelTask( AnyStr(), "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + tasks=( + PregelTask( + AnyStr(), + name="inner_2", + ), + ), + next=("inner_2",), + config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), ), ), next=("inner",), @@ -8949,6 +9120,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -8961,47 +9133,6 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=( - PregelTask( - AnyStr(), - name="inner_2", - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ), StateSnapshot( values={"my_key": "my value"}, @@ -9014,7 +9145,7 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -9036,6 +9167,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -9063,6 +9195,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -9078,7 +9211,46 @@ def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), next=("inner",), config={ "configurable": { @@ -9088,6 +9260,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -9100,42 +9273,6 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ), StateSnapshot( values={"my_key": "my value"}, @@ -9148,7 +9285,7 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -9170,6 +9307,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -9196,6 +9334,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -9223,6 +9362,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -9238,7 +9378,46 @@ def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "5", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), next=("inner",), config={ "configurable": { @@ -9248,6 +9427,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -9260,42 +9440,6 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ), StateSnapshot( values={"my_key": "my value"}, @@ -9308,7 +9452,7 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -9330,6 +9474,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -9347,6 +9492,40 @@ def test_nested_graph_interrupts( } state_history = [c for c in app.get_state_history(config)] assert state_history == [ + StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + tasks=(PregelTask(AnyStr(), "inner_2"),), + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), StateSnapshot( values={"my_key": "hi my value"}, tasks=(PregelTask(AnyStr(), "inner"),), @@ -9359,6 +9538,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -9371,42 +9551,6 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ), StateSnapshot( values={"my_key": "my value"}, @@ -9419,7 +9563,7 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -9441,6 +9585,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -9451,26 +9596,49 @@ def test_nested_graph_interrupts( ] child_state_history = [ c - for c in app.get_state_history( - {"configurable": {"thread_id": "6", "checkpoint_ns": "inner"}} + for c in app.checkpointer.list( + { + "configurable": { + "thread_id": "6", + "checkpoint_ns": f"inner:{state_history[0].tasks[0].id}", + } + } ) ] assert child_state_history == [ - StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), + CheckpointTuple( config={ "configurable": { "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), + "checkpoint_ns": "inner:478a8877-4528-5ecf-9ba3-773c53e2db7f", + "checkpoint_id": "1ef64d0c-0c7a-63ee-8001-4c403fb2fdd8", } }, + checkpoint={ + "v": 1, + "ts": "2024-08-28T00:02:02.205896+00:00", + "id": "1ef64d0c-0c7a-63ee-8001-4c403fb2fdd8", + "channel_values": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + "inner_1": "inner_1", + }, + "channel_versions": { + "__start__": 2, + "my_key": 3, + "start:inner_1": 3, + "inner_1": 3, + "my_other_key": 3, + }, + "versions_seen": { + "__input__": {}, + "__start__": {"__start__": 1}, + "inner_1": {"start:inner_1": 2}, + }, + "pending_sends": [], + }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_1": { @@ -9480,17 +9648,78 @@ def test_nested_graph_interrupts( }, "step": 1, }, - created_at=AnyStr(), parent_config={ "configurable": { "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), + "checkpoint_ns": "inner:478a8877-4528-5ecf-9ba3-773c53e2db7f", + "checkpoint_id": "1ef64d0c-0c78-6f26-8000-2c99f0dae586", } }, + pending_writes=[], + ), + CheckpointTuple( + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner:478a8877-4528-5ecf-9ba3-773c53e2db7f", + "checkpoint_id": "1ef64d0c-0c78-6f26-8000-2c99f0dae586", + } + }, + checkpoint={ + "v": 1, + "ts": "2024-08-28T00:02:02.205364+00:00", + "id": "1ef64d0c-0c78-6f26-8000-2c99f0dae586", + "channel_values": { + "my_key": "hi my value", + "start:inner_1": "__start__", + }, + "channel_versions": {"__start__": 2, "my_key": 2, "start:inner_1": 2}, + "versions_seen": {"__input__": {}, "__start__": {"__start__": 1}}, + "pending_sends": [], + }, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner:478a8877-4528-5ecf-9ba3-773c53e2db7f", + "checkpoint_id": "1ef64d0c-0c78-63dc-bfff-5d20aa222eff", + } + }, + pending_writes=[ + ("1defebd2-5caa-5a51-87f8-e38b7a5c16f9", "inner_1", "inner_1"), + ("1defebd2-5caa-5a51-87f8-e38b7a5c16f9", "my_key", "hi my value here"), + ("1defebd2-5caa-5a51-87f8-e38b7a5c16f9", "my_other_key", "hi my value"), + ], + ), + CheckpointTuple( + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": "inner:478a8877-4528-5ecf-9ba3-773c53e2db7f", + "checkpoint_id": "1ef64d0c-0c78-63dc-bfff-5d20aa222eff", + } + }, + checkpoint={ + "v": 1, + "ts": "2024-08-28T00:02:02.205076+00:00", + "id": "1ef64d0c-0c78-63dc-bfff-5d20aa222eff", + "channel_values": {"__start__": {"my_key": "hi my value"}}, + "channel_versions": {"__start__": 1}, + "versions_seen": {"__input__": {}}, + "pending_sends": [], + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"my_key": "hi my value"}, + "step": -1, + }, + parent_config=None, + pending_writes=[ + ("494a4aca-177b-5e8a-986c-987ff91978f5", "my_key", "hi my value"), + ("494a4aca-177b-5e8a-986c-987ff91978f5", "start:inner_1", "__start__"), + ], ), - # there should be a single child checkpoint because we only keep - # one child checkpoint per parent checkpoint (in which child ran) ] # check that child snapshot matches id of parent @@ -9510,7 +9739,46 @@ def test_nested_graph_interrupts( assert list(app.get_state_history(config)) == [ StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + tasks=(PregelTask(AnyStr(), "inner_2"),), + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), next=("inner",), config={ "configurable": { @@ -9520,6 +9788,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -9532,45 +9801,49 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ), - }, ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + tasks=(PregelTask(AnyStr(), "inner_2"),), + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), next=("inner",), config={ "configurable": { @@ -9580,6 +9853,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -9592,41 +9866,6 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ), - }, ), StateSnapshot( values={"my_key": "my value"}, @@ -9639,7 +9878,7 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -9661,6 +9900,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -9687,6 +9927,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -9714,6 +9955,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -9729,7 +9971,46 @@ def test_nested_graph_interrupts( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + tasks=(PregelTask(AnyStr(), "inner_2"),), + next=("inner_2",), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), next=("inner",), config={ "configurable": { @@ -9739,6 +10020,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -9751,46 +10033,49 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "inner_2": { + "my_key": "hi my value here and there", + "my_other_key": "hi my value here", + } + }, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "6", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), next=("inner",), config={ "configurable": { @@ -9800,6 +10085,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -9812,63 +10098,6 @@ def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, ), StateSnapshot( values={}, @@ -9882,6 +10111,7 @@ def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -9892,6 +10122,7 @@ def test_nested_graph_interrupts( ] +@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_nested_graph_interrupts_parallel( request: pytest.FixtureRequest, checkpointer_name: str @@ -10016,6 +10247,7 @@ def test_nested_graph_interrupts_parallel( ] +@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_doubly_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str @@ -10167,9 +10399,16 @@ def test_nested_graph_state( config = {"configurable": {"thread_id": "1"}} app.invoke({"my_key": "my value"}, config, debug=True) # test state w/ nested subgraph state (right after interrupt) + # first get_state without subgraph state assert app.get_state(config) == StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + state={"configurable": {"thread_id": "1", "checkpoint_ns": AnyStr()}}, + ), + ), next=("inner",), config={ "configurable": { @@ -10179,6 +10418,7 @@ def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -10191,78 +10431,15 @@ def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={"my_key": "hi my value here", "my_other_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - name="inner_2", - error=None, - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ) - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots={ - "inner": StateSnapshot( + # now, get_state with subgraphs state + assert app.get_state(config, subgraphs=True) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( values={ "my_key": "hi my value here", "my_other_key": "hi my value", @@ -10278,11 +10455,14 @@ def test_nested_graph_state( config={ "configurable": { "thread_id": "1", - "checkpoint_ns": "inner", + "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), } }, metadata={ + "parents": { + "": AnyStr(), + }, "source": "loop", "writes": { "inner_1": { @@ -10296,12 +10476,74 @@ def test_nested_graph_state( parent_config={ "configurable": { "thread_id": "1", - "checkpoint_ns": "inner", + "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, - ) + ), + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # get_state_history returns outer graph checkpoints + history = list(app.get_state_history(config)) + assert history == [ + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + } + }, + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } }, ), StateSnapshot( @@ -10315,7 +10557,7 @@ def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -10324,7 +10566,6 @@ def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ), StateSnapshot( values={}, @@ -10338,15 +10579,98 @@ def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, }, created_at=AnyStr(), parent_config=None, - subgraph_state_snapshots=None, ), ] + # get_state_history for a subgraph returns its checkpoints + child_history = [*app.get_state_history(history[0].tasks[0].state)] + assert child_history == [ + StateSnapshot( + values={"my_key": "hi my value here", "my_other_key": "hi my value"}, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="inner_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="inner_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "hi my value", "other_parent_key": None}, + "step": -1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + + # resume app.invoke(None, config, debug=True) # test state w/ nested subgraph state (after resuming from interrupt) assert app.get_state(config) == StateSnapshot( @@ -10361,6 +10685,7 @@ def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -10391,6 +10716,7 @@ def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -10405,7 +10731,6 @@ def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ), StateSnapshot( values={"my_key": "hi my value here and there"}, @@ -10419,6 +10744,7 @@ def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -10431,11 +10757,18 @@ def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + state={ + "configurable": {"thread_id": "1", "checkpoint_ns": AnyStr()} + }, + ), + ), next=("inner",), config={ "configurable": { @@ -10445,6 +10778,7 @@ def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -10457,42 +10791,6 @@ def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, ), StateSnapshot( values={"my_key": "my value"}, @@ -10505,7 +10803,7 @@ def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -10514,7 +10812,6 @@ def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ), StateSnapshot( values={}, @@ -10528,13 +10825,13 @@ def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, }, created_at=AnyStr(), parent_config=None, - subgraph_state_snapshots=None, ), ] assert actual_history == expected_history @@ -10601,9 +10898,21 @@ def test_doubly_nested_graph_state( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} app.invoke({"my_key": "my value"}, config, debug=True) + # get state without subgraphs assert app.get_state(config) == StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "child"),), + tasks=( + PregelTask( + AnyStr(), + "child", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child"), + } + }, + ), + ), next=("child",), config={ "configurable": { @@ -10613,6 +10922,7 @@ def test_doubly_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"parent_1": {"my_key": "hi my value"}}, "step": 1, @@ -10625,98 +10935,85 @@ def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "child": StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "child_1"),), - next=("child_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots={ - "child_1": StateSnapshot( - values={"my_key": "hi my value here"}, - tasks=( - PregelTask( - AnyStr(), - "grandchild_2", + ) + # get state with subgraphs + assert app.get_state(config, subgraphs=True) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state=StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child_1", + state=StateSnapshot( + values={"my_key": "hi my value here"}, + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + ), + ), + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + "source": "loop", + "writes": { + "grandchild_1": {"my_key": "hi my value here"} + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, ), ), - next=("grandchild_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"grandchild_1": {"my_key": "hi my value here"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, - ) - }, - ) - app.invoke(None, config, debug=True) - assert app.get_state(config) == StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "parent_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - - # test getting snapshot by ID - config = list(app.get_state_history(config))[2].config - # test getting grandchild snapshot - assert app.get_state(config) == StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "child"),), + ), + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {"": AnyStr()}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), next=("child",), config={ "configurable": { @@ -10726,6 +11023,7 @@ def test_doubly_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"parent_1": {"my_key": "hi my value"}}, "step": 1, @@ -10738,66 +11036,390 @@ def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots={ - "child": StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"child_1": {"my_key": "hi my value here and there"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots={ - "child_1": StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "grandchild_2": {"my_key": "hi my value here and there"} - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - subgraph_state_snapshots=None, - ) - }, - ) - }, ) + # resume + app.invoke(None, config, debug=True) + # get state with and without subgraphs + assert ( + app.get_state(config) + == app.get_state(config, subgraphs=True) + == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "parent_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + ) + # get outer graph history + outer_history = list(app.get_state_history(config)) + assert outer_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "parent_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=("parent_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"child": {"my_key": "hi my value here and there"}}, + "step": 2, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child"), + } + }, + ), + ), + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("parent_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"source": "loop", "writes": None, "step": 0, "parents": {}}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + # get child graph history + child_history = list(app.get_state_history(outer_history[2].tasks[0].state)) + assert child_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="child_1", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + } + }, + ), + ), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "hi my value"}, + "step": -1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + # get grandchild graph history + grandchild_history = list(app.get_state_history(child_history[1].tasks[0].state)) + assert grandchild_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"grandchild_2": {"my_key": "hi my value here and there"}}, + "step": 2, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + StateSnapshot( + values={"my_key": "hi my value here"}, + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="grandchild_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("grandchild_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="grandchild_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "hi my value"}, + "step": -1, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] +@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_send_to_nested_graphs( request: pytest.FixtureRequest, checkpointer_name: str @@ -10846,12 +11468,12 @@ def test_send_to_nested_graphs( "jokes": [], } actual_snapshot = graph.get_state(config) - subgraph_nodes = list(actual_snapshot.subgraph_state_snapshots.keys()) + subgraph_nodes = list(actual_snapshot.subgraphs.keys()) assert len(subgraph_nodes) == 2 for subgraph_node in subgraph_nodes: assert subgraph_node.split(":")[0] == "generate_joke" - subgraph_state_snapshots = { + subgraphs = { subgraph_node: graph.get_state( {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} ) @@ -10872,7 +11494,7 @@ def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -10881,7 +11503,7 @@ def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=subgraph_state_snapshots, + subgraphs=subgraphs, ) assert actual_snapshot == expected_snapshot @@ -10907,6 +11529,7 @@ def test_send_to_nested_graphs( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "generate_joke": [ @@ -10931,7 +11554,7 @@ def test_send_to_nested_graphs( actual_history = list(graph.get_state_history(config)) # get subgraph node state for expected history - subgraph_state_snapshots = { + subgraphs = { subgraph_node: graph.get_state( {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} ) @@ -10953,6 +11576,7 @@ def test_send_to_nested_graphs( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "generate_joke": [ @@ -10970,7 +11594,6 @@ def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=None, ), StateSnapshot( values={"subjects": ["cats", "dogs"], "jokes": []}, @@ -10986,7 +11609,7 @@ def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -10995,7 +11618,7 @@ def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraph_state_snapshots=subgraph_state_snapshots, + subgraphs=subgraphs, ), StateSnapshot( values={"jokes": []}, @@ -11009,13 +11632,13 @@ def test_send_to_nested_graphs( } }, metadata={ + "parents": {}, "source": "input", "writes": {"subjects": ["cats", "dogs"]}, "step": -1, }, created_at=AnyStr(), parent_config=None, - subgraph_state_snapshots=None, ), ] assert actual_history == expected_history diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 6d66ddac8..94b60a7f5 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -69,7 +69,7 @@ from langgraph.pregel import ( from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore -from tests.any_str import AnyStr, AnyVersion, UnsortedSequence +from tests.any_str import AnyDict, AnyStr, AnyVersion, UnsortedSequence from tests.conftest import ALL_CHECKPOINTERS_ASYNC from tests.fake_tracer import FakeTracer from tests.memory_assert import ( @@ -272,11 +272,13 @@ async def test_dynamic_interrupt( } assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ { + "parents": {}, "source": "loop", "step": 0, "writes": None, }, { + "parents": {}, "source": "input", "step": -1, "writes": {"my_key": "value ⛰️", "market": "DE"}, @@ -295,7 +297,7 @@ async def test_dynamic_interrupt( ), config=tup.config, created_at=tup.checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ -1 ].config, @@ -452,7 +454,12 @@ async def test_cancel_graph_astream( "aparallelwhile", "alittlewhile", ) - assert state.metadata == {"source": "loop", "step": 0, "writes": None} + assert state.metadata == { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + } @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) @@ -529,6 +536,7 @@ async def test_cancel_graph_astream_events_v2( assert state.values == {"value": 2} assert state.next == ("awhile",) assert state.metadata == { + "parents": {}, "source": "loop", "step": 1, "writes": {"alittlewhile": {"value": 2}}, @@ -885,7 +893,7 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 6, "writes": {"two": 5}}, + metadata={"parents": {}, "source": "loop", "step": 6, "writes": {"two": 5}}, created_at=AnyStr(), parent_config=history[1].config, ), @@ -900,7 +908,12 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 5, "writes": {"one": None}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 5, + "writes": {"one": None}, + }, created_at=AnyStr(), parent_config=history[2].config, ), @@ -915,7 +928,7 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": 4, "writes": 3}, + metadata={"parents": {}, "source": "input", "step": 4, "writes": 3}, created_at=AnyStr(), parent_config=history[3].config, ), @@ -930,7 +943,12 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 3, "writes": {"one": None}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"one": None}, + }, created_at=AnyStr(), parent_config=history[4].config, ), @@ -945,7 +963,7 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": 2, "writes": 20}, + metadata={"parents": {}, "source": "input", "step": 2, "writes": 20}, created_at=AnyStr(), parent_config=history[5].config, ), @@ -960,7 +978,7 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 1, "writes": {"two": 4}}, + metadata={"parents": {}, "source": "loop", "step": 1, "writes": {"two": 4}}, created_at=AnyStr(), parent_config=history[6].config, ), @@ -975,7 +993,12 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 0, "writes": {"one": None}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 0, + "writes": {"one": None}, + }, created_at=AnyStr(), parent_config=history[7].config, ), @@ -990,7 +1013,7 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": -1, "writes": 2}, + metadata={"parents": {}, "source": "input", "step": -1, "writes": 2}, created_at=AnyStr(), parent_config=None, ), @@ -1070,7 +1093,12 @@ async def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 5, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 5, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[1].config, ), @@ -1085,7 +1113,12 @@ async def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 4, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 4, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[2].config, ), @@ -1100,7 +1133,12 @@ async def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 3, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[3].config, ), @@ -1115,7 +1153,12 @@ async def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 2, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 2, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[4].config, ), @@ -1130,7 +1173,12 @@ async def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 1, "writes": {"add_one": 1}}, + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"add_one": 1}, + }, created_at=AnyStr(), parent_config=history[5].config, ), @@ -1145,7 +1193,7 @@ async def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, created_at=AnyStr(), parent_config=history[6].config, ), @@ -1160,7 +1208,7 @@ async def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"source": "input", "step": -1, "writes": 1}, + metadata={"parents": {}, "source": "input", "step": -1, "writes": 1}, created_at=AnyStr(), parent_config=None, ), @@ -1583,7 +1631,12 @@ async def test_pending_writes_resume( PregelTask(AnyStr(), "one"), PregelTask(AnyStr(), "two", 'ValueError("I\'m not good")'), ) - assert state.metadata == {"source": "loop", "step": 0, "writes": None} + assert state.metadata == { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + } # should contain pending write of "one" checkpoint = await checkpointer.aget_tuple(thread1) assert checkpoint is not None @@ -1671,6 +1724,7 @@ async def test_pending_writes_resume( "channel_values": {"one": "one", "two": "two", "value": 6}, }, metadata={ + "parents": {}, "step": 1, "source": "loop", "writes": {"one": {"value": 2}, "two": {"value": 3}}, @@ -1718,7 +1772,7 @@ async def test_pending_writes_resume( "start:two": "__start__", }, }, - metadata={"step": 0, "source": "loop", "writes": None}, + metadata={"parents": {}, "step": 0, "source": "loop", "writes": None}, parent_config={ "configurable": { "thread_id": "1", @@ -1753,7 +1807,7 @@ async def test_pending_writes_resume( }, "channel_values": {"__start__": {"value": 1}}, }, - metadata={"step": -1, "source": "input", "writes": {"value": 1}}, + metadata={"parents": {}, "step": -1, "source": "input", "writes": {"value": 1}}, parent_config=None, pending_writes=UnsortedSequence( (AnyStr(), "value", 1), @@ -2442,6 +2496,7 @@ async def test_conditional_graph( "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 0, "writes": { @@ -2492,6 +2547,7 @@ async def test_conditional_graph( "ts" ], metadata={ + "parents": {}, "source": "update", "step": 1, "writes": { @@ -2596,6 +2652,7 @@ async def test_conditional_graph( "ts" ], metadata={ + "parents": {}, "source": "update", "step": 4, "writes": { @@ -2664,6 +2721,7 @@ async def test_conditional_graph( "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 0, "writes": { @@ -2714,6 +2772,7 @@ async def test_conditional_graph( "ts" ], metadata={ + "parents": {}, "source": "update", "step": 1, "writes": { @@ -2818,6 +2877,7 @@ async def test_conditional_graph( "ts" ], metadata={ + "parents": {}, "source": "update", "step": 4, "writes": { @@ -2886,6 +2946,7 @@ async def test_conditional_graph( "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 0, "writes": { @@ -3276,6 +3337,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -3321,6 +3383,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: "ts" ], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -3400,6 +3463,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: "ts" ], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": { @@ -3454,6 +3518,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -3498,6 +3563,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: "ts" ], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -3575,6 +3641,7 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: "ts" ], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": { @@ -4156,6 +4223,7 @@ async def test_state_graph_packets( "ts" ], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -4208,6 +4276,7 @@ async def test_state_graph_packets( config=tup.config, created_at=tup.checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -4309,6 +4378,7 @@ async def test_state_graph_packets( config=tup.config, created_at=tup.checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -4373,6 +4443,7 @@ async def test_state_graph_packets( config=tup.config, created_at=tup.checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": {"agent": {"messages": AIMessage(content="answer", id="ai2")}}, @@ -4598,6 +4669,7 @@ async def test_message_graph( config=tup.config, created_at=tup.checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": { @@ -4641,6 +4713,7 @@ async def test_message_graph( config=tup.config, created_at=tup.checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 2, "writes": { @@ -4712,6 +4785,7 @@ async def test_message_graph( config=tup.config, created_at=tup.checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 4, "writes": { @@ -4764,6 +4838,7 @@ async def test_message_graph( config=tup.config, created_at=tup.checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 5, "writes": {"agent": AIMessage(content="answer", id="ai2")}, @@ -5081,11 +5156,13 @@ async def test_start_branch_then( } assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ { + "parents": {}, "source": "loop", "step": 0, "writes": None, }, { + "parents": {}, "source": "input", "step": -1, "writes": {"my_key": "value", "market": "DE"}, @@ -5097,7 +5174,7 @@ async def test_start_branch_then( next=("tool_two_slow",), config=(await tool_two.checkpointer.aget_tuple(thread1)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ -1 ].config, @@ -5114,6 +5191,7 @@ async def test_start_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread1)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"tool_two_slow": {"my_key": " slow"}}, @@ -5135,7 +5213,7 @@ async def test_start_branch_then( next=("tool_two_fast",), config=(await tool_two.checkpointer.aget_tuple(thread2)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ -1 ].config, @@ -5152,6 +5230,7 @@ async def test_start_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread2)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"tool_two_fast": {"my_key": " fast"}}, @@ -5173,7 +5252,7 @@ async def test_start_branch_then( next=("tool_two_fast",), config=(await tool_two.checkpointer.aget_tuple(thread3)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], - metadata={"source": "loop", "step": 0, "writes": None}, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, parent_config=[c async for c in tool_two.checkpointer.alist(thread3, limit=2)][ -1 ].config, @@ -5187,6 +5266,7 @@ async def test_start_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread3)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], metadata={ + "parents": {}, "source": "update", "step": 1, "writes": {START: {"my_key": "key"}}, @@ -5207,6 +5287,7 @@ async def test_start_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread3)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 2, "writes": {"tool_two_fast": {"my_key": " fast"}}, @@ -5277,6 +5358,7 @@ async def test_branch_then( }, "values": {"my_key": ""}, "metadata": { + "parents": {}, "source": "input", "step": -1, "writes": {"my_key": "value", "market": "DE"}, @@ -5306,6 +5388,7 @@ async def test_branch_then( "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 0, "writes": None, @@ -5358,6 +5441,7 @@ async def test_branch_then( "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5410,6 +5494,7 @@ async def test_branch_then( "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 2, "writes": {"tool_two_slow": {"my_key": " slow"}}, @@ -5462,6 +5547,7 @@ async def test_branch_then( "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -5506,6 +5592,7 @@ async def test_branch_then( }, "values": {"my_key": ""}, "metadata": { + "parents": {}, "source": "input", "step": -1, "writes": {"my_key": "value", "market": "DE"}, @@ -5535,6 +5622,7 @@ async def test_branch_then( "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 0, "writes": None, @@ -5587,6 +5675,7 @@ async def test_branch_then( "market": "DE", }, "metadata": { + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5603,6 +5692,7 @@ async def test_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread1)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5623,6 +5713,7 @@ async def test_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread1)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -5645,6 +5736,7 @@ async def test_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread2)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5665,6 +5757,7 @@ async def test_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread2)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -5695,6 +5788,7 @@ async def test_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread1)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5715,6 +5809,7 @@ async def test_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread1)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -5737,6 +5832,7 @@ async def test_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread2)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5757,6 +5853,7 @@ async def test_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread2)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -5777,6 +5874,7 @@ async def test_branch_then( config=uconfig, created_at=AnyStr(), metadata={ + "parents": {}, "source": "update", "step": 0, "writes": {START: {"my_key": "key", "market": "DE"}}, @@ -5796,6 +5894,7 @@ async def test_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread3)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 1, "writes": {"prepare": {"my_key": " prepared"}}, @@ -5814,6 +5913,7 @@ async def test_branch_then( config=(await tool_two.checkpointer.aget_tuple(thread3)).config, created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], metadata={ + "parents": {}, "source": "loop", "step": 3, "writes": {"finish": {"my_key": " finished"}}, @@ -6159,6 +6259,7 @@ async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, "step": 4, @@ -6700,7 +6801,7 @@ async def test_nested_graph(snapshot: SnapshotAssertion) -> None: assert times_called == 1 -@pytest.mark.repeat(10) +@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str @@ -6776,6 +6877,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -6809,6 +6911,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_1": { @@ -6840,7 +6943,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -6862,6 +6965,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -6886,6 +6990,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -6913,6 +7018,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -6938,6 +7044,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -6966,6 +7073,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_2": { @@ -6997,7 +7105,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -7019,6 +7127,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -7091,6 +7200,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -7115,7 +7225,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -7137,6 +7247,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -7165,6 +7276,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -7198,6 +7310,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_1": { @@ -7229,7 +7342,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -7251,6 +7364,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -7280,6 +7394,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -7307,6 +7422,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -7332,6 +7448,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -7360,6 +7477,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_2": { @@ -7391,7 +7509,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -7413,6 +7531,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -7454,6 +7573,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -7487,6 +7607,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_1": { @@ -7518,7 +7639,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -7540,6 +7661,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -7567,6 +7689,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -7592,6 +7715,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -7620,6 +7744,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_2": { @@ -7651,7 +7776,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -7673,6 +7798,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -7699,6 +7825,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -7726,6 +7853,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -7751,6 +7879,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -7779,6 +7908,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_2": { @@ -7810,7 +7940,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -7832,6 +7962,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -7865,6 +7996,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -7898,6 +8030,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_1": { @@ -7929,7 +8062,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -7951,6 +8084,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -7982,6 +8116,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_1": { @@ -8031,6 +8166,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -8059,6 +8195,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_1": { @@ -8091,6 +8228,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -8119,6 +8257,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_1": { @@ -8150,7 +8289,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -8172,6 +8311,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -8198,6 +8338,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -8225,6 +8366,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -8250,6 +8392,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -8278,6 +8421,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_1": { @@ -8310,6 +8454,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -8338,6 +8483,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "inner_2": { @@ -8369,7 +8515,7 @@ async def test_nested_graph_interrupts( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -8391,6 +8537,7 @@ async def test_nested_graph_interrupts( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -8401,6 +8548,7 @@ async def test_nested_graph_interrupts( ] +@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_nested_graph_interrupts_parallel( request: pytest.FixtureRequest, checkpointer_name: str @@ -8527,6 +8675,7 @@ async def test_nested_graph_interrupts_parallel( ] +@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_doubly_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str @@ -8635,13 +8784,13 @@ async def test_nested_graph_state( my_key: str my_other_key: str - async def inner_1(state: InnerState): + def inner_1(state: InnerState): return { "my_key": state["my_key"] + " here", "my_other_key": state["my_key"], } - async def inner_2(state: InnerState): + def inner_2(state: InnerState): return { "my_key": state["my_key"] + " and there", "my_other_key": state["my_key"], @@ -8658,10 +8807,10 @@ async def test_nested_graph_state( my_key: str other_parent_key: str - async def outer_1(state: State): + def outer_1(state: State): return {"my_key": "hi " + state["my_key"]} - async def outer_2(state: State): + def outer_2(state: State): return {"my_key": state["my_key"] + " and back again"} graph = StateGraph(State) @@ -8681,12 +8830,14 @@ async def test_nested_graph_state( config = {"configurable": {"thread_id": "1"}} await app.ainvoke({"my_key": "my value"}, config, debug=True) # test state w/ nested subgraph state (right after interrupt) + # first get_state without subgraph state assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value"}, tasks=( PregelTask( AnyStr(), "inner", + state={"configurable": {"thread_id": "1", "checkpoint_ns": AnyStr()}}, ), ), next=("inner",), @@ -8698,6 +8849,7 @@ async def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -8710,77 +8862,15 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraphs={ - "inner": StateSnapshot( - values={"my_key": "hi my value here", "my_other_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - name="inner_2", - error=None, - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, ) - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( + # now, get_state with subgraphs state + assert await app.aget_state(config, subgraphs=True) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( values={ "my_key": "hi my value here", "my_other_key": "hi my value", @@ -8796,11 +8886,14 @@ async def test_nested_graph_state( config={ "configurable": { "thread_id": "1", - "checkpoint_ns": "inner", + "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), } }, metadata={ + "parents": { + "": AnyStr(), + }, "source": "loop", "writes": { "inner_1": { @@ -8814,11 +8907,74 @@ async def test_nested_graph_state( parent_config={ "configurable": { "thread_id": "1", - "checkpoint_ns": "inner", + "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), } }, - ) + ), + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # get_state_history returns outer graph checkpoints + history = [c async for c in app.aget_state_history(config)] + assert history == [ + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + } + }, + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } }, ), StateSnapshot( @@ -8832,7 +8988,12 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={ + "parents": {}, + "source": "loop", + "writes": None, + "step": 0, + }, created_at=AnyStr(), parent_config={ "configurable": { @@ -8854,6 +9015,7 @@ async def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -8862,6 +9024,89 @@ async def test_nested_graph_state( parent_config=None, ), ] + # get_state_history for a subgraph returns its checkpoints + child_history = [c async for c in app.aget_state_history(history[0].tasks[0].state)] + assert child_history == [ + StateSnapshot( + values={"my_key": "hi my value here", "my_other_key": "hi my value"}, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="inner_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="inner_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "hi my value", "other_parent_key": None}, + "step": -1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + + # resume await app.ainvoke(None, config, debug=True) # test state w/ nested subgraph state (after resuming from interrupt) assert await app.aget_state(config) == StateSnapshot( @@ -8876,6 +9121,7 @@ async def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -8892,7 +9138,7 @@ async def test_nested_graph_state( }, ) # test full history at the end - actual_history = [s async for s in app.aget_state_history(config)] + actual_history = [c async for c in app.aget_state_history(config)] expected_history = [ StateSnapshot( values={"my_key": "hi my value here and there and back again"}, @@ -8906,6 +9152,7 @@ async def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "outer_2": {"my_key": "hi my value here and there and back again"} @@ -8933,6 +9180,7 @@ async def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"inner": {"my_key": "hi my value here and there"}}, "step": 2, @@ -8948,7 +9196,15 @@ async def test_nested_graph_state( ), StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), + tasks=( + PregelTask( + AnyStr(), + "inner", + state={ + "configurable": {"thread_id": "1", "checkpoint_ns": AnyStr()} + }, + ), + ), next=("inner",), config={ "configurable": { @@ -8958,6 +9214,7 @@ async def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"outer_1": {"my_key": "hi my value"}}, "step": 1, @@ -8970,41 +9227,6 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, ), StateSnapshot( values={"my_key": "my value"}, @@ -9017,7 +9239,12 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={ + "parents": {}, + "source": "loop", + "writes": None, + "step": 0, + }, created_at=AnyStr(), parent_config={ "configurable": { @@ -9039,6 +9266,7 @@ async def test_nested_graph_state( } }, metadata={ + "parents": {}, "source": "input", "writes": {"my_key": "my value"}, "step": -1, @@ -9068,10 +9296,10 @@ async def test_doubly_nested_graph_state( class GrandChildState(TypedDict): my_key: str - async def grandchild_1(state: ChildState): + def grandchild_1(state: ChildState): return {"my_key": state["my_key"] + " here"} - async def grandchild_2(state: ChildState): + def grandchild_2(state: ChildState): return { "my_key": state["my_key"] + " and there", } @@ -9091,10 +9319,10 @@ async def test_doubly_nested_graph_state( child.set_entry_point("child_1") child.set_finish_point("child_1") - async def parent_1(state: State): + def parent_1(state: State): return {"my_key": "hi " + state["my_key"]} - async def parent_2(state: State): + def parent_2(state: State): return {"my_key": state["my_key"] + " and back again"} graph = StateGraph(State) @@ -9111,9 +9339,21 @@ async def test_doubly_nested_graph_state( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} await app.ainvoke({"my_key": "my value"}, config, debug=True) + # get state without subgraphs assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "child"),), + tasks=( + PregelTask( + AnyStr(), + "child", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child"), + } + }, + ), + ), next=("child",), config={ "configurable": { @@ -9123,6 +9363,7 @@ async def test_doubly_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"parent_1": {"my_key": "hi my value"}}, "step": 1, @@ -9135,101 +9376,85 @@ async def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraphs={ - "child": StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "child_1", - ), - ), - next=("child_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - metadata={"source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "child_1": StateSnapshot( - values={"my_key": "hi my value here"}, - tasks=( - PregelTask( - AnyStr(), - "grandchild_2", + ) + # get state with subgraphs + assert await app.aget_state(config, subgraphs=True) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state=StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child_1", + state=StateSnapshot( + values={"my_key": "hi my value here"}, + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + ), + ), + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + "source": "loop", + "writes": { + "grandchild_1": {"my_key": "hi my value here"} + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, ), ), - next=("grandchild_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"grandchild_1": {"my_key": "hi my value here"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ) - }, - ) - await app.ainvoke(None, config, debug=True) - assert await app.aget_state(config) == StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "parent_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - # test getting snapshot by ID - config = [s async for s in app.aget_state_history(config)][2].config - # test getting grandchild snapshot - assert await app.aget_state(config) == StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "child"),), + ), + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {"": AnyStr()}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), next=("child",), config={ "configurable": { @@ -9239,6 +9464,7 @@ async def test_doubly_nested_graph_state( } }, metadata={ + "parents": {}, "source": "loop", "writes": {"parent_1": {"my_key": "hi my value"}}, "step": 1, @@ -9251,65 +9477,399 @@ async def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - subgraphs={ - "child": StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": {"child_1": {"my_key": "hi my value here and there"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "child_1": StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "source": "loop", - "writes": { - "grandchild_2": {"my_key": "hi my value here and there"} - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "child|child_1", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ) - }, ) + # resume + await app.ainvoke(None, config, debug=True) + # get state with and without subgraphs + assert ( + await app.aget_state(config) + == await app.aget_state(config, subgraphs=True) + == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "parent_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + ) + # get outer graph history + outer_history = [c async for c in app.aget_state_history(config)] + assert outer_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "parent_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=("parent_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"child": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child"), + } + }, + ), + ), + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("parent_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + # get child graph history + child_history = [ + c async for c in app.aget_state_history(outer_history[2].tasks[0].state) + ] + assert child_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="child_1", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + } + }, + ), + ), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "hi my value"}, + "step": -1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + # get grandchild graph history + grandchild_history = [ + c async for c in app.aget_state_history(child_history[1].tasks[0].state) + ] + assert grandchild_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"grandchild_2": {"my_key": "hi my value here and there"}}, + "step": 2, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + StateSnapshot( + values={"my_key": "hi my value here"}, + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="grandchild_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("grandchild_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="grandchild_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "input", + "writes": {"my_key": "hi my value"}, + "step": -1, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] +@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_send_to_nested_graphs( request: pytest.FixtureRequest, checkpointer_name: str @@ -9383,7 +9943,7 @@ async def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -9418,6 +9978,7 @@ async def test_send_to_nested_graphs( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "generate_joke": [ @@ -9463,6 +10024,7 @@ async def test_send_to_nested_graphs( } }, metadata={ + "parents": {}, "source": "loop", "writes": { "generate_joke": [ @@ -9495,7 +10057,7 @@ async def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - metadata={"source": "loop", "writes": None, "step": 0}, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, created_at=AnyStr(), parent_config={ "configurable": { @@ -9518,6 +10080,7 @@ async def test_send_to_nested_graphs( } }, metadata={ + "parents": {}, "source": "input", "writes": {"subjects": ["cats", "dogs"]}, "step": -1, From 9d68aac76f001e924c07315eba239a079303db4c Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 10:47:50 -0700 Subject: [PATCH 177/287] Enable passing subgraph checkpoint config to get_state/aget_state --- libs/langgraph/langgraph/pregel/__init__.py | 52 ++++++++++++- libs/langgraph/tests/test_pregel.py | 81 ++++++++++++++++++++- libs/langgraph/tests/test_pregel_async.py | 81 ++++++++++++++++++++- 3 files changed, 208 insertions(+), 6 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 352cae7e2..235606929 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -370,9 +370,9 @@ class Pregel( ) async def aget_subgraphs( - self, recursive: bool = False + self, recurse: bool = False ) -> AsyncIterator[tuple[str, Pregel]]: - for name, node in self.get_subgraphs(recurse=recursive): + for name, node in self.get_subgraphs(recurse=recurse): yield name, node def _prepare_state_snapshot( @@ -482,7 +482,7 @@ class Pregel( for_execution=False, ) # get the subgraphs - subgraphs = dict(self.get_subgraphs()) + subgraphs = {n: g async for n, g in self.aget_subgraphs()} parent_ns = saved.config["configurable"].get("checkpoint_ns", "") task_states: dict[str, Union[RunnableConfig, StateSnapshot]] = {} for task in next_tasks: @@ -534,6 +534,28 @@ class Pregel( if not checkpointer: raise ValueError("No checkpointer set") + if ( + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) + ) + # find the subgraph with the matching name + for name, pregel in self.get_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + return pregel.get_state( + { + "configurable": { + **config["configurable"], + CONFIG_KEY_CHECKPOINTER: checkpointer, + } + }, + subgraphs=subgraphs, + ) + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") + config = merge_configs(self.config, config) if self.config else config saved = checkpointer.get_tuple(config) return self._prepare_state_snapshot( @@ -550,6 +572,28 @@ class Pregel( if not checkpointer: raise ValueError("No checkpointer set") + if ( + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) + ) + # find the subgraph with the matching name + async for name, pregel in self.aget_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + return await pregel.aget_state( + { + "configurable": { + **config["configurable"], + CONFIG_KEY_CHECKPOINTER: checkpointer, + } + }, + subgraphs=subgraphs, + ) + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") + config = merge_configs(self.config, config) if self.config else config saved = await checkpointer.aget_tuple(config) return await self._aprepare_state_snapshot( @@ -630,7 +674,7 @@ class Pregel( part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) ) # find the subgraph with the matching name - for name, pregel in self.get_subgraphs(recurse=True): + async for name, pregel in self.aget_subgraphs(recurse=True): if name == recast_checkpoint_ns: async for state in pregel.aget_state_history( { diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index e5d263121..9b6a1967d 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -10899,7 +10899,8 @@ def test_doubly_nested_graph_state( config = {"configurable": {"thread_id": "1"}} app.invoke({"my_key": "my value"}, config, debug=True) # get state without subgraphs - assert app.get_state(config) == StateSnapshot( + outer_state = app.get_state(config) + assert outer_state == StateSnapshot( values={"my_key": "hi my value"}, tasks=( PregelTask( @@ -10936,6 +10937,84 @@ def test_doubly_nested_graph_state( } }, ) + child_state = app.get_state(outer_state.tasks[0].state) + assert ( + child_state.tasks[0] + == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child_1", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + } + }, + ), + ), + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {"": AnyStr()}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + ).tasks[0] + ) + grandchild_state = app.get_state(child_state.tasks[0].state) + assert grandchild_state == StateSnapshot( + values={"my_key": "hi my value here"}, + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + ), + ), + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ) # get state with subgraphs assert app.get_state(config, subgraphs=True) == StateSnapshot( values={"my_key": "hi my value"}, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 94b60a7f5..9163bfe7f 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -9340,7 +9340,8 @@ async def test_doubly_nested_graph_state( config = {"configurable": {"thread_id": "1"}} await app.ainvoke({"my_key": "my value"}, config, debug=True) # get state without subgraphs - assert await app.aget_state(config) == StateSnapshot( + outer_state = await app.aget_state(config) + assert outer_state == StateSnapshot( values={"my_key": "hi my value"}, tasks=( PregelTask( @@ -9377,6 +9378,84 @@ async def test_doubly_nested_graph_state( } }, ) + child_state = await app.aget_state(outer_state.tasks[0].state) + assert ( + child_state.tasks[0] + == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child_1", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + } + }, + ), + ), + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {"": AnyStr()}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + ).tasks[0] + ) + grandchild_state = await app.aget_state(child_state.tasks[0].state) + assert grandchild_state == StateSnapshot( + values={"my_key": "hi my value here"}, + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + ), + ), + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ) # get state with subgraphs assert await app.aget_state(config, subgraphs=True) == StateSnapshot( values={"my_key": "hi my value"}, From 98789acaccfd8a2e9e9a6b756995ec63794f9aff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:05:50 +0000 Subject: [PATCH 178/287] Bump notebook from 7.2.1 to 7.2.2 in /libs/langgraph Bumps [notebook](https://github.com/jupyter/notebook) from 7.2.1 to 7.2.2. - [Release notes](https://github.com/jupyter/notebook/releases) - [Changelog](https://github.com/jupyter/notebook/blob/@jupyter-notebook/tree@7.2.2/CHANGELOG.md) - [Commits](https://github.com/jupyter/notebook/compare/@jupyter-notebook/tree@7.2.1...@jupyter-notebook/tree@7.2.2) --- updated-dependencies: - dependency-name: notebook dependency-type: indirect ... Signed-off-by: dependabot[bot] --- libs/langgraph/poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index 36bbe5651..104a0299c 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -2243,13 +2243,13 @@ files = [ [[package]] name = "notebook" -version = "7.2.1" +version = "7.2.2" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.8" files = [ - {file = "notebook-7.2.1-py3-none-any.whl", hash = "sha256:f45489a3995746f2195a137e0773e2130960b51c9ac3ce257dbc2705aab3a6ca"}, - {file = "notebook-7.2.1.tar.gz", hash = "sha256:4287b6da59740b32173d01d641f763d292f49c30e7a51b89c46ba8473126341e"}, + {file = "notebook-7.2.2-py3-none-any.whl", hash = "sha256:c89264081f671bc02eec0ed470a627ed791b9156cad9285226b31611d3e9fe1c"}, + {file = "notebook-7.2.2.tar.gz", hash = "sha256:2ef07d4220421623ad3fe88118d687bc0450055570cdd160814a59cf3a1c516e"}, ] [package.dependencies] From 209e4cef7b0917bc6469b0afccf8ded8b17dcdcd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:10:05 +0000 Subject: [PATCH 179/287] Bump jupyterlab from 4.2.2 to 4.2.5 in /libs/langgraph Bumps [jupyterlab](https://github.com/jupyterlab/jupyterlab) from 4.2.2 to 4.2.5. - [Release notes](https://github.com/jupyterlab/jupyterlab/releases) - [Changelog](https://github.com/jupyterlab/jupyterlab/blob/@jupyterlab/lsp@4.2.5/CHANGELOG.md) - [Commits](https://github.com/jupyterlab/jupyterlab/compare/@jupyterlab/lsp@4.2.2...@jupyterlab/lsp@4.2.5) --- updated-dependencies: - dependency-name: jupyterlab dependency-type: indirect ... Signed-off-by: dependabot[bot] --- libs/langgraph/poetry.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index 36bbe5651..efd77c042 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -1634,13 +1634,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.2.2" +version = "4.2.5" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.2.2-py3-none-any.whl", hash = "sha256:59ee9b839f43308c3dfd55d72d1f1a299ed42a7f91f2d1afe9c12a783f9e525f"}, - {file = "jupyterlab-4.2.2.tar.gz", hash = "sha256:a534b6a25719a92a40d514fb133a9fe8f0d9981b0bbce5d8a5fcaa33344a3038"}, + {file = "jupyterlab-4.2.5-py3-none-any.whl", hash = "sha256:73b6e0775d41a9fee7ee756c80f58a6bed4040869ccc21411dc559818874d321"}, + {file = "jupyterlab-4.2.5.tar.gz", hash = "sha256:ae7f3a1b8cb88b4f55009ce79fa7c06f99d70cd63601ee4aa91815d054f46f75"}, ] [package.dependencies] @@ -1665,7 +1665,7 @@ dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] -upgrade-extension = ["copier (>=8,<10)", "jinja2-time (<0.3)", "pydantic (<2.0)", "pyyaml-include (<2.0)", "tomli-w (<2.0)"] +upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"] [[package]] name = "jupyterlab-pygments" From 01f14d316617e9d7e1a3b809f6a94d103b5e0d94 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Thu, 29 Aug 2024 14:28:22 -0400 Subject: [PATCH 180/287] docs: update how-to for streaming from final node (#1533) --- examples/streaming-from-final-node.ipynb | 230 +++++++++++++++-------- 1 file changed, 149 insertions(+), 81 deletions(-) diff --git a/examples/streaming-from-final-node.ipynb b/examples/streaming-from-final-node.ipynb index ee7e14a4b..30496a3a1 100644 --- a/examples/streaming-from-final-node.ipynb +++ b/examples/streaming-from-final-node.ipynb @@ -23,7 +23,8 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture --no-stderr\n%pip install -U langgraph langchain-openai" + "%%capture --no-stderr\n", + "%pip install -U langgraph langchain-openai" ] }, { @@ -41,7 +42,16 @@ } ], "source": [ - "import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"OPENAI_API_KEY\")" + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" ] }, { @@ -55,33 +65,42 @@ { "cell_type": "code", "execution_count": 3, - "id": "1d51c35c-dbf2-4c01-932d-c5d308ea37d2", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Literal\nfrom langchain_community.tools.tavily_search import TavilySearchResults\nfrom langchain_core.runnables import ConfigurableField\nfrom langchain_core.tools import tool\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import create_react_agent\nfrom langgraph.prebuilt import ToolNode\n\n\n@tool\ndef get_weather(city: Literal[\"nyc\", \"sf\"]):\n \"\"\"Use this to get weather information.\"\"\"\n if city == \"nyc\":\n return \"It might be cloudy in nyc\"\n elif city == \"sf\":\n return \"It's always sunny in sf\"\n else:\n raise AssertionError(\"Unknown city\")\n\n\ntools = [get_weather]\nmodel = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\nfinal_model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n\nmodel = model.bind_tools(tools)\n# NOTE: this is where we're adding a tag that we'll be using later to filter the outputs of the final node\nfinal_model = final_model.with_config(tags=[\"final_node\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "0af37212-e592-484d-9194-35d53fa79678", + "id": "5e62618d-0e0c-483c-acd3-40a26e61894a", "metadata": {}, "outputs": [], "source": [ + "from typing import Literal\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.runnables import ConfigurableField\n", + "from langchain_core.tools import tool\n", + "from langchain_openai import ChatOpenAI\n", + "from langgraph.prebuilt import create_react_agent\n", + "from langgraph.prebuilt import ToolNode\n", + "\n", + "\n", + "@tool\n", + "def get_weather(city: Literal[\"nyc\", \"sf\"]):\n", + " \"\"\"Use this to get weather information.\"\"\"\n", + " if city == \"nyc\":\n", + " return \"It might be cloudy in nyc\"\n", + " elif city == \"sf\":\n", + " return \"It's always sunny in sf\"\n", + " else:\n", + " raise AssertionError(\"Unknown city\")\n", + "\n", + "\n", + "tools = [get_weather]\n", + "model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", + "final_model = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n", + "\n", + "model = model.bind_tools(tools)\n", + "# NOTE: this is where we're adding a tag that we'll can use later to filter the model stream events to only the model called in the final node.\n", + "# This is not necessary if you call a single LLM but might be important in case you call multiple models within the node and want to filter events\n", + "# from only one of them.\n", + "final_model = final_model.with_config(tags=[\"final_node\"])\n", "tool_node = ToolNode(tools=tools)" ] }, - { - "cell_type": "code", - "execution_count": 5, - "id": "ac9d4f5b-655a-48f3-b514-a4a0815714a6", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import TypedDict, Annotated\n\nfrom langgraph.graph import END, StateGraph, START\nfrom langgraph.graph.message import MessagesState\nfrom langchain_core.messages import BaseMessage" - ] - }, { "cell_type": "markdown", "id": "9acef997-5dd6-4108-baf1-c4d6be3e4999", @@ -92,21 +111,18 @@ }, { "cell_type": "code", - "execution_count": 6, - "id": "3948c6b8-0317-4001-b699-32b25306a023", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.messages import SystemMessage, HumanMessage" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "2efe9fb4-c6c2-4171-becd-d45bbf899209", + "execution_count": 4, + "id": "8c7339d2-1835-4b5a-a99c-a60e150280af", "metadata": {}, "outputs": [], "source": [ + "from typing import TypedDict, Annotated\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "from langgraph.graph.message import MessagesState\n", + "from langchain_core.messages import BaseMessage, SystemMessage, HumanMessage\n", + "\n", + "\n", "def should_continue(state: MessagesState) -> Literal[\"tools\", \"final\"]:\n", " messages = state[\"messages\"]\n", " last_message = messages[-1]\n", @@ -135,16 +151,8 @@ " )\n", " # overwrite the last AI message from the agent\n", " response.id = last_ai_message.id\n", - " return {\"messages\": [response]}" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "b1a9a981-8629-4d25-a0e1-d666c3968b30", - "metadata": {}, - "outputs": [], - "source": [ + " return {\"messages\": [response]}\n", + "\n", "workflow = StateGraph(MessagesState)\n", "\n", "workflow.add_node(\"agent\", call_model)\n", @@ -159,38 +167,20 @@ ")\n", "\n", "workflow.add_edge(\"tools\", \"agent\")\n", - "workflow.add_edge(\"final\", END)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "a7b0251f-dcee-49d6-8133-af50d4a55e22", - "metadata": {}, - "outputs": [], - "source": [ + "workflow.add_edge(\"final\", END)\n", + "\n", "app = workflow.compile()" ] }, { "cell_type": "code", - "execution_count": 10, - "id": "f8b77e74-17e9-4fee-a164-4637013b55ff", - "metadata": {}, - "outputs": [], - "source": [ - "from IPython.display import display, Image" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "98adcef2-1dec-4503-99cc-48613cdb7a85", + "execution_count": 5, + "id": "2ab6d079-ba06-48ba-abe5-e72df24407af", "metadata": {}, "outputs": [ { "data": { - "image/jpeg": "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", + "image/jpeg": "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", "text/plain": [ "" ] @@ -200,6 +190,8 @@ } ], "source": [ + "from IPython.display import display, Image\n", + "\n", "display(Image(app.get_graph().draw_mermaid_png()))" ] }, @@ -213,18 +205,38 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 6, + "id": "84d65cbe-4cfe-44f8-b49e-b37632887c91", + "metadata": {}, + "outputs": [], + "source": [ + "import warnings\n", + "from langchain_core._api import LangChainBetaWarning\n", + "warnings.filterwarnings('ignore', category=LangChainBetaWarning)" + ] + }, + { + "cell_type": "markdown", + "id": "5cfaeb64-5506-4546-96c0-4891e6288ad9", + "metadata": {}, + "source": [ + "### Filter on event metadata" + ] + }, + { + "cell_type": "markdown", + "id": "f218a05d-1590-4d5c-b0b7-97d94c744efb", + "metadata": {}, + "source": [ + "First option to get the LLM events from within a specific node (`final` node in our case) is to filter on the `langgraph_node` field in the event metadata. This will be sufficient in case you need to stream events from ALL LLM calls inside the node. This means that if you have multiple different LLMs invoked inside the node, this filter will include events from all of them." + ] + }, + { + "cell_type": "code", + "execution_count": 7, "id": "a37c3a5f-5a43-46db-940e-c583df776520", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/vadymbarda/.virtualenvs/langgraph/lib/python3.12/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: This API is in beta and may change in the future.\n", - " warn_beta(\n" - ] - }, { "name": "stdout", "output_type": "stream", @@ -234,15 +246,71 @@ } ], "source": [ - "inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\nasync for event in app.astream_events(inputs, version=\"v2\"):\n kind = event[\"event\"]\n tags = event.get(\"tags\", [])\n if kind == \"on_chat_model_stream\" and \"final_node\" in tags:\n data = event[\"data\"]\n if data[\"chunk\"].content:\n # Empty content in the context of OpenAI or Anthropic usually means\n # that the model is asking for a tool to be invoked.\n # So we only print non-empty content\n print(data[\"chunk\"].content, end=\"|\")" + "inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\n", + "async for event in app.astream_events(inputs, version=\"v2\"):\n", + " kind = event[\"event\"]\n", + " tags = event.get(\"tags\", [])\n", + " # filter on the langgraph node name\n", + " if kind == \"on_chat_model_stream\" and event[\"metadata\"].get(\"langgraph_node\") == \"final\":\n", + " data = event[\"data\"]\n", + " if data[\"chunk\"].content:\n", + " # Empty content in the context of OpenAI or Anthropic usually means\n", + " # that the model is asking for a tool to be invoked.\n", + " # So we only print non-empty content\n", + " print(data[\"chunk\"].content, end=\"|\", flush=True)" + ] + }, + { + "cell_type": "markdown", + "id": "b0bb447a-6650-4166-b124-2d5b99a1f88b", + "metadata": {}, + "source": [ + "### Filter on custom tags" + ] + }, + { + "cell_type": "markdown", + "id": "ea4db927-44b6-46ab-8b8d-f237edaf1438", + "metadata": {}, + "source": [ + "Alternatively, you can add configuration with custom tags to your LLM, like we did in the beginning, by adding `final_model.with_config(tags=[\"final_node\"])`. This will allow us to more precisely filter the events to keep the ones only from this model." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "55d60dfa-96e3-442f-9924-0c99f46baed8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Well| folks|,| looks| like| we|'ve| got| some| cloudy| skies| in| the| Big| Apple| today|.| So| grab| your| umbrella| just| in| case|,| and| don|'t| let| those| clouds| rain| on| your| parade|!|" + ] + } + ], + "source": [ + "inputs = {\"messages\": [(\"human\", \"what's the weather in nyc?\")]}\n", + "async for event in app.astream_events(inputs, version=\"v2\"):\n", + " kind = event[\"event\"]\n", + " tags = event.get(\"tags\", [])\n", + " # filter on the custom tag\n", + " if kind == \"on_chat_model_stream\" and \"final_node\" in event.get(\"tags\", []):\n", + " data = event[\"data\"]\n", + " if data[\"chunk\"].content:\n", + " # Empty content in the context of OpenAI or Anthropic usually means\n", + " # that the model is asking for a tool to be invoked.\n", + " # So we only print non-empty content\n", + " print(data[\"chunk\"].content, end=\"|\", flush=True)" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "langgraph", "language": "python", - "name": "python3" + "name": "langgraph" }, "language_info": { "codemirror_mode": { @@ -254,7 +322,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.11.9" } }, "nbformat": 4, From 80e442e13d4d3ff3b032d1cd13c2da5cc38705db Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 12:00:02 -0700 Subject: [PATCH 181/287] Stream output from subgraphs - enabled by new argument stream(subgraphs=True) - the same stream_mode requested for parent graph is applied to all subgraphs --- libs/langgraph/langgraph/constants.py | 4 ++- libs/langgraph/langgraph/pregel/__init__.py | 28 +++++++++++-------- libs/langgraph/langgraph/pregel/loop.py | 31 ++++++++++++++++++++- libs/langgraph/tests/any_str.py | 1 - libs/langgraph/tests/test_pregel.py | 12 ++++++-- libs/langgraph/tests/test_pregel_async.py | 14 ++++++++-- 6 files changed, 72 insertions(+), 18 deletions(-) diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index eeb320ed3..0a748a032 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -5,10 +5,12 @@ INPUT = "__input__" CONFIG_KEY_SEND = "__pregel_send" CONFIG_KEY_READ = "__pregel_read" CONFIG_KEY_CHECKPOINTER = "__pregel_checkpointer" -CONFIG_KEY_CHECKPOINT_MAP = "checkpoint_map" +CONFIG_KEY_STREAM = "__pregel_stream" CONFIG_KEY_STORE = "__pregel_store" CONFIG_KEY_RESUMING = "__pregel_resuming" CONFIG_KEY_TASK_ID = "__pregel_task_id" +# this one part of public API so more readable +CONFIG_KEY_CHECKPOINT_MAP = "checkpoint_map" INTERRUPT = "__interrupt__" ERROR = "__error__" TASKS = "__pregel_tasks" diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 235606929..3d676a7bc 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -63,6 +63,7 @@ from langgraph.constants import ( CONFIG_KEY_READ, CONFIG_KEY_RESUMING, CONFIG_KEY_SEND, + CONFIG_KEY_STREAM, ERROR, INTERRUPT, NS_END, @@ -990,18 +991,17 @@ class Pregel( def _defaults( self, - config: Optional[RunnableConfig] = None, + config: RunnableConfig, *, - stream_mode: Optional[Union[StreamMode, list[StreamMode]]] = None, - output_keys: Optional[Union[str, Sequence[str]]] = None, - interrupt_before: Optional[Union[All, Sequence[str]]] = None, - interrupt_after: Optional[Union[All, Sequence[str]]] = None, - debug: Optional[bool] = None, + stream_mode: Optional[Union[StreamMode, list[StreamMode]]], + output_keys: Optional[Union[str, Sequence[str]]], + interrupt_before: Optional[Union[All, Sequence[str]]], + interrupt_after: Optional[Union[All, Sequence[str]]], + debug: Optional[bool], ) -> tuple[ bool, Sequence[StreamMode], Union[str, Sequence[str]], - Union[str, Sequence[str]], Optional[Sequence[str]], Optional[Sequence[str]], Optional[BaseCheckpointSaver], @@ -1016,12 +1016,10 @@ class Pregel( stream_mode = stream_mode if stream_mode is not None else self.stream_mode if not isinstance(stream_mode, list): stream_mode = [stream_mode] - if config and config.get("configurable", {}).get(CONFIG_KEY_READ) is not None: + if CONFIG_KEY_READ in config.get("configurable", {}): # if being called as a node in another graph, always use values mode stream_mode = ["values"] - if config is not None and config.get("configurable", {}).get( - CONFIG_KEY_CHECKPOINTER - ): + if CONFIG_KEY_CHECKPOINTER in config.get("configurable", {}): checkpointer: Optional[BaseCheckpointSaver] = config["configurable"][ CONFIG_KEY_CHECKPOINTER ] @@ -1046,6 +1044,7 @@ class Pregel( interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, + subgraphs: bool = False, ) -> Iterator[Union[dict[str, Any], Any]]: """Stream graph steps for a single input. @@ -1062,6 +1061,7 @@ class Pregel( 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. debug: Whether to print debug information during execution, defaults to False. + subgraphs: Whether to stream subgraphs, defaults to False. Yields: The output of each step in the graph. The output shape depends on the stream_mode. @@ -1155,6 +1155,8 @@ class Pregel( output_keys=output_keys, stream_keys=self.stream_channels_asis, ) as loop: + if subgraphs: + loop.config["configurable"][CONFIG_KEY_STREAM] = loop.stream # Similarly to Bulk Synchronous Parallel / Pregel model # computation proceeds in steps, while there are channel updates # channel updates from step N are only visible in step N+1 @@ -1287,6 +1289,7 @@ class Pregel( interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, + subgraphs: bool = False, ) -> AsyncIterator[Union[dict[str, Any], Any]]: """Stream graph steps for a single input. @@ -1303,6 +1306,7 @@ class Pregel( 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. debug: Whether to print debug information during execution, defaults to False. + subgraphs: Whether to stream subgraphs, defaults to False. Yields: The output of each step in the graph. The output shape depends on the stream_mode. @@ -1404,6 +1408,8 @@ class Pregel( output_keys=output_keys, stream_keys=self.stream_channels_asis, ) as loop: + if subgraphs: + loop.config["configurable"][CONFIG_KEY_STREAM] = loop.stream aioloop = asyncio.get_event_loop() # Similarly to Bulk Synchronous Parallel / Pregel model # computation proceeds in steps, while there are channel updates diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 58441f831..7ff95cc38 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -2,16 +2,19 @@ import asyncio import concurrent.futures from collections import deque from contextlib import AsyncExitStack, ExitStack +from itertools import tee from types import TracebackType from typing import ( Any, AsyncContextManager, Callable, ContextManager, + Iterable, List, Literal, Mapping, Optional, + Protocol, Sequence, Tuple, Type, @@ -39,6 +42,7 @@ from langgraph.constants import ( CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_READ, CONFIG_KEY_RESUMING, + CONFIG_KEY_STREAM, ERROR, INPUT, INTERRUPT, @@ -86,6 +90,27 @@ INPUT_RESUMING = object() EMPTY_SEQ = () +class StreamProtocol(Protocol): + def extend(self, values: Iterable[Tuple[str, Any]]) -> None: ... + def popleft(self) -> Tuple[str, Any]: ... + def __bool__(self) -> bool: ... + + +class DuplexStream(StreamProtocol): + def __init__(self, *streams: StreamProtocol) -> None: + self.streams = streams + + def extend(self, values: Iterable[Tuple[str, Any]]) -> None: + for stream, vv in zip(self.streams, tee(values, len(self.streams))): + stream.extend(vv) + + def popleft(self) -> Tuple[str, Any]: + return self.streams[0].popleft() + + def __bool__(self) -> bool: + return bool(self.streams[0]) + + class PregelLoop: input: Optional[Any] config: RunnableConfig @@ -127,7 +152,7 @@ class PregelLoop: "pending", "done", "interrupt_before", "interrupt_after", "out_of_steps" ] tasks: Sequence[PregelExecutableTask] - stream: deque[Tuple[str, Any]] + stream: StreamProtocol output: Union[None, dict[str, Any], Any] = None # public @@ -154,6 +179,10 @@ class PregelLoop: self.output_keys = output_keys self.stream_keys = stream_keys self.is_nested = CONFIG_KEY_READ in self.config.get("configurable", {}) + if CONFIG_KEY_STREAM in config["configurable"]: + self.stream = DuplexStream( + self.stream, config["configurable"][CONFIG_KEY_STREAM] + ) def put_writes(self, task_id: str, writes: Sequence[tuple[str, Any]]) -> None: """Put writes for a task, to be read by the next tick.""" diff --git a/libs/langgraph/tests/any_str.py b/libs/langgraph/tests/any_str.py index 28a67ddf6..32383df34 100644 --- a/libs/langgraph/tests/any_str.py +++ b/libs/langgraph/tests/any_str.py @@ -18,7 +18,6 @@ class AnyDict(dict): super().__init__(*args, **kwargs) def __eq__(self, other: object) -> bool: - print("did we get here") if not isinstance(other, dict) or len(self) != len(other): return False for k, v in self.items(): diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 9b6a1967d..645761881 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -10897,7 +10897,10 @@ def test_doubly_nested_graph_state( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} - app.invoke({"my_key": "my value"}, config, debug=True) + assert [c for c in app.stream({"my_key": "my value"}, config, subgraphs=True)] == [ + {"parent_1": {"my_key": "hi my value"}}, + {"grandchild_1": {"my_key": "hi my value here"}}, + ] # get state without subgraphs outer_state = app.get_state(config) assert outer_state == StateSnapshot( @@ -11117,7 +11120,12 @@ def test_doubly_nested_graph_state( }, ) # resume - app.invoke(None, config, debug=True) + assert [c for c in app.stream(None, config, subgraphs=True)] == [ + {"grandchild_2": {"my_key": "hi my value here and there"}}, + {"child_1": {"my_key": "hi my value here and there"}}, + {"child": {"my_key": "hi my value here and there"}}, + {"parent_2": {"my_key": "hi my value here and there and back again"}}, + ] # get state with and without subgraphs assert ( app.get_state(config) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 9163bfe7f..f59003ac4 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -9338,7 +9338,12 @@ async def test_doubly_nested_graph_state( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} - await app.ainvoke({"my_key": "my value"}, config, debug=True) + assert [ + c async for c in app.astream({"my_key": "my value"}, config, subgraphs=True) + ] == [ + {"parent_1": {"my_key": "hi my value"}}, + {"grandchild_1": {"my_key": "hi my value here"}}, + ] # get state without subgraphs outer_state = await app.aget_state(config) assert outer_state == StateSnapshot( @@ -9558,7 +9563,12 @@ async def test_doubly_nested_graph_state( }, ) # resume - await app.ainvoke(None, config, debug=True) + assert [c async for c in app.astream(None, config, subgraphs=True)] == [ + {"grandchild_2": {"my_key": "hi my value here and there"}}, + {"child_1": {"my_key": "hi my value here and there"}}, + {"child": {"my_key": "hi my value here and there"}}, + {"parent_2": {"my_key": "hi my value here and there and back again"}}, + ] # get state with and without subgraphs assert ( await app.aget_state(config) From dd52248ef28d9ada7b254208f3b4f7e34a7ba834 Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:02:59 -0700 Subject: [PATCH 182/287] [docs]: make react agent HIL guide a little better (#1526) * ocd * x * spelling * changes * edits * nits * changes * last nit --- examples/create-react-agent-hitl.ipynb | 125 ++++++++++++++++++++++--- 1 file changed, 110 insertions(+), 15 deletions(-) diff --git a/examples/create-react-agent-hitl.ipynb b/examples/create-react-agent-hitl.ipynb index 7a14da497..235f2b057 100644 --- a/examples/create-react-agent-hitl.ipynb +++ b/examples/create-react-agent-hitl.ipynb @@ -73,7 +73,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "id": "7a154152-973e-4b5d-aa13-48c617744a4c", "metadata": {}, "outputs": [], @@ -92,14 +92,14 @@ "\n", "\n", "@tool\n", - "def get_weather(city: Literal[\"nyc\", \"sf\"]):\n", - " \"\"\"Use this to get weather information.\"\"\"\n", - " if city == \"nyc\":\n", + "def get_weather(location: str):\n", + " \"\"\"Use this to get weather information from a given location.\"\"\"\n", + " if location.lower() in [\"nyc\", \"new york\"]:\n", " return \"It might be cloudy in nyc\"\n", - " elif city == \"sf\":\n", + " elif location.lower() in [\"sf\", \"san francisco\"]:\n", " return \"It's always sunny in sf\"\n", " else:\n", - " raise AssertionError(\"Unknown city\")\n", + " raise AssertionError(\"Unknown Location\")\n", "\n", "\n", "tools = [get_weather]\n", @@ -144,7 +144,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "9ffff6c3-a4f5-47c9-b51d-97caaee85cd6", "metadata": {}, "outputs": [ @@ -154,26 +154,35 @@ "text": [ "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", - "What's the weather in SF?\n", + "what is the weather in SF?\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "Tool Calls:\n", - " get_weather (call_0OMmuTLec9t8kxMVkllZCSxo)\n", - " Call ID: call_0OMmuTLec9t8kxMVkllZCSxo\n", + " get_weather (call_TcDfLuoCKLmQ7eG71SedxLZ6)\n", + " Call ID: call_TcDfLuoCKLmQ7eG71SedxLZ6\n", " Args:\n", - " city: sf\n" + " location: San Francisco, CA\n" ] } ], "source": [ + "from langchain_core.messages import HumanMessage\n", "config = {\"configurable\": {\"thread_id\": \"42\"}}\n", - "inputs = {\"messages\": [(\"user\", \"What's the weather in SF?\")]}\n", + "inputs = {\"messages\": [(\"user\", \"what is the weather in SF?\")]}\n", "\n", "print_stream(graph.stream(inputs, config, stream_mode=\"values\"))" ] }, + { + "cell_type": "markdown", + "id": "ca40a719", + "metadata": {}, + "source": [ + "We can verify that our graph stopped at the right place:" + ] + }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "3decf001-7228-4ed5-8779-2b9ed98a74ea", "metadata": {}, "outputs": [ @@ -190,9 +199,87 @@ "print(\"Next step: \", snapshot.next)" ] }, + { + "cell_type": "markdown", + "id": "7de6ca78", + "metadata": {}, + "source": [ + "Now we can either approve or edit the tool call before proceeding to the next node. If we wanted to approve the tool call, we would simply continue streaming the graph with `None` input. If we wanted to edit the tool call we need to update the state to have the correct tool call, and then after the update has been applied we can continue.\n", + "\n", + "We can try resuming and we will see an error arise:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "740bbaeb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: get_weather\n", + "\n", + "Error: AssertionError('Unknown Location')\n", + " Please fix your mistakes.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "It seems there was an issue with the location provided. Let's try specifying \"San Francisco, California\" more clearly.\n", + "Tool Calls:\n", + " get_weather (call_TZm9HCShGNEreglVJcmUdXqG)\n", + " Call ID: call_TZm9HCShGNEreglVJcmUdXqG\n", + " Args:\n", + " location: San Francisco, California\n" + ] + } + ], + "source": [ + "print_stream(graph.stream(None, config, stream_mode=\"values\"))" + ] + }, + { + "cell_type": "markdown", + "id": "c1cf5950", + "metadata": {}, + "source": [ + "This error arose because our tool argument of \"San Francisco, CA\" is not a location our tool recognizes.\n", + "\n", + "Let's show how we would edit the tool call to search for \"San Francisco\" instead of \"San Francisco, CA\" - since our tool as written treats \"San Francisco, CA\" as an unknown location. We will update the state and then resume streaming the graph and should see no errors arise:" + ] + }, { "cell_type": "code", "execution_count": 6, + "id": "1c81ed9f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'configurable': {'thread_id': '42',\n", + " 'checkpoint_ns': '',\n", + " 'checkpoint_id': '1ef66368-9772-67ea-8004-07c779869a0a'}}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state = graph.get_state(config)\n", + "\n", + "last_message = state.values['messages'][-1]\n", + "last_message.tool_calls[0]['args'] = {\"location\": \"San Francisco\"}\n", + "\n", + "graph.update_state(config, {\"messages\": [ last_message]})" + ] + }, + { + "cell_type": "code", + "execution_count": 7, "id": "83148e08-63e8-49e5-a08b-02dc907bed1d", "metadata": {}, "outputs": [ @@ -206,13 +293,21 @@ "It's always sunny in sf\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "The weather in San Francisco is currently sunny.\n" + "The weather in San Francisco is currently sunny. Enjoy the sunshine!\n" ] } ], "source": [ "print_stream(graph.stream(None, config, stream_mode=\"values\"))" ] + }, + { + "cell_type": "markdown", + "id": "8202a5f9", + "metadata": {}, + "source": [ + "Fantastic! Our graph updated properly to query the weather in San Francisco and got the correct \"It's always sunny in sf\" response from the tool, and then responded to the user accordingly." + ] } ], "metadata": { @@ -231,7 +326,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.11.9" } }, "nbformat": 4, From b479b88c7e2912bebbbc78484f48af1e03a48218 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 13:20:28 -0700 Subject: [PATCH 183/287] Add name of subgraph to streaming output --- libs/langgraph/langgraph/pregel/__init__.py | 91 +++++++++++---------- libs/langgraph/langgraph/pregel/loop.py | 21 +++-- libs/langgraph/tests/test_pregel.py | 12 +-- libs/langgraph/tests/test_pregel_async.py | 12 +-- 4 files changed, 73 insertions(+), 63 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 3d676a7bc..515a33aa4 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -1113,6 +1113,25 @@ class Pregel( {'type': 'task_result', 'timestamp': '2024-06-23T...+00:00', 'step': 2, 'payload': {'id': '...', 'name': 'b', 'result': [('alist', ['there'])]}} ``` """ + + def output() -> Iterator: + while loop.stream: + ns, mode, payload = loop.stream.popleft() + ns = ( + NS_SEP.join(p.split(NS_END)[0] for p in ns.split(NS_SEP)) + if ns + else "" + ) + if mode in stream_modes: + if subgraphs and isinstance(stream_mode, list): + yield (ns, mode, payload) + elif isinstance(stream_mode, list): + yield (mode, payload) + elif subgraphs: + yield (ns, payload) + else: + yield payload + config = ensure_config(merge_configs(self.config, config)) callback_manager = get_callback_manager_for_config(config) run_manager = callback_manager.on_chain_start( @@ -1176,13 +1195,7 @@ class Pregel( self.stream_channels_list, ) # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload + yield from output() # debug flag if debug: print_step_tasks(loop.step, loop.tasks) @@ -1237,13 +1250,8 @@ class Pregel( # remove references to loop vars del fut, task # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload + yield from output() + # maybe stop other tasks if _should_stop_others(done): break @@ -1259,13 +1267,7 @@ class Pregel( self.stream_channels_list, ) # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload + yield from output() # handle exit if loop.status == "out_of_steps": raise GraphRecursionError( @@ -1358,6 +1360,25 @@ class Pregel( {'type': 'task_result', 'timestamp': '2024-06-23T...+00:00', 'step': 2, 'payload': {'id': '...', 'name': 'b', 'result': [('alist', ['there'])]}} ``` """ + + def output() -> Iterator: + while loop.stream: + ns, mode, payload = loop.stream.popleft() + ns = ( + NS_SEP.join(p.split(NS_END)[0] for p in ns.split(NS_SEP)) + if ns + else "" + ) + if mode in stream_modes: + if subgraphs and isinstance(stream_mode, list): + yield (ns, mode, payload) + elif isinstance(stream_mode, list): + yield (mode, payload) + elif subgraphs: + yield (ns, payload) + else: + yield payload + config = ensure_config(merge_configs(self.config, config)) callback_manager = get_async_callback_manager_for_config(config) run_manager = await callback_manager.on_chain_start( @@ -1430,13 +1451,8 @@ class Pregel( self.stream_channels_list, ) # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload + for o in output(): + yield o # debug flag if debug: print_step_tasks(loop.step, loop.tasks) @@ -1493,13 +1509,9 @@ class Pregel( # remove references to loop vars del fut, task # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload + for o in output(): + yield o + # maybe stop other tasks if _should_stop_others(done): break @@ -1515,13 +1527,8 @@ class Pregel( self.stream_channels_list, ) # emit output - while loop.stream: - mode, payload = loop.stream.popleft() - if mode in stream_modes: - if isinstance(stream_mode, list): - yield (mode, payload) - else: - yield payload + for o in output(): + yield o # handle exit if loop.status == "out_of_steps": raise GraphRecursionError( diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 7ff95cc38..87551b83f 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -91,8 +91,8 @@ EMPTY_SEQ = () class StreamProtocol(Protocol): - def extend(self, values: Iterable[Tuple[str, Any]]) -> None: ... - def popleft(self) -> Tuple[str, Any]: ... + def extend(self, values: Iterable[Tuple[str, str, Any]]) -> None: ... + def popleft(self) -> Tuple[str, str, Any]: ... def __bool__(self) -> bool: ... @@ -100,11 +100,11 @@ class DuplexStream(StreamProtocol): def __init__(self, *streams: StreamProtocol) -> None: self.streams = streams - def extend(self, values: Iterable[Tuple[str, Any]]) -> None: + def extend(self, values: Iterable[Tuple[str, str, Any]]) -> None: for stream, vv in zip(self.streams, tee(values, len(self.streams))): stream.extend(vv) - def popleft(self) -> Tuple[str, Any]: + def popleft(self) -> Tuple[str, str, Any]: return self.streams[0].popleft() def __bool__(self) -> bool: @@ -207,11 +207,11 @@ class PregelLoop: ) if task := next((t for t in self.tasks if t.id == task_id), None): self.stream.extend( - ("updates", v) + (self.config["configurable"].get("checkpoint_ns", ""), "updates", v) for v in map_output_updates(self.output_keys, [(task, writes)]) ) self.stream.extend( - ("debug", v) + (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) for v in map_debug_task_results( self.step, [(task, writes)], self.stream_keys ) @@ -247,7 +247,7 @@ class PregelLoop: self._update_mv(key, values) # produce values output self.stream.extend( - ("values", v) + (self.config["configurable"].get("checkpoint_ns", ""), "values", v) for v in map_output_values(self.output_keys, writes, self.channels) ) # clear pending writes @@ -295,7 +295,7 @@ class PregelLoop: # produce debug output if self._checkpointer_put_after_previous is not None: self.stream.extend( - ("debug", v) + (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) for v in map_debug_checkpoint( self.step - 1, # printing checkpoint for previous step self.checkpoint_config, @@ -339,7 +339,10 @@ class PregelLoop: return False # produce debug output - self.stream.extend(("debug", v) for v in map_debug_tasks(self.step, self.tasks)) + self.stream.extend( + (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) + for v in map_debug_tasks(self.step, self.tasks) + ) return True diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 645761881..a6a338a75 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -10898,8 +10898,8 @@ def test_doubly_nested_graph_state( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} assert [c for c in app.stream({"my_key": "my value"}, config, subgraphs=True)] == [ - {"parent_1": {"my_key": "hi my value"}}, - {"grandchild_1": {"my_key": "hi my value here"}}, + ("", {"parent_1": {"my_key": "hi my value"}}), + ("child|child_1", {"grandchild_1": {"my_key": "hi my value here"}}), ] # get state without subgraphs outer_state = app.get_state(config) @@ -11121,10 +11121,10 @@ def test_doubly_nested_graph_state( ) # resume assert [c for c in app.stream(None, config, subgraphs=True)] == [ - {"grandchild_2": {"my_key": "hi my value here and there"}}, - {"child_1": {"my_key": "hi my value here and there"}}, - {"child": {"my_key": "hi my value here and there"}}, - {"parent_2": {"my_key": "hi my value here and there and back again"}}, + ("child|child_1", {"grandchild_2": {"my_key": "hi my value here and there"}}), + ("child", {"child_1": {"my_key": "hi my value here and there"}}), + ("", {"child": {"my_key": "hi my value here and there"}}), + ("", {"parent_2": {"my_key": "hi my value here and there and back again"}}), ] # get state with and without subgraphs assert ( diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index f59003ac4..830d69238 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -9341,8 +9341,8 @@ async def test_doubly_nested_graph_state( assert [ c async for c in app.astream({"my_key": "my value"}, config, subgraphs=True) ] == [ - {"parent_1": {"my_key": "hi my value"}}, - {"grandchild_1": {"my_key": "hi my value here"}}, + ("", {"parent_1": {"my_key": "hi my value"}}), + ("child|child_1", {"grandchild_1": {"my_key": "hi my value here"}}), ] # get state without subgraphs outer_state = await app.aget_state(config) @@ -9564,10 +9564,10 @@ async def test_doubly_nested_graph_state( ) # resume assert [c async for c in app.astream(None, config, subgraphs=True)] == [ - {"grandchild_2": {"my_key": "hi my value here and there"}}, - {"child_1": {"my_key": "hi my value here and there"}}, - {"child": {"my_key": "hi my value here and there"}}, - {"parent_2": {"my_key": "hi my value here and there and back again"}}, + ("child|child_1", {"grandchild_2": {"my_key": "hi my value here and there"}}), + ("child", {"child_1": {"my_key": "hi my value here and there"}}), + ("", {"child": {"my_key": "hi my value here and there"}}), + ("", {"parent_2": {"my_key": "hi my value here and there and back again"}}), ] # get state with and without subgraphs assert ( From ddf67d9233c296945aed2f611ec81c8458b54296 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 13:32:21 -0700 Subject: [PATCH 184/287] Add ns to subgraph stream events --- libs/langgraph/langgraph/pregel/__init__.py | 18 ++++-------------- libs/langgraph/tests/test_pregel.py | 18 ++++++++++++------ libs/langgraph/tests/test_pregel_async.py | 18 ++++++++++++------ 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 515a33aa4..cee4e16dc 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -1117,18 +1117,13 @@ class Pregel( def output() -> Iterator: while loop.stream: ns, mode, payload = loop.stream.popleft() - ns = ( - NS_SEP.join(p.split(NS_END)[0] for p in ns.split(NS_SEP)) - if ns - else "" - ) if mode in stream_modes: if subgraphs and isinstance(stream_mode, list): - yield (ns, mode, payload) + yield (tuple(ns.split(NS_SEP)) if ns else (), mode, payload) elif isinstance(stream_mode, list): yield (mode, payload) elif subgraphs: - yield (ns, payload) + yield (tuple(ns.split(NS_SEP)) if ns else (), payload) else: yield payload @@ -1364,18 +1359,13 @@ class Pregel( def output() -> Iterator: while loop.stream: ns, mode, payload = loop.stream.popleft() - ns = ( - NS_SEP.join(p.split(NS_END)[0] for p in ns.split(NS_SEP)) - if ns - else "" - ) if mode in stream_modes: if subgraphs and isinstance(stream_mode, list): - yield (ns, mode, payload) + yield (tuple(ns.split(NS_SEP)) if ns else (), mode, payload) elif isinstance(stream_mode, list): yield (mode, payload) elif subgraphs: - yield (ns, payload) + yield (tuple(ns.split(NS_SEP)) if ns else (), payload) else: yield payload diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index a6a338a75..269e2bbcc 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -10898,8 +10898,11 @@ def test_doubly_nested_graph_state( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} assert [c for c in app.stream({"my_key": "my value"}, config, subgraphs=True)] == [ - ("", {"parent_1": {"my_key": "hi my value"}}), - ("child|child_1", {"grandchild_1": {"my_key": "hi my value here"}}), + ((), {"parent_1": {"my_key": "hi my value"}}), + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_1": {"my_key": "hi my value here"}}, + ), ] # get state without subgraphs outer_state = app.get_state(config) @@ -11121,10 +11124,13 @@ def test_doubly_nested_graph_state( ) # resume assert [c for c in app.stream(None, config, subgraphs=True)] == [ - ("child|child_1", {"grandchild_2": {"my_key": "hi my value here and there"}}), - ("child", {"child_1": {"my_key": "hi my value here and there"}}), - ("", {"child": {"my_key": "hi my value here and there"}}), - ("", {"parent_2": {"my_key": "hi my value here and there and back again"}}), + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_2": {"my_key": "hi my value here and there"}}, + ), + ((AnyStr("child:"),), {"child_1": {"my_key": "hi my value here and there"}}), + ((), {"child": {"my_key": "hi my value here and there"}}), + ((), {"parent_2": {"my_key": "hi my value here and there and back again"}}), ] # get state with and without subgraphs assert ( diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 830d69238..b24848e6d 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -9341,8 +9341,11 @@ async def test_doubly_nested_graph_state( assert [ c async for c in app.astream({"my_key": "my value"}, config, subgraphs=True) ] == [ - ("", {"parent_1": {"my_key": "hi my value"}}), - ("child|child_1", {"grandchild_1": {"my_key": "hi my value here"}}), + ((), {"parent_1": {"my_key": "hi my value"}}), + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_1": {"my_key": "hi my value here"}}, + ), ] # get state without subgraphs outer_state = await app.aget_state(config) @@ -9564,10 +9567,13 @@ async def test_doubly_nested_graph_state( ) # resume assert [c async for c in app.astream(None, config, subgraphs=True)] == [ - ("child|child_1", {"grandchild_2": {"my_key": "hi my value here and there"}}), - ("child", {"child_1": {"my_key": "hi my value here and there"}}), - ("", {"child": {"my_key": "hi my value here and there"}}), - ("", {"parent_2": {"my_key": "hi my value here and there and back again"}}), + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_2": {"my_key": "hi my value here and there"}}, + ), + ((AnyStr("child:"),), {"child_1": {"my_key": "hi my value here and there"}}), + ((), {"child": {"my_key": "hi my value here and there"}}), + ((), {"parent_2": {"my_key": "hi my value here and there and back again"}}), ] # get state with and without subgraphs assert ( From 11e87286854c77e32c5ab37d4bcaa89b00c2d61c Mon Sep 17 00:00:00 2001 From: Rafael Viana Date: Thu, 29 Aug 2024 17:49:04 -0300 Subject: [PATCH 185/287] [docs]: fix parameter misspelled --- docs/docs/cloud/how-tos/interrupt_concurrent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/cloud/how-tos/interrupt_concurrent.md b/docs/docs/cloud/how-tos/interrupt_concurrent.md index d58df875b..e67fb4ce5 100644 --- a/docs/docs/cloud/how-tos/interrupt_concurrent.md +++ b/docs/docs/cloud/how-tos/interrupt_concurrent.md @@ -60,7 +60,7 @@ Now we can start our two runs and join the second on euntil it has completed: thread["thread_id"], assistant_id, input={"messages": [{"role": "human", "content": "what's the weather in nyc?"}]}, - multitask_strategychrom="interrupt", + multitask_strategy="interrupt", ) # wait until the second run completes await client.runs.join(thread["thread_id"], run["run_id"]) From c183f4f16a7f3f9568f4a8d1787c5ae7fadc6b5d Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 27 Aug 2024 11:22:23 -0700 Subject: [PATCH 186/287] Emit output for skipped tasks --- libs/langgraph/langgraph/pregel/loop.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 87551b83f..f03a26e31 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -205,6 +205,9 @@ class PregelLoop: writes, task_id, ) + self._output_writes(task_id, writes) + + def _output_writes(self, task_id: str, writes: Sequence[tuple[str, Any]]) -> None: if task := next((t for t in self.tasks if t.id == task_id), None): self.stream.extend( (self.config["configurable"].get("checkpoint_ns", ""), "updates", v) @@ -320,6 +323,10 @@ class PregelLoop: continue if task := next((t for t in self.tasks if t.id == tid), None): task.writes.append((k, v)) + # print output for any tasks we applied previous writes to + for task in self.tasks: + if task.writes: + self._output_writes(task.id, task.writes) # if all tasks have finished, re-tick if all(task.writes for task in self.tasks): From 791ab654cb2ab0b4c05ec7498800070cad57d2a7 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 13:56:26 -0700 Subject: [PATCH 187/287] Add cached mark --- libs/langgraph/langgraph/pregel/io.py | 3 +++ libs/langgraph/langgraph/pregel/loop.py | 19 +++++++++++-------- libs/langgraph/tests/test_pregel.py | 10 +++++++--- libs/langgraph/tests/test_pregel_async.py | 10 +++++++--- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/io.py index 6c9205248..acc6574e8 100644 --- a/libs/langgraph/langgraph/pregel/io.py +++ b/libs/langgraph/langgraph/pregel/io.py @@ -96,6 +96,7 @@ class AddableUpdatesDict(AddableDict): def map_output_updates( output_channels: Union[str, Sequence[str]], tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]], + cached: bool = False, ) -> Iterator[dict[str, Union[Any, dict[str, Any]]]]: """Map pending writes (a sequence of tuples (channel, value)) to output chunk.""" output_tasks = [ @@ -130,6 +131,8 @@ def map_output_updates( grouped[node] = None if len(value) == 1: grouped[node] = value[0] + if cached: + grouped["__metadata__"] = {"cached": cached} yield AddableUpdatesDict(grouped) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index f03a26e31..be92c9018 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -207,18 +207,21 @@ class PregelLoop: ) self._output_writes(task_id, writes) - def _output_writes(self, task_id: str, writes: Sequence[tuple[str, Any]]) -> None: + def _output_writes( + self, task_id: str, writes: Sequence[tuple[str, Any]], *, cached: bool = False + ) -> None: if task := next((t for t in self.tasks if t.id == task_id), None): self.stream.extend( (self.config["configurable"].get("checkpoint_ns", ""), "updates", v) - for v in map_output_updates(self.output_keys, [(task, writes)]) + for v in map_output_updates(self.output_keys, [(task, writes)], cached) ) - self.stream.extend( - (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) - for v in map_debug_task_results( - self.step, [(task, writes)], self.stream_keys + if not cached: + self.stream.extend( + (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) + for v in map_debug_task_results( + self.step, [(task, writes)], self.stream_keys + ) ) - ) def tick( self, @@ -326,7 +329,7 @@ class PregelLoop: # print output for any tasks we applied previous writes to for task in self.tasks: if task.writes: - self._output_writes(task.id, task.writes) + self._output_writes(task.id, task.writes, cached=True) # if all tasks have finished, re-tick if all(task.writes for task in self.tasks): diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 269e2bbcc..4aefed25a 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -798,8 +798,12 @@ def test_invoke_two_processes_in_out_interrupt( # re-running from any previous checkpoint w/out forking should do nothing assert [c for c in app.stream(None, history[0].config, stream_mode="updates")] == [] - assert [c for c in app.stream(None, history[1].config, stream_mode="updates")] == [] - assert [c for c in app.stream(None, history[2].config, stream_mode="updates")] == [] + assert [c for c in app.stream(None, history[1].config, stream_mode="updates")] == [ + {"two": {"output": 5}, "__metadata__": {"cached": True}}, + ] + assert [c for c in app.stream(None, history[2].config, stream_mode="updates")] == [ + {"one": {"inbox": 4}, "__metadata__": {"cached": True}}, + ] # forking and re-running from any prev checkpoint should re-run nodes fork_config = app.update_state(history[0].config, None) @@ -987,7 +991,7 @@ def test_fork_always_re_runs_nodes( ] == [] assert [ c for c in graph.stream(None, history[1].config, stream_mode="updates") - ] == [] + ] == [{"add_one": 1, "__metadata__": {"cached": True}}] # forking and re-running from any prev checkpoint should re-run nodes fork_config = graph.update_state(history[0].config, None) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index b24848e6d..b12209977 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1025,10 +1025,14 @@ async def test_invoke_two_processes_in_out_interrupt( ] == [] assert [ c async for c in app.astream(None, history[1].config, stream_mode="updates") - ] == [] + ] == [ + {"two": {"output": 5}, "__metadata__": {"cached": True}}, + ] assert [ c async for c in app.astream(None, history[2].config, stream_mode="updates") - ] == [] + ] == [ + {"one": {"inbox": 4}, "__metadata__": {"cached": True}}, + ] # forking and re-running from any prev checkpoint should re-run nodes fork_config = await app.aupdate_state(history[0].config, None) @@ -1220,7 +1224,7 @@ async def test_fork_always_re_runs_nodes( ] == [] assert [ c async for c in graph.astream(None, history[1].config, stream_mode="updates") - ] == [] + ] == [{"add_one": 1, "__metadata__": {"cached": True}}] # forking and re-running from any prev checkpoint should re-run nodes fork_config = await graph.aupdate_state(history[0].config, None) From 5343350a5cd0cf35f5cfb8914389f0e0c4304d1d Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:09:18 -0700 Subject: [PATCH 188/287] [docs]: conceptual guides for streaming from specific nodes/LLMs (#1537) * x * changes * typo --- docs/docs/concepts/low_level.md | 72 ++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index 8e3cf236f..7dcb58c8b 100644 --- a/docs/docs/concepts/low_level.md +++ b/docs/docs/concepts/low_level.md @@ -493,7 +493,7 @@ workflow.add_edge("call_model", END) app = workflow.compile() inputs = [{"role": "user", "content": "hi!"}] -async for event in app.astream_events({"messages": inputs}, version="v1"): +async for event in app.astream_events({"messages": inputs}, version="v2"): kind = event["event"] print(f"{kind}: {event['name']}") ``` @@ -567,4 +567,72 @@ guide for that [here](../how-tos/streaming-tokens.ipynb). !!! warning "ASYNC IN PYTHON<=3.10" - You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case. Please see examples [here](../how-tos/streaming-content.ipynb) and [here](../how-tos/streaming-events-from-within-tools.ipynb). \ No newline at end of file + You may fail to see events being emitted from inside a node when using `.astream_events` in Python <= 3.10. If you're using a Langchain RunnableLambda, a RunnableGenerator, or Tool asynchronously inside your node, you will have to propagate callbacks to these objects manually. This is because LangChain cannot automatically propagate callbacks to child objects in this case. Please see examples [here](../how-tos/streaming-content.ipynb) and [here](../how-tos/streaming-events-from-within-tools.ipynb). + +#### Only stream tokens from specific nodes/LLMs + + +There are certain cases where you have multiple nodes in your graph that make LLM calls, and you do not wish to stream the tokens from every single LLM call. For example, you may use one LLM as a planner for the next steps to take, and another LLM somewhere else in the graph that actually responds to the user. In that case, you most likely WON'T want to stream tokens from the planner LLM but WILL want to stream them from the respond to user LLM. Below we show two different ways of doing this, one by streaming from specific nodes only and the second by streaming from specific LLMs only. + +First, let's define our graph: + +```python +from langchain_openai import ChatOpenAI +from langgraph.graph import StateGraph, MessagesState, START, END + +model_1 = ChatOpenAI(model="gpt-3.5-turbo", name="model_1") +model_2 = ChatOpenAI(model="gpt-3.5-turbo", name="model_2") + +def call_first_model(state: MessagesState): + response = model_1.invoke(state['messages']) + return {"messages": response} + +def call_second_model(state: MessagesState): + response = model_2.invoke(state['messages']) + return {"messages": response} + +workflow = StateGraph(MessagesState) +workflow.add_node(call_first_model) +workflow.add_node(call_second_model) +workflow.add_edge(START, "call_first_model") +workflow.add_edge("call_first_model", "call_second_model") +workflow.add_edge("call_second_model", END) +app = workflow.compile() +``` + +**Streaming from specific node** + +In the case that we only want the output from a single node, we can use the event metadata to filter node names: + +```python +inputs = [{"role": "user", "content": "hi!"}] + +async for event in app.astream_events({"messages": inputs}, version="v2"): + # Get chat model tokens from a particular node + if event["event"] == "on_chat_model_stream" and event['metadata'].get('langgraph_node','') == "call_second_model": + print(event["data"]["chunk"].content, end="|", flush=True) +``` + +```shell +|Hello|!| How| can| I| help| you| today|?|| +``` + +As we can see only the response from the second LLM was streamed (you can tell because we only received a single response, if we had streamed both we would have received two "Hello! How can I help you today?" messages). + +**Streaming from specific LLM** + +Sometimes you might want to stream from specific LLMs instead of specific nodes. This could be the case if you have multiple LLM calls inside a single node, and only want to stream the output of a specific one or if you use the same LLM in different nodes and want to stream it's output anytime it is called. We can do this by using the `name` parameter for LLMs and events: + +```python +inputs = [{"role": "user", "content": "hi!"}] +async for event in app.astream_events({"messages": inputs}, version="v2"): + # Get chat model tokens from a particular LLM inside a particular node + if event["event"] == "on_chat_model_stream" and event['name'] == "model_2": + print(event["data"]["chunk"].content, end="|", flush=True) +``` + +```shell +|Hello|!| How| can| I| assist| you| today|?|| +``` + +As expected, we only see a single LLM response since the response from `model_1` was not streamed. \ No newline at end of file From 090c759da0cb09a97462be1824184525948d3925 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 15:39:14 -0700 Subject: [PATCH 189/287] Replay a nested checkpoint - note this isn't very useful until update_state is added --- libs/langgraph/langgraph/pregel/__init__.py | 115 +- libs/langgraph/langgraph/pregel/algo.py | 16 +- libs/langgraph/langgraph/pregel/config.py | 12 + libs/langgraph/langgraph/pregel/debug.py | 28 +- libs/langgraph/langgraph/pregel/loop.py | 39 +- libs/langgraph/langgraph/pregel/types.py | 10 + libs/langgraph/tests/any_str.py | 11 +- libs/langgraph/tests/test_pregel.py | 1902 +------------------ libs/langgraph/tests/test_pregel_async.py | 360 ++-- 9 files changed, 478 insertions(+), 2015 deletions(-) create mode 100644 libs/langgraph/langgraph/pregel/config.py diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index cee4e16dc..0ce8449c1 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -59,11 +59,13 @@ from langgraph.checkpoint.base import ( empty_checkpoint, ) from langgraph.constants import ( + CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_READ, CONFIG_KEY_RESUMING, CONFIG_KEY_SEND, CONFIG_KEY_STREAM, + CONFIG_KEY_TASK_ID, ERROR, INTERRUPT, NS_END, @@ -77,6 +79,7 @@ from langgraph.pregel.algo import ( local_write, prepare_next_tasks, ) +from langgraph.pregel.config import patch_configurable from langgraph.pregel.debug import ( print_step_checkpoint, print_step_tasks, @@ -442,7 +445,19 @@ class Pregel( return StateSnapshot( read_channels(channels, self.stream_channels_asis), tuple(t.name for t in next_tasks), - saved.config, + patch_configurable( + saved.config, + { + CONFIG_KEY_CHECKPOINT_MAP: { + **saved.metadata["parents"], + saved.config["configurable"][ + "checkpoint_ns" + ]: saved.checkpoint["id"], + } + }, + ) + if saved.metadata.get("parents") + else saved.config, saved.metadata, saved.checkpoint["ts"], saved.parent_config, @@ -518,7 +533,19 @@ class Pregel( return StateSnapshot( read_channels(channels, self.stream_channels_asis), tuple(t.name for t in next_tasks), - saved.config, + patch_configurable( + saved.config, + { + CONFIG_KEY_CHECKPOINT_MAP: { + **saved.metadata["parents"], + saved.config["configurable"][ + "checkpoint_ns" + ]: saved.checkpoint["id"], + } + }, + ) + if saved.metadata.get("parents") + else saved.config, saved.metadata, saved.checkpoint["ts"], saved.parent_config, @@ -546,12 +573,9 @@ class Pregel( for name, pregel in self.get_subgraphs(recurse=True): if name == recast_checkpoint_ns: return pregel.get_state( - { - "configurable": { - **config["configurable"], - CONFIG_KEY_CHECKPOINTER: checkpointer, - } - }, + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), subgraphs=subgraphs, ) else: @@ -584,12 +608,9 @@ class Pregel( async for name, pregel in self.aget_subgraphs(recurse=True): if name == recast_checkpoint_ns: return await pregel.aget_state( - { - "configurable": { - **config["configurable"], - CONFIG_KEY_CHECKPOINTER: checkpointer, - } - }, + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), subgraphs=subgraphs, ) else: @@ -627,12 +648,9 @@ class Pregel( for name, pregel in self.get_subgraphs(recurse=True): if name == recast_checkpoint_ns: yield from pregel.get_state_history( - { - "configurable": { - **config["configurable"], - CONFIG_KEY_CHECKPOINTER: checkpointer, - } - }, + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), filter=filter, before=before, limit=limit, @@ -678,12 +696,9 @@ class Pregel( async for name, pregel in self.aget_subgraphs(recurse=True): if name == recast_checkpoint_ns: async for state in pregel.aget_state_history( - { - "configurable": { - **config["configurable"], - CONFIG_KEY_CHECKPOINTER: checkpointer, - } - }, + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), filter=filter, before=before, limit=limit, @@ -717,9 +732,32 @@ class Pregel( node `as_node`. If `as_node` is not provided, it will be set to the last node that updated the state, if not ambiguous. """ - if not self.checkpointer: + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: raise ValueError("No checkpointer set") + if ( + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) + ) + # find the subgraph with the matching name + for name, pregel in self.get_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + return pregel.update_state( + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), + values, + as_node, + ) + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") + # get last checkpoint config = merge_configs(self.config, config) if self.config else config saved = self.checkpointer.get_tuple(config) @@ -729,21 +767,12 @@ class Pregel( ) step = saved.metadata.get("step", -1) if saved else -1 # merge configurable fields with previous checkpoint config - checkpoint_config = { - **config, - "configurable": { - **config["configurable"], - # TODO: add proper support for updating nested subgraph state - "checkpoint_ns": "", - }, - } + checkpoint_config = patch_configurable( + config, + {"checkpoint_ns": config["configurable"].get("checkpoint_ns", "")}, + ) if saved: - checkpoint_config = { - "configurable": { - **config.get("configurable", {}), - **saved.config["configurable"], - } - } + checkpoint_config = patch_configurable(config, saved.config["configurable"]) # find last node that updated the state, if not provided if values is None and as_node is None: return self.checkpointer.put( @@ -798,6 +827,7 @@ class Pregel( None, [INTERRUPT], None, + None, str(uuid5(UUID(checkpoint["id"]), INTERRUPT)), ) # execute task @@ -935,6 +965,7 @@ class Pregel( None, [INTERRUPT], None, + None, str(uuid5(UUID(checkpoint["id"]), INTERRUPT)), ) # execute task @@ -1016,7 +1047,7 @@ class Pregel( stream_mode = stream_mode if stream_mode is not None else self.stream_mode if not isinstance(stream_mode, list): stream_mode = [stream_mode] - if CONFIG_KEY_READ in config.get("configurable", {}): + if CONFIG_KEY_TASK_ID in config.get("configurable", {}): # if being called as a node in another graph, always use values mode stream_mode = ["values"] if CONFIG_KEY_CHECKPOINTER in config.get("configurable", {}): diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 936c609a4..e4e4b45eb 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -50,7 +50,7 @@ from langgraph.pregel.io import read_channel, read_channels from langgraph.pregel.log import logger from langgraph.pregel.manager import ChannelsManager from langgraph.pregel.read import PregelNode -from langgraph.pregel.types import All, PregelExecutableTask, PregelTask +from langgraph.pregel.types import EXACT_MATCH, All, PregelExecutableTask, PregelTask class WritesProtocol(Protocol): @@ -303,6 +303,7 @@ def prepare_next_tasks( if node := proc.get_node(): managed.replace_runtime_placeholders(step, packet.arg) writes = deque() + task_checkpoint_ns = f"{checkpoint_ns}:{task_id}" tasks.append( PregelExecutableTask( packet.node, @@ -351,11 +352,15 @@ def prepare_next_tasks( }, CONFIG_KEY_RESUMING: is_resuming, "checkpoint_id": None, - "checkpoint_ns": f"{checkpoint_ns}:{task_id}", + "checkpoint_ns": task_checkpoint_ns, }, ), triggers, proc.retry_policy, + None + if task_checkpoint_ns + in configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}) + else EXACT_MATCH, task_id, ) ) @@ -406,6 +411,7 @@ def prepare_next_tasks( if for_execution: if node := proc.get_node(): writes = deque() + task_checkpoint_ns = f"{checkpoint_ns}:{task_id}" tasks.append( PregelExecutableTask( name, @@ -455,11 +461,15 @@ def prepare_next_tasks( parent_ns: checkpoint["id"], }, CONFIG_KEY_RESUMING: is_resuming, - "checkpoint_ns": f"{checkpoint_ns}:{task_id}", + "checkpoint_ns": task_checkpoint_ns, }, ), triggers, proc.retry_policy, + None + if task_checkpoint_ns + in configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}) + else EXACT_MATCH, task_id, ) ) diff --git a/libs/langgraph/langgraph/pregel/config.py b/libs/langgraph/langgraph/pregel/config.py new file mode 100644 index 000000000..45b2ab1a9 --- /dev/null +++ b/libs/langgraph/langgraph/pregel/config.py @@ -0,0 +1,12 @@ +from typing import Any, Optional + +from langchain_core.runnables import RunnableConfig + + +def patch_configurable( + config: Optional[RunnableConfig], patch: dict[str, Any] +) -> RunnableConfig: + if config is None: + return {"configurable": patch} + else: + return {**config, "configurable": {**config["configurable"], **patch}} diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index fe32fe600..d408971a9 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -78,11 +78,11 @@ def map_debug_tasks( step: int, tasks: list[PregelExecutableTask] ) -> Iterator[DebugOutputTask]: ts = datetime.now(timezone.utc).isoformat() - for name, input, _, _, config, triggers, _, _ in tasks: - if config is not None and TAG_HIDDEN in config.get("tags", []): + for task in tasks: + if task.config is not None and TAG_HIDDEN in task.config.get("tags", []): continue - metadata = config["metadata"].copy() + metadata = task.config["metadata"].copy() metadata.pop("checkpoint_id", None) yield { @@ -90,10 +90,12 @@ def map_debug_tasks( "timestamp": ts, "step": step, "payload": { - "id": str(uuid5(TASK_NAMESPACE, json.dumps((name, step, metadata)))), - "name": name, - "input": input, - "triggers": triggers, + "id": str( + uuid5(TASK_NAMESPACE, json.dumps((task.name, step, metadata))) + ), + "name": task.name, + "input": task.input, + "triggers": task.triggers, }, } @@ -107,11 +109,11 @@ def map_debug_task_results( [stream_keys] if isinstance(stream_keys, str) else stream_keys ) ts = datetime.now(timezone.utc).isoformat() - for (name, _, _, _, config, _, _, _), writes in tasks: - if config is not None and TAG_HIDDEN in config.get("tags", []): + for task, writes in tasks: + if task.config is not None and TAG_HIDDEN in task.config.get("tags", []): continue - metadata = config["metadata"].copy() + metadata = task.config["metadata"].copy() metadata.pop("checkpoint_id", None) # TODO: make task IDs deterministic in tests and reuse task IDs for payload ID @@ -120,8 +122,10 @@ def map_debug_task_results( "timestamp": ts, "step": step, "payload": { - "id": str(uuid5(TASK_NAMESPACE, json.dumps((name, step, metadata)))), - "name": name, + "id": str( + uuid5(TASK_NAMESPACE, json.dumps((task.name, step, metadata))) + ), + "name": task.name, "error": next((w[1] for w in writes if w[0] == ERROR), None), "result": [w for w in writes if w[0] in stream_channels_list], "interrupts": [asdict(w[1]) for w in writes if w[0] == INTERRUPT], diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index be92c9018..6245a7ab0 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -40,9 +40,9 @@ from langgraph.checkpoint.base import ( ) from langgraph.constants import ( CONFIG_KEY_CHECKPOINT_MAP, - CONFIG_KEY_READ, CONFIG_KEY_RESUMING, CONFIG_KEY_STREAM, + CONFIG_KEY_TASK_ID, ERROR, INPUT, INTERRUPT, @@ -60,6 +60,7 @@ from langgraph.pregel.algo import ( prepare_next_tasks, should_interrupt, ) +from langgraph.pregel.config import patch_configurable from langgraph.pregel.debug import ( map_debug_checkpoint, map_debug_task_results, @@ -178,11 +179,30 @@ class PregelLoop: self.specs = specs self.output_keys = output_keys self.stream_keys = stream_keys - self.is_nested = CONFIG_KEY_READ in self.config.get("configurable", {}) + self.is_nested = CONFIG_KEY_TASK_ID in self.config.get("configurable", {}) if CONFIG_KEY_STREAM in config["configurable"]: self.stream = DuplexStream( self.stream, config["configurable"][CONFIG_KEY_STREAM] ) + if not self.is_nested and config["configurable"].get("checkpoint_ns"): + self.config = patch_configurable( + config, {"checkpoint_ns": "", "checkpoint_id": None} + ) + if ( + CONFIG_KEY_CHECKPOINT_MAP in self.config["configurable"] + and self.config["configurable"].get("checkpoint_ns") + in self.config["configurable"][CONFIG_KEY_CHECKPOINT_MAP] + ): + self.checkpoint_config = patch_configurable( + self.config, + { + "checkpoint_id": config["configurable"][CONFIG_KEY_CHECKPOINT_MAP][ + self.config["configurable"]["checkpoint_ns"] + ] + }, + ) + else: + self.checkpoint_config = config def put_writes(self, task_id: str, writes: Sequence[tuple[str, Any]]) -> None: """Put writes for a task, to be read by the next tick.""" @@ -324,7 +344,14 @@ class PregelLoop: for tid, k, v in self.checkpoint_pending_writes: if k in (ERROR, INTERRUPT): continue - if task := next((t for t in self.tasks if t.id == tid), None): + if task := next( + ( + t + for t in self.tasks + if t.id == tid and t.cache_policy is not None + ), + None, + ): task.writes.append((k, v)) # print output for any tasks we applied previous writes to for task in self.tasks: @@ -524,7 +551,9 @@ class SyncPregelLoop(PregelLoop, ContextManager): def __enter__(self) -> Self: saved = ( - self.checkpointer.get_tuple(self.config) if self.checkpointer else None + self.checkpointer.get_tuple(self.checkpoint_config) + if self.checkpointer + else None ) or CheckpointTuple(self.config, empty_checkpoint(), {"step": -2}, None, []) self.checkpoint_config = { **self.config, @@ -616,7 +645,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): async def __aenter__(self) -> Self: saved = ( - await self.checkpointer.aget_tuple(self.config) + await self.checkpointer.aget_tuple(self.checkpoint_config) if self.checkpointer else None ) or CheckpointTuple(self.config, empty_checkpoint(), {"step": -2}, None, []) diff --git a/libs/langgraph/langgraph/pregel/types.py b/libs/langgraph/langgraph/pregel/types.py index d1ac00fd1..8a5a37b37 100644 --- a/libs/langgraph/langgraph/pregel/types.py +++ b/libs/langgraph/langgraph/pregel/types.py @@ -57,6 +57,15 @@ class RetryPolicy(NamedTuple): """List of exception classes that should trigger a retry, or a callable that returns True for exceptions that should trigger a retry.""" +class CachePolicy(NamedTuple): + """Configuration for caching nodes.""" + + pass + + +EXACT_MATCH = CachePolicy() + + class PregelTask(NamedTuple): id: str name: str @@ -73,6 +82,7 @@ class PregelExecutableTask(NamedTuple): config: RunnableConfig triggers: list[str] retry_policy: Optional[RetryPolicy] + cache_policy: Optional[CachePolicy] id: str diff --git a/libs/langgraph/tests/any_str.py b/libs/langgraph/tests/any_str.py index 32383df34..66dc381b7 100644 --- a/libs/langgraph/tests/any_str.py +++ b/libs/langgraph/tests/any_str.py @@ -1,16 +1,21 @@ +import re from typing import Any, Sequence class AnyStr(str): - def __init__(self, prefix: str = "") -> None: + def __init__(self, prefix: str | re.Pattern = "") -> None: super().__init__() self.prefix = prefix def __eq__(self, other: object) -> bool: - return isinstance(other, str) and other.startswith(self.prefix) + return isinstance(other, str) and ( + other.startswith(self.prefix) + if isinstance(self.prefix, str) + else self.prefix.match(other) + ) def __hash__(self) -> int: - return hash(str(self)) + return hash((str(self), self.prefix)) class AnyDict(dict): diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 4aefed25a..1c274cf92 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -1,5 +1,6 @@ import json import operator +import re import time import warnings from collections import Counter @@ -8318,1815 +8319,6 @@ def test_nested_graph(snapshot: SnapshotAssertion) -> None: ] -@pytest.mark.skip("TODO") -@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) -def test_nested_graph_interrupts( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - - class InnerState(TypedDict): - my_key: str - my_other_key: str - - def inner_1(state: InnerState): - return { - "my_key": state["my_key"] + " here", - "my_other_key": state["my_key"], - } - - def inner_2(state: InnerState): - return { - "my_key": state["my_key"] + " and there", - "my_other_key": state["my_key"], - } - - 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 - - def outer_1(state: State): - return {"my_key": "hi " + state["my_key"]} - - def outer_2(state: State): - return {"my_key": state["my_key"] + " and back again"} - - graph = StateGraph(State) - graph.add_node("outer_1", outer_1) - graph.add_node( - "inner", - inner.compile(interrupt_before=["inner_2"]), - ) - graph.add_node("outer_2", outer_2) - graph.set_entry_point("outer_1") - graph.add_edge("outer_1", "inner") - graph.add_edge("inner", "outer_2") - graph.set_finish_point("outer_2") - - app = graph.compile(checkpointer=checkpointer) - - # test invoke w/ nested interrupt - config = {"configurable": {"thread_id": "1"}} - assert app.invoke({"my_key": "my value"}, config) == { - "my_key": "hi my value", - } - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=( - PregelTask( - AnyStr(), - "inner_2", - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert app.invoke(None, config, debug=True) == { - "my_key": "hi my value here and there and back again", - } - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - # test stream updates w/ nested interrupt - config = {"configurable": {"thread_id": "2"}} - assert [*app.stream({"my_key": "my value"}, config)] == [ - {"outer_1": {"my_key": "hi my value"}}, - ] - assert [*app.stream(None, config)] == [ - {"inner": {"my_key": "hi my value here and there"}}, - {"outer_2": {"my_key": "hi my value here and there and back again"}}, - ] - - # test stream values w/ nested interrupt - config = {"configurable": {"thread_id": "3"}} - assert [*app.stream({"my_key": "my value"}, config, stream_mode="values")] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - { - "my_key": "hi my value here and there and back again", - }, - ] - - # test interrupts BEFORE the node w/ interrupts - app = graph.compile(checkpointer=checkpointer, interrupt_before=["inner"]) - config = {"configurable": {"thread_id": "4"}} - assert [*app.stream({"my_key": "my value"}, config, stream_mode="values")] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - history = list(app.get_state_history(config)) - assert history[0] == StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=None, # no state because we haven't entered this node yet - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - assert history[1] == StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - assert history[2] == StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ) - - # while we're waiting for the node w/ interrupt inside to finish - assert [*app.stream(None, config, stream_mode="values")] == [] - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=( - PregelTask( - AnyStr(), - "inner_2", - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - { - "my_key": "hi my value here and there and back again", - }, - ] - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - # test interrupts AFTER the node w/ interrupts - app = graph.compile(checkpointer=checkpointer, interrupt_after=["inner"]) - config = {"configurable": {"thread_id": "5"}} - assert [*app.stream({"my_key": "my value"}, config, stream_mode="values")] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=( - PregelTask( - AnyStr(), - name="inner_2", - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - ] - # interrupted after "inner" - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there and back again", - }, - ] - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - # test restarting from checkpoint_id - config = {"configurable": {"thread_id": "6"}} - app = graph.compile(checkpointer=checkpointer) - assert app.invoke({"my_key": "my value"}, config, debug=True) == { - "my_key": "hi my value" - } - state_history = [c for c in app.get_state_history(config)] - assert state_history == [ - StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - child_state_history = [ - c - for c in app.checkpointer.list( - { - "configurable": { - "thread_id": "6", - "checkpoint_ns": f"inner:{state_history[0].tasks[0].id}", - } - } - ) - ] - assert child_state_history == [ - CheckpointTuple( - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner:478a8877-4528-5ecf-9ba3-773c53e2db7f", - "checkpoint_id": "1ef64d0c-0c7a-63ee-8001-4c403fb2fdd8", - } - }, - checkpoint={ - "v": 1, - "ts": "2024-08-28T00:02:02.205896+00:00", - "id": "1ef64d0c-0c7a-63ee-8001-4c403fb2fdd8", - "channel_values": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - "inner_1": "inner_1", - }, - "channel_versions": { - "__start__": 2, - "my_key": 3, - "start:inner_1": 3, - "inner_1": 3, - "my_other_key": 3, - }, - "versions_seen": { - "__input__": {}, - "__start__": {"__start__": 1}, - "inner_1": {"start:inner_1": 2}, - }, - "pending_sends": [], - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner:478a8877-4528-5ecf-9ba3-773c53e2db7f", - "checkpoint_id": "1ef64d0c-0c78-6f26-8000-2c99f0dae586", - } - }, - pending_writes=[], - ), - CheckpointTuple( - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner:478a8877-4528-5ecf-9ba3-773c53e2db7f", - "checkpoint_id": "1ef64d0c-0c78-6f26-8000-2c99f0dae586", - } - }, - checkpoint={ - "v": 1, - "ts": "2024-08-28T00:02:02.205364+00:00", - "id": "1ef64d0c-0c78-6f26-8000-2c99f0dae586", - "channel_values": { - "my_key": "hi my value", - "start:inner_1": "__start__", - }, - "channel_versions": {"__start__": 2, "my_key": 2, "start:inner_1": 2}, - "versions_seen": {"__input__": {}, "__start__": {"__start__": 1}}, - "pending_sends": [], - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner:478a8877-4528-5ecf-9ba3-773c53e2db7f", - "checkpoint_id": "1ef64d0c-0c78-63dc-bfff-5d20aa222eff", - } - }, - pending_writes=[ - ("1defebd2-5caa-5a51-87f8-e38b7a5c16f9", "inner_1", "inner_1"), - ("1defebd2-5caa-5a51-87f8-e38b7a5c16f9", "my_key", "hi my value here"), - ("1defebd2-5caa-5a51-87f8-e38b7a5c16f9", "my_other_key", "hi my value"), - ], - ), - CheckpointTuple( - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner:478a8877-4528-5ecf-9ba3-773c53e2db7f", - "checkpoint_id": "1ef64d0c-0c78-63dc-bfff-5d20aa222eff", - } - }, - checkpoint={ - "v": 1, - "ts": "2024-08-28T00:02:02.205076+00:00", - "id": "1ef64d0c-0c78-63dc-bfff-5d20aa222eff", - "channel_values": {"__start__": {"my_key": "hi my value"}}, - "channel_versions": {"__start__": 1}, - "versions_seen": {"__input__": {}}, - "pending_sends": [], - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "hi my value"}, - "step": -1, - }, - parent_config=None, - pending_writes=[ - ("494a4aca-177b-5e8a-986c-987ff91978f5", "my_key", "hi my value"), - ("494a4aca-177b-5e8a-986c-987ff91978f5", "start:inner_1", "__start__"), - ], - ), - ] - - # check that child snapshot matches id of parent - child_snapshot = child_state_history[0] - assert ( - child_snapshot.config["configurable"]["checkpoint_id"] - == state_history[0].config["configurable"]["checkpoint_id"] - ) - # check resuming from interrupt w/ checkpoint_id - interrupt_state_snapshot, before_interrupt_state_snapshot = state_history[:2] - before_interrupt_config = before_interrupt_state_snapshot.config - # going to get to interrupt again here, so the output is None - assert app.invoke(None, before_interrupt_config, debug=True) == { - "my_key": "hi my value" - } - # one more "identical" snapshot than before, at top of list - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - # going to restart from interrupt - interrupt_config = interrupt_state_snapshot.config - assert app.invoke(None, interrupt_config, debug=True) == { - "my_key": "hi my value here and there and back again", - } - assert list(app.get_state_history(config)) == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - -@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_nested_graph_interrupts_parallel( request: pytest.FixtureRequest, checkpointer_name: str @@ -10164,10 +8356,7 @@ def test_nested_graph_interrupts_parallel( return {"my_key": " and back again"} graph = StateGraph(State) - graph.add_node( - "inner", - inner.compile(interrupt_before=["inner_2"]), - ) + graph.add_node("inner", inner.compile(interrupt_before=["inner_2"])) graph.add_node("outer_1", outer_1) graph.add_node("outer_2", outer_2) @@ -10193,11 +8382,13 @@ 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)] == [ + assert [*app.stream({"my_key": ""}, config, subgraphs=True)] == [ # we got to parallel node first - {"outer_1": {"my_key": " and parallel"}}, + ((), {"outer_1": {"my_key": " and parallel"}}), + ((AnyStr("inner:"),), {"inner_1": {"my_key": "got here", "my_other_key": ""}}), ] assert [*app.stream(None, config)] == [ + {"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}}, {"inner": {"my_key": "got here and there"}}, {"outer_2": {"my_key": " and back again"}}, ] @@ -10461,6 +8652,9 @@ def test_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -10603,6 +8797,9 @@ def test_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -10634,6 +8831,9 @@ def test_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -10660,6 +8860,9 @@ def test_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -11003,6 +9206,13 @@ def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), } }, metadata={ @@ -11052,6 +9262,15 @@ def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr( + re.compile(r"child:.+|child1:") + ): AnyStr(), + } + ), } }, metadata={ @@ -11084,6 +9303,9 @@ def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -11316,6 +9538,9 @@ def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -11342,6 +9567,9 @@ def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -11379,6 +9607,9 @@ def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -11403,6 +9634,13 @@ def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), } }, metadata={ @@ -11434,6 +9672,13 @@ def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), } }, metadata={ @@ -11465,6 +9710,13 @@ def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), } }, metadata={ @@ -11496,6 +9748,13 @@ def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), } }, metadata={ @@ -11515,6 +9774,19 @@ def test_doubly_nested_graph_state( ), ] + # replay grandchild checkpoint + assert [ + c for c in app.stream(None, grandchild_history[2].config, subgraphs=True) + ] == [ + ( + (AnyStr("child:"), AnyStr("child_1:")), + { + "grandchild_1": {"my_key": "hi my value here"}, + "__metadata__": {"cached": True}, + }, + ) + ] + @pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index b12209977..9c39ec2b1 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1,6 +1,7 @@ import asyncio import json import operator +import re from collections import Counter from contextlib import asynccontextmanager, contextmanager from typing import ( @@ -8892,6 +8893,9 @@ async def test_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -9039,6 +9043,9 @@ async def test_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("inner:"): AnyStr()} + ), } }, metadata={ @@ -9070,6 +9077,9 @@ async def test_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("inner:"): AnyStr()} + ), } }, metadata={ @@ -9096,6 +9106,9 @@ async def test_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("inner:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("inner:"): AnyStr()} + ), } }, metadata={ @@ -9413,6 +9426,9 @@ async def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -9446,6 +9462,13 @@ async def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), } }, metadata={ @@ -9495,6 +9518,15 @@ async def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr( + re.compile(r"child:.+|child1:") + ): AnyStr(), + } + ), } }, metadata={ @@ -9527,6 +9559,9 @@ async def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -9614,145 +9649,150 @@ async def test_doubly_nested_graph_state( ) # get outer graph history outer_history = [c async for c in app.aget_state_history(config)] - assert outer_history == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "parent_2": {"my_key": "hi my value here and there and back again"} + assert ( + outer_history[0] + == [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - next=("parent_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"child": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - tasks=(PregelTask(id=AnyStr(), name="parent_2"),), - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "child", - state={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child"), + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "parent_2": { + "my_key": "hi my value here and there and back again" } }, - ), + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, ), - next=("child",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"parent_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - next=("parent_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": None, - "step": 0, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - tasks=(PregelTask(id=AnyStr(), name="parent_1"),), - ), - StateSnapshot( - values={}, - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - tasks=(PregelTask(id=AnyStr(), name="__start__"),), - ), - ] + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=("parent_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"child": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child"), + } + }, + ), + ), + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("parent_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ][0] + ) # get child graph history child_history = [ c async for c in app.aget_state_history(outer_history[2].tasks[0].state) @@ -9766,6 +9806,9 @@ async def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -9792,6 +9835,9 @@ async def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -9829,6 +9875,9 @@ async def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ @@ -9855,6 +9904,13 @@ async def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), } }, metadata={ @@ -9886,6 +9942,13 @@ async def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), } }, metadata={ @@ -9917,6 +9980,13 @@ async def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), } }, metadata={ @@ -9948,6 +10018,13 @@ async def test_doubly_nested_graph_state( "thread_id": "1", "checkpoint_ns": AnyStr(), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), } }, metadata={ @@ -9967,6 +10044,19 @@ async def test_doubly_nested_graph_state( ), ] + # replay grandchild checkpoint + assert [ + c async for c in app.astream(None, grandchild_history[2].config, subgraphs=True) + ] == [ + ( + (AnyStr("child:"), AnyStr("child_1:")), + { + "grandchild_1": {"my_key": "hi my value here"}, + "__metadata__": {"cached": True}, + }, + ) + ] + @pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) From a707eef6f74c1dbc6c7b750f7aab4b360287281d Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 15:42:25 -0700 Subject: [PATCH 190/287] Lint --- libs/langgraph/tests/any_str.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/tests/any_str.py b/libs/langgraph/tests/any_str.py index 66dc381b7..9a1977a8c 100644 --- a/libs/langgraph/tests/any_str.py +++ b/libs/langgraph/tests/any_str.py @@ -1,9 +1,9 @@ import re -from typing import Any, Sequence +from typing import Any, Sequence, Union class AnyStr(str): - def __init__(self, prefix: str | re.Pattern = "") -> None: + def __init__(self, prefix: Union[str, re.Pattern] = "") -> None: super().__init__() self.prefix = prefix From 88658311045eca5489682cc8e148e7698df92f48 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 15:42:29 -0700 Subject: [PATCH 191/287] Update async test --- libs/langgraph/tests/test_pregel_async.py | 1754 +-------------------- 1 file changed, 4 insertions(+), 1750 deletions(-) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 9c39ec2b1..51e9690c8 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -6806,1754 +6806,6 @@ async def test_nested_graph(snapshot: SnapshotAssertion) -> None: assert times_called == 1 -@pytest.mark.skip("TODO") -@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_nested_graph_interrupts( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - - class InnerState(TypedDict): - my_key: str - my_other_key: str - - async def inner_1(state: InnerState): - return { - "my_key": state["my_key"] + " here", - "my_other_key": state["my_key"], - } - - async def inner_2(state: InnerState): - return { - "my_key": state["my_key"] + " and there", - "my_other_key": state["my_key"], - } - - 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 - - async def outer_1(state: State): - return {"my_key": "hi " + state["my_key"]} - - async def outer_2(state: State): - return {"my_key": state["my_key"] + " and back again"} - - graph = StateGraph(State) - graph.add_node("outer_1", outer_1) - graph.add_node( - "inner", - inner.compile(interrupt_before=["inner_2"]), - ) - graph.add_node("outer_2", outer_2) - graph.set_entry_point("outer_1") - graph.add_edge("outer_1", "inner") - graph.add_edge("inner", "outer_2") - graph.set_finish_point("outer_2") - - app = graph.compile(checkpointer=checkpointer) - - # test invoke w/ nested interrupt - config = {"configurable": {"thread_id": "1"}} - assert await app.ainvoke({"my_key": "my value"}, config, debug=True) == { - "my_key": "hi my value", - } - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=( - PregelTask( - AnyStr(), - "inner_2", - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert await app.ainvoke(None, config, debug=True) == { - "my_key": "hi my value here and there and back again", - } - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - next=(), - tasks=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - # test stream updates w/ nested interrupt - config = {"configurable": {"thread_id": "2"}} - assert [c async for c in app.astream({"my_key": "my value"}, config)] == [ - {"outer_1": {"my_key": "hi my value"}}, - ] - assert [c async for c in app.astream(None, config)] == [ - {"inner": {"my_key": "hi my value here and there"}}, - {"outer_2": {"my_key": "hi my value here and there and back again"}}, - ] - - # test stream values w/ nested interrupt - config = {"configurable": {"thread_id": "3"}} - assert [ - c - async for c in app.astream({"my_key": "my value"}, config, stream_mode="values") - ] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - { - "my_key": "hi my value here and there and back again", - }, - ] - - # test interrupts BEFORE the node w/ interrupts - app = graph.compile(checkpointer=checkpointer, interrupt_before=["inner"]) - config = {"configurable": {"thread_id": "4"}} - assert [ - c - async for c in app.astream({"my_key": "my value"}, config, stream_mode="values") - ] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - # 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 [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=( - PregelTask( - AnyStr(), - "inner_2", - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - { - "my_key": "hi my value here and there and back again", - }, - ] - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "4", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - # test interrupts AFTER the node w/ interrupts - app = graph.compile(checkpointer=checkpointer, interrupt_after=["inner"]) - config = {"configurable": {"thread_id": "5"}} - assert [ - c - async for c in app.astream({"my_key": "my value"}, config, stream_mode="values") - ] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, - ] - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=( - PregelTask( - AnyStr(), - name="inner_2", - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - ] - # interrupted after "inner" - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there and back again", - }, - ] - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "5", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - # test restarting from checkpoint_id - config = {"configurable": {"thread_id": "6"}} - app = graph.compile(checkpointer=checkpointer) - await app.ainvoke({"my_key": "my value"}, config, debug=True) - - state_history = [c async for c in app.aget_state_history(config)] - assert state_history == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=( - PregelTask( - AnyStr(), - "inner_2", - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - child_state_history = [ - c - async for c in app.aget_state_history( - {"configurable": {"thread_id": "6", "checkpoint_ns": "inner"}} - ) - ] - assert child_state_history == [ - StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ), - # there should be a single child checkpoint because we only keep - # one child checkpoint per parent checkpoint (in which child ran) - ] - - # check that child snapshot matches id of parent - child_snapshot = child_state_history[0] - assert ( - child_snapshot.config["configurable"]["checkpoint_id"] - == state_history[0].config["configurable"]["checkpoint_id"] - ) - # check resuming from interrupt w/ checkpoint_id - interrupt_state_snapshot, before_interrupt_state_snapshot = state_history[:2] - before_interrupt_config = before_interrupt_state_snapshot.config - # going to get to interrupt again here, so the output is None - assert await app.ainvoke(None, before_interrupt_config, debug=True) == { - "my_key": "hi my value" - } - # one more "identical" snapshot than before, at top of list - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ), - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ), - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - # going to restart from interrupt - interrupt_config = interrupt_state_snapshot.config - assert await app.ainvoke(None, interrupt_config, debug=True) == { - "my_key": "hi my value here and there and back again", - } - assert [s async for s in app.aget_state_history(config)] == [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=(PregelTask(AnyStr(), "inner_2"),), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ), - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=(PregelTask(AnyStr(), "inner"),), - next=("inner",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - subgraphs={ - "inner": StateSnapshot( - values={ - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "inner_2": { - "my_key": "hi my value here and there", - "my_other_key": "hi my value here", - } - }, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "inner", - "checkpoint_id": AnyStr(), - } - }, - ) - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "6", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"my_key": "my value"}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - - -@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_nested_graph_interrupts_parallel( request: pytest.FixtureRequest, checkpointer_name: str @@ -8620,11 +6872,13 @@ async 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 [c async for c in app.astream({"my_key": ""}, config)] == [ + assert [c async for c in app.astream({"my_key": ""}, config, subgraphs=True)] == [ # we got to parallel node first - {"outer_1": {"my_key": " and parallel"}}, + ((), {"outer_1": {"my_key": " and parallel"}}), + ((AnyStr("inner:"),), {"inner_1": {"my_key": "got here", "my_other_key": ""}}), ] assert [c async for c in app.astream(None, config)] == [ + {"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}}, {"inner": {"my_key": "got here and there"}}, {"outer_2": {"my_key": " and back again"}}, ] From 4fc4080597e56c33e9f65d71174972bfb966aa65 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 15:49:43 -0700 Subject: [PATCH 192/287] Enable test for doubly nested interrupts --- libs/langgraph/tests/test_pregel.py | 1 - libs/langgraph/tests/test_pregel_async.py | 33 +++++++++-------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 1c274cf92..d4801ffc3 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -8442,7 +8442,6 @@ def test_nested_graph_interrupts_parallel( ] -@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_doubly_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 51e9690c8..f0c48efdc 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -6888,17 +6888,11 @@ async def test_nested_graph_interrupts_parallel( assert [ c async for c in app.astream({"my_key": ""}, config, stream_mode="values") ] == [ - { - "my_key": "", - }, + {"my_key": ""}, ] assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "got here and there and parallel", - }, - { - "my_key": "got here and there and parallel and back again", - }, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, ] # # test interrupts BEFORE the parallel node @@ -6906,16 +6900,14 @@ async def test_nested_graph_interrupts_parallel( config = {"configurable": {"thread_id": "4"}} assert [ c async for c in app.astream({"my_key": ""}, config, stream_mode="values") - ] == [{"my_key": ""}] + ] == [ + {"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")] == [ - { - "my_key": "got here and there and parallel", - }, - { - "my_key": "got here and there and parallel and back again", - }, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, ] # test interrupts AFTER the parallel node @@ -6923,18 +6915,17 @@ async def test_nested_graph_interrupts_parallel( config = {"configurable": {"thread_id": "5"}} assert [ c async for c in app.astream({"my_key": ""}, config, stream_mode="values") - ] == [{"my_key": ""}] + ] == [ + {"my_key": ""}, + ] assert [c async for c in app.astream(None, config, stream_mode="values")] == [ {"my_key": "got here and there and parallel"}, ] assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "got here and there and parallel and back again", - }, + {"my_key": "got here and there and parallel and back again"}, ] -@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_doubly_nested_graph_interrupts( request: pytest.FixtureRequest, checkpointer_name: str From 413391067036115ca05ce3cac704eb12b81f96b3 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 16:41:47 -0700 Subject: [PATCH 193/287] Implement update_state for nested graphs --- libs/langgraph/langgraph/pregel/__init__.py | 83 ++++++----- libs/langgraph/langgraph/pregel/config.py | 22 +++ libs/langgraph/tests/test_pregel.py | 156 +++++++++++++++----- libs/langgraph/tests/test_pregel_async.py | 94 ++++++++---- 4 files changed, 249 insertions(+), 106 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 0ce8449c1..173304ed2 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -59,7 +59,6 @@ from langgraph.checkpoint.base import ( empty_checkpoint, ) from langgraph.constants import ( - CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_READ, CONFIG_KEY_RESUMING, @@ -79,7 +78,7 @@ from langgraph.pregel.algo import ( local_write, prepare_next_tasks, ) -from langgraph.pregel.config import patch_configurable +from langgraph.pregel.config import patch_checkpoint_map, patch_configurable from langgraph.pregel.debug import ( print_step_checkpoint, print_step_tasks, @@ -445,19 +444,7 @@ class Pregel( return StateSnapshot( read_channels(channels, self.stream_channels_asis), tuple(t.name for t in next_tasks), - patch_configurable( - saved.config, - { - CONFIG_KEY_CHECKPOINT_MAP: { - **saved.metadata["parents"], - saved.config["configurable"][ - "checkpoint_ns" - ]: saved.checkpoint["id"], - } - }, - ) - if saved.metadata.get("parents") - else saved.config, + patch_checkpoint_map(saved.config, saved.metadata), saved.metadata, saved.checkpoint["ts"], saved.parent_config, @@ -533,19 +520,7 @@ class Pregel( return StateSnapshot( read_channels(channels, self.stream_channels_asis), tuple(t.name for t in next_tasks), - patch_configurable( - saved.config, - { - CONFIG_KEY_CHECKPOINT_MAP: { - **saved.metadata["parents"], - saved.config["configurable"][ - "checkpoint_ns" - ]: saved.checkpoint["id"], - } - }, - ) - if saved.metadata.get("parents") - else saved.config, + patch_checkpoint_map(saved.config, saved.metadata), saved.metadata, saved.checkpoint["ts"], saved.parent_config, @@ -738,6 +713,7 @@ class Pregel( if not checkpointer: raise ValueError("No checkpointer set") + # delegate to subgraph if ( checkpoint_ns := config["configurable"].get("checkpoint_ns", "") ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: @@ -760,7 +736,7 @@ class Pregel( # get last checkpoint config = merge_configs(self.config, config) if self.config else config - saved = self.checkpointer.get_tuple(config) + saved = checkpointer.get_tuple(config) checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() checkpoint_previous_versions = ( saved.checkpoint["channel_versions"].copy() if saved else {} @@ -775,7 +751,7 @@ class Pregel( checkpoint_config = patch_configurable(config, saved.config["configurable"]) # find last node that updated the state, if not provided if values is None and as_node is None: - return self.checkpointer.put( + next_config = checkpointer.put( checkpoint_config, create_checkpoint(checkpoint, None, step), { @@ -786,6 +762,7 @@ class Pregel( }, {}, ) + return patch_checkpoint_map(next_config, saved.metadata if saved else None) elif as_node is None and not any( v for vv in checkpoint["versions_seen"].values() for v in vv.values() ): @@ -860,13 +837,13 @@ class Pregel( ) # save task writes if saved: - self.checkpointer.put_writes(checkpoint_config, task.writes, task.id) + checkpointer.put_writes(checkpoint_config, task.writes, task.id) # apply to checkpoint and save assert not apply_writes( - checkpoint, channels, [task], self.checkpointer.get_next_version + checkpoint, channels, [task], checkpointer.get_next_version ), "Can't write to SharedValues from update_state" checkpoint = create_checkpoint(checkpoint, channels, step + 1) - return self.checkpointer.put( + next_config = checkpointer.put( checkpoint_config, checkpoint, { @@ -879,6 +856,7 @@ class Pregel( checkpoint_previous_versions, checkpoint["channel_versions"] ), ) + return patch_checkpoint_map(next_config, saved.metadata if saved else None) async def aupdate_state( self, @@ -886,12 +864,36 @@ class Pregel( values: dict[str, Any] | Any, as_node: Optional[str] = None, ) -> RunnableConfig: - if not self.checkpointer: + checkpointer: Optional[BaseCheckpointSaver] = config["configurable"].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: raise ValueError("No checkpointer set") + # delegate to subgraph + if ( + checkpoint_ns := config["configurable"].get("checkpoint_ns", "") + ) and CONFIG_KEY_CHECKPOINTER not in config["configurable"]: + # remove task_ids from checkpoint_ns + recast_checkpoint_ns = NS_SEP.join( + part.split(NS_END)[0] for part in checkpoint_ns.split(NS_SEP) + ) + # find the subgraph with the matching name + async for name, pregel in self.aget_subgraphs(recurse=True): + if name == recast_checkpoint_ns: + return await pregel.aupdate_state( + patch_configurable( + config, {CONFIG_KEY_CHECKPOINTER: checkpointer} + ), + values, + as_node, + ) + else: + raise ValueError(f"Subgraph {recast_checkpoint_ns} not found") + # get last checkpoint config = merge_configs(self.config, config) if self.config else config - saved = await self.checkpointer.aget_tuple(config) + saved = await checkpointer.aget_tuple(config) checkpoint = copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() checkpoint_previous_versions = ( saved.checkpoint["channel_versions"].copy() if saved else {} @@ -915,7 +917,7 @@ class Pregel( } # find last node that updated the state, if not provided if values is None and as_node is None: - return await self.checkpointer.aput( + return await checkpointer.aput( checkpoint_config, create_checkpoint(checkpoint, None, step), { @@ -998,15 +1000,13 @@ class Pregel( ) # save task writes if saved: - await self.checkpointer.aput_writes( - checkpoint_config, task.writes, task.id - ) + await checkpointer.aput_writes(checkpoint_config, task.writes, task.id) # apply to checkpoint and save assert not apply_writes( - checkpoint, channels, [task], self.checkpointer.get_next_version + checkpoint, channels, [task], checkpointer.get_next_version ), "Can't write to SharedValues from update_state" checkpoint = create_checkpoint(checkpoint, channels, step + 1) - return await self.checkpointer.aput( + next_config = await checkpointer.aput( checkpoint_config, checkpoint, { @@ -1019,6 +1019,7 @@ class Pregel( checkpoint_previous_versions, checkpoint["channel_versions"] ), ) + return patch_checkpoint_map(next_config, saved.metadata if saved else None) def _defaults( self, diff --git a/libs/langgraph/langgraph/pregel/config.py b/libs/langgraph/langgraph/pregel/config.py index 45b2ab1a9..f4c64eb39 100644 --- a/libs/langgraph/langgraph/pregel/config.py +++ b/libs/langgraph/langgraph/pregel/config.py @@ -2,6 +2,9 @@ from typing import Any, Optional from langchain_core.runnables import RunnableConfig +from langgraph.checkpoint.base import CheckpointMetadata +from langgraph.constants import CONFIG_KEY_CHECKPOINT_MAP + def patch_configurable( config: Optional[RunnableConfig], patch: dict[str, Any] @@ -10,3 +13,22 @@ def patch_configurable( return {"configurable": patch} else: return {**config, "configurable": {**config["configurable"], **patch}} + + +def patch_checkpoint_map( + config: RunnableConfig, metadata: Optional[CheckpointMetadata] +) -> RunnableConfig: + if parents := (metadata.get("parents") if metadata else None): + return patch_configurable( + config, + { + CONFIG_KEY_CHECKPOINT_MAP: { + **parents, + config["configurable"]["checkpoint_ns"]: config["configurable"][ + "checkpoint_id" + ], + }, + }, + ) + else: + return config diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index d4801ffc3..4a69ef917 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -9786,8 +9786,16 @@ def test_doubly_nested_graph_state( ) ] + # fork and replay + fork = app.update_state(grandchild_history[2].config, None) + assert [c for c in app.stream(None, fork, subgraphs=True)] == [ + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_1": {"my_key": "hi my value here"}}, + ) + ] + -@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_send_to_nested_graphs( request: pytest.FixtureRequest, checkpointer_name: str @@ -9835,24 +9843,30 @@ def test_send_to_nested_graphs( "subjects": ["cats", "dogs"], "jokes": [], } - actual_snapshot = graph.get_state(config) - subgraph_nodes = list(actual_snapshot.subgraphs.keys()) - assert len(subgraph_nodes) == 2 - for subgraph_node in subgraph_nodes: - assert subgraph_node.split(":")[0] == "generate_joke" - - subgraphs = { - subgraph_node: graph.get_state( - {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} - ) - for subgraph_node in subgraph_nodes - } - - expected_snapshot = StateSnapshot( + outer_state = graph.get_state(config) + assert outer_state == StateSnapshot( values={"subjects": ["cats", "dogs"], "jokes": []}, tasks=( - PregelTask(AnyStr(), "generate_joke"), - PregelTask(AnyStr(), "generate_joke"), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), ), next=("generate_joke", "generate_joke"), config={ @@ -9871,21 +9885,86 @@ def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraphs=subgraphs, ) - assert actual_snapshot == expected_snapshot + # check state of each of the inner tasks + assert graph.get_state(outer_state.tasks[0].state) == StateSnapshot( + values={"subject": "cats - hohoho", "jokes": []}, + next=("generate",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("generate_joke:"): AnyStr(), + } + ), + } + }, + metadata={ + "step": 1, + "source": "loop", + "writes": {"edit": None}, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(""), name="generate"),), + ) + assert graph.get_state(outer_state.tasks[1].state) == StateSnapshot( + values={"subject": "dogs - hohoho", "jokes": []}, + next=("generate",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("generate_joke:"): AnyStr(), + } + ), + } + }, + metadata={ + "step": 1, + "source": "loop", + "writes": {"edit": None}, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(""), name="generate"),), + ) + # update state of dogs joke graph + graph.update_state(outer_state.tasks[1].state, {"subject": "turtles - hohoho"}) # continue past interrupt assert graph.invoke(None, config=config) == { "subjects": ["cats", "dogs"], - "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], } actual_snapshot = graph.get_state(config) expected_snapshot = StateSnapshot( values={ "subjects": ["cats", "dogs"], - "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], }, tasks=(), next=(), @@ -9902,7 +9981,7 @@ def test_send_to_nested_graphs( "writes": { "generate_joke": [ {"jokes": ["Joke about cats - hohoho"]}, - {"jokes": ["Joke about dogs - hohoho"]}, + {"jokes": ["Joke about turtles - hohoho"]}, ] }, "step": 1, @@ -9922,17 +10001,11 @@ def test_send_to_nested_graphs( actual_history = list(graph.get_state_history(config)) # get subgraph node state for expected history - subgraphs = { - subgraph_node: graph.get_state( - {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} - ) - for subgraph_node in subgraph_nodes - } expected_history = [ StateSnapshot( values={ "subjects": ["cats", "dogs"], - "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], }, tasks=(), next=(), @@ -9949,7 +10022,7 @@ def test_send_to_nested_graphs( "writes": { "generate_joke": [ {"jokes": ["Joke about cats - hohoho"]}, - {"jokes": ["Joke about dogs - hohoho"]}, + {"jokes": ["Joke about turtles - hohoho"]}, ] }, "step": 1, @@ -9966,8 +10039,26 @@ def test_send_to_nested_graphs( StateSnapshot( values={"subjects": ["cats", "dogs"], "jokes": []}, tasks=( - PregelTask(AnyStr(), "generate_joke"), - PregelTask(AnyStr(), "generate_joke"), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), ), next=("generate_joke", "generate_joke"), config={ @@ -9986,7 +10077,6 @@ def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraphs=subgraphs, ), StateSnapshot( values={"jokes": []}, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index f0c48efdc..c415c2cb3 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -8302,8 +8302,16 @@ async def test_doubly_nested_graph_state( ) ] + # fork and replay + fork = app.update_state(grandchild_history[2].config, None) + assert [c for c in app.stream(None, fork, subgraphs=True)] == [ + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_1": {"my_key": "hi my value here"}}, + ) + ] + -@pytest.mark.skip("TODO") @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_send_to_nested_graphs( request: pytest.FixtureRequest, checkpointer_name: str @@ -8351,23 +8359,32 @@ async def test_send_to_nested_graphs( "subjects": ["cats", "dogs"], "jokes": [], } - actual_snapshot = await graph.aget_state(config) - subgraph_nodes = list(actual_snapshot.subgraphs.keys()) - assert len(subgraph_nodes) == 2 - for subgraph_node in subgraph_nodes: - assert subgraph_node.split(":")[0] == "generate_joke" + # check state + outer_state = await graph.aget_state(config) - subgraphs = { - subgraph_node: await graph.aget_state( - {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} - ) - for subgraph_node in subgraph_nodes - } - expected_snapshot = StateSnapshot( + assert outer_state == StateSnapshot( values={"subjects": ["cats", "dogs"], "jokes": []}, tasks=( - PregelTask(AnyStr(), "generate_joke"), - PregelTask(AnyStr(), "generate_joke"), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), ), next=("generate_joke", "generate_joke"), config={ @@ -8386,21 +8403,24 @@ async def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraphs=subgraphs, ) - assert actual_snapshot == expected_snapshot + + # update state of dogs joke graph + await graph.aupdate_state( + outer_state.tasks[1].state, {"subject": "turtles - hohoho"} + ) # continue past interrupt assert await graph.ainvoke(None, config=config) == { "subjects": ["cats", "dogs"], - "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], } actual_snapshot = await graph.aget_state(config) expected_snapshot = StateSnapshot( values={ "subjects": ["cats", "dogs"], - "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], }, tasks=(), next=(), @@ -8417,7 +8437,7 @@ async def test_send_to_nested_graphs( "writes": { "generate_joke": [ {"jokes": ["Joke about cats - hohoho"]}, - {"jokes": ["Joke about dogs - hohoho"]}, + {"jokes": ["Joke about turtles - hohoho"]}, ] }, "step": 1, @@ -8435,18 +8455,11 @@ async def test_send_to_nested_graphs( # test full history actual_history = [c async for c in graph.aget_state_history(config)] - # get subgraph node state for expected history - subgraphs = { - subgraph_node: await graph.aget_state( - {"configurable": {"thread_id": "1", "checkpoint_ns": subgraph_node}} - ) - for subgraph_node in subgraph_nodes - } expected_history = [ StateSnapshot( values={ "subjects": ["cats", "dogs"], - "jokes": ["Joke about cats - hohoho", "Joke about dogs - hohoho"], + "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], }, tasks=(), next=(), @@ -8463,7 +8476,7 @@ async def test_send_to_nested_graphs( "writes": { "generate_joke": [ {"jokes": ["Joke about cats - hohoho"]}, - {"jokes": ["Joke about dogs - hohoho"]}, + {"jokes": ["Joke about turtles - hohoho"]}, ] }, "step": 1, @@ -8481,8 +8494,26 @@ async def test_send_to_nested_graphs( values={"subjects": ["cats", "dogs"], "jokes": []}, next=("generate_joke", "generate_joke"), tasks=( - PregelTask(AnyStr(), "generate_joke"), - PregelTask(AnyStr(), "generate_joke"), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), ), config={ "configurable": { @@ -8500,7 +8531,6 @@ async def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - subgraphs=subgraphs, ), StateSnapshot( values={"jokes": []}, From 44153e4a49329d14629361a2c338828a2b8063e3 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 16:52:50 -0700 Subject: [PATCH 194/287] Update test --- libs/langgraph/langgraph/pregel/__init__.py | 3 ++- libs/langgraph/tests/test_pregel_async.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 173304ed2..8419eb913 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -917,7 +917,7 @@ class Pregel( } # find last node that updated the state, if not provided if values is None and as_node is None: - return await checkpointer.aput( + next_config = await checkpointer.aput( checkpoint_config, create_checkpoint(checkpoint, None, step), { @@ -928,6 +928,7 @@ class Pregel( }, {}, ) + return patch_checkpoint_map(next_config, saved.metadata if saved else None) elif as_node is None and not saved: if ( isinstance(self.input_channels, str) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index c415c2cb3..1403ee90c 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -8303,8 +8303,8 @@ async def test_doubly_nested_graph_state( ] # fork and replay - fork = app.update_state(grandchild_history[2].config, None) - assert [c for c in app.stream(None, fork, subgraphs=True)] == [ + fork = await app.aupdate_state(grandchild_history[2].config, None) + assert [c async for c in app.astream(None, fork, subgraphs=True)] == [ ( (AnyStr("child:"), AnyStr("child_1:")), {"grandchild_1": {"my_key": "hi my value here"}}, From 30598cd9139cbe9e74890cd2f08657c5697703de Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Thu, 29 Aug 2024 21:50:14 -0400 Subject: [PATCH 195/287] docs: fix images in structured react agent (#1544) --- examples/react-agent-structured-output.ipynb | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/react-agent-structured-output.ipynb b/examples/react-agent-structured-output.ipynb index 6bad5ad75..31b7847f6 100644 --- a/examples/react-agent-structured-output.ipynb +++ b/examples/react-agent-structured-output.ipynb @@ -1,6 +1,17 @@ { "cells": [ { + "attachments": { + "59e8ed35-f2b4-421e-8d21-880e7ab31e5f.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAg0AAAF+CAYAAAABRilmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAUGVYSWZNTQAqAAAACAACARIAAwAAAAEAAQAAh2kABAAAAAEAAAAmAAAAAAADoAEAAwAAAAEAAQAAoAIABAAAAAEAAAINoAMABAAAAAEAAAF+AAAAAMvtFIkAAAIyaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zODI8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+NTI1PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT4xPC9leGlmOkNvbG9yU3BhY2U+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpiGWaJAABAAElEQVR4Aey9B5wmRbX+fybPbIJNsOScYckgOYMgScmCyNULGK9ZuIb74Sr+7l9RrwrXBAaMiAQlKyJBQKJIRsICS4Zl2Tx55v98T3e90/PuO2l3lt2ZPbXbUx2qKzz9dp+nzjlVVdWtYBECgUAgEAgEAoFAIBAYAIHqAa7H5UAgEAgEAoFAIBAIBByBIA3xQwgEAoFAIBAIBAKBQSEQpGFQMEWiQCAQCAQCgUAgEAjSEL+BQCAQCAQCgUAgEBgUAkEaBgVTJAoEAoFAIBAIBAKBIA3xGwgEAoFAIBAIBAKBQSEQpGFQMEWiQCAQCAQCgUAgEAjSEL+BQCAQCAQCgUAgEBgUAkEaBgVTJAoEAoFAYOVFoK2tzTo6OnoBwLyA7e3t1tnZ2et8HAwOgZGKXe3gmhepAoFAIBAIBObNm2cvvPCCLViwoARGVVWV1dTUWENDg02ZMsUmT55sdXV1peuD3UGIPPvss573hhtuaKuuuupgby2lmzt3rtXX11tjY6NRr+EIra2t9o9//MPWXntt3yAPr7zyir3xxhv25ptvenmrrLKKrbnmmt722tpMrEAmwIm6gM2yDC0tLU5qKCuVvyzLK8/7tddeszlz5tgaa6xhEyZMKL9cOgbLV1991XEDO+o7adIkx42YZzeUsKza3d+zC9IwlCcUaQOBQGClRuDBBx+0s88+25577jmbOnWqVVdnylo+9gjGPffc00466STbbLPNhkwcICNf+MIX7NZbb7Uf/ehHdsQRRwwpj/nz59s111xjW265pU2fPt2JzHA8rOeff94uuugiO+GEE5wYPPzww/bTn/7U7rrrLkPbgICBJL3rXe+yE0880TbeeGMX3DNnzrR77rnHdtxxRz83HHWplAd1uPfee52g7LTTTv5cKqVblueuvvpqu/766+3Tn/607bHHHhWLgkDdd999dskll9hDDz1kXV1d/vuBaG677bb2nve8x7baaqtBE4dl2e7+nl2QhoqPN04GAoFAINA3AmeccYYddthhThT4eNPzvuOOO+y3v/2tC4KPfOQj3uvsO4fFr/z973934Ttx4kTv2e+yyy7es188ZeUz//rXv1y4f+hDH3LSUDnV0M8+9thjrkFZbbXVbPbs2fad73zH6FmfeeaZhpBubm62O++80y6++GIXhB/+8Ie97X/605/spptusnXXXXeZkwZIDLghdFfEAEGADH772992zcJnPvMZJwporiA8v/nNb2zWrFnG72aLLbYYlLaE392yand/zy5Iw4r4C4s6BQKBwAqNAIJwm222cfUyFaWnSA8TbcEjjzziqntU1XzYEQao8lFNo43gPOr8pKXgfgTv3Xff7QKWvJ944gk3Vay11lq9zAzkQX4Ib8wP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" + }, + "e9ef3df1-dbc0-4ff0-8040-0280372d67ac.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAosAAAFFCAYAAACE4KdOAABAAElEQVR4AexdB5wV1fU+7C69d6R3RAEBCxZUEFGwRUURNXaMNUaNf7sxRo0aiSWx914iib0idkSwUETp0ntnFxa2/u93d89jdpjXy8577zv83puZO7d+d9j53jn3nFuj3IhQiAARIAJEgAgQASJABIiABwI5HmlMIgJEgAgQASJABIgAESACFgGSRT4IRIAIEAEiQASIABEgAkERIFkMCg1vEAEiQASIABEgAkSACJAs8hkgAkSACBABIkAEiAARCIoAyWJQaHiDCBABIkAEiAARIAJEgGSRzwARIAJEgAgQASJABIhAUARIFoNCwxtEgAgQASJABIgAESACJIt8BogAESACRIAIEAEiQASCIkCyGBQa3iACRIAIEAEiQASIABEgWeQzQASIABEgAkSACBABIhAUAZLFoNDwBhEgAkSACBABIkAEiADJIp8BIkAEiAARIAJEgAgQgaAIkCwGhYY3iAARIAJEgAgQASJABEgW+QwQASJABIgAESACRIAIBEWAZDEoNLxBBIgAESACRIAIEAEiQLLIZ4AIEAEiQASIABEgAkQgKAIki0Gh4Q0iQASIABEgAkSACBABkkU+A0SACBABIkAEiAARIAJBESBZDAoNbxABIkAEiAARIAJEgAiQLPIZIAJEgAgQASJABIgAEQiKAMliUGh4gwgQASJABIgAESACRIBkkc8AESACRIAIEAEiQASIQFAESBaDQsMbRIAIEAEiQASIABEgAiSLfAaIABEgAkSACBABIkAEgiJAshgUGt4gAkSACBABIkAEiAARIFnkM0AEiAARIAJEgAgQASIQFAGSxaDQ8AYRIAJEgAgQASJABIgAySKfASJABIhABiEwc+2vctknN1bbiNztu6+rrWNsmAgQgZgRIFmMGToWJAJEgAj4D4ElW1fIpBXfy46SndXSOXf77utq6RQbJQJEIC4ESBbjgo+FiQARIAL+QqC0vNR2qLBkR7V0zN2++7paOsVGiQARiAsBksW44GNhIkAEiIC/ECgqLbIdysvJjblj24sLYy7rbt99HXPFLEgEiEC1IZBXbS2zYSJABIhAFiGwccdmmbZmlhSVFgvOt+7cKtsMKSstK5VezbvJiT1GJAQN1A+pm1cn4vp2GoL52LQX5NMlX0tx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" + }, + "f717c664-605d-48d7-b534-deec99087214.png": { + "image/png": "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" + } + }, "cell_type": "markdown", "metadata": {}, "source": [ @@ -10,11 +21,11 @@ "\n", "This notebook will walk through two different options for forcing a function calling agent to structure its output. We will be using a basic [ReAct agent](https://langchain-ai.github.io/langgraph/how-tos/create-react-agent/) (a model node and a tool-calling node) together with a third node at the end that will format response for the user. Both of the options will use the same graph structure as shown in the diagram below, but will have different mechanisms under the hood.\n", "\n", - "![React Diagrams](./react_diagrams.png)\n", + "![react_diagrams.png](attachment:59e8ed35-f2b4-421e-8d21-880e7ab31e5f.png)\n", "\n", "**Option 1**\n", "\n", - "![option1](./option1.png)\n", + "![option1.png](attachment:f717c664-605d-48d7-b534-deec99087214.png)\n", "\n", "The first way you can force your tool calling agent to have structured output is to bind the output you would like as an additional tool for the `agent` node to use. In contrast to the basic ReAct agent, the `agent` node in this case is not selecting between `tools` and `END` but rather selecting between the specific tools it calls. The expected flow in this case is that the LLM in the `agent` node will first select the action tool, and after receiving the action tool output it will call the response tool, which will then route to the `respond` node which simply structures the arguments from the `agent` node tool call.\n", "\n", @@ -24,7 +35,7 @@ "\n", "**Option 2**\n", "\n", - "![option1](./option2.png)\n", + "![option2.png](attachment:e9ef3df1-dbc0-4ff0-8040-0280372d67ac.png)\n", "\n", "The second way you can force your tool calling agent to have structured output is to use a second LLM (in this case `model_with_structured_output`) to respond to the user. \n", "\n", @@ -348,7 +359,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, From 65b64c13312fdc69dd2b8df4b50673171892a528 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Fri, 30 Aug 2024 09:35:06 -0700 Subject: [PATCH 196/287] Support optional types from typeddict In the input schema --- libs/langgraph/langgraph/graph/state.py | 24 +++++----- libs/langgraph/langgraph/utils.py | 29 ++++++++++-- libs/langgraph/tests/test_state.py | 24 +++++++++- libs/langgraph/tests/test_utils.py | 63 ++++++++++++++++++++++++- 4 files changed, 121 insertions(+), 19 deletions(-) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 1416d14d0..10e8ca5ff 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -20,9 +20,7 @@ from typing import ( from langchain_core.pydantic_v1 import BaseModel from langchain_core.runnables import Runnable, RunnableConfig from langchain_core.runnables.base import RunnableLike -from langchain_core.runnables.utils import ( - create_model, -) +from langchain_core.runnables.utils import create_model from langgraph.channels.base import BaseChannel from langgraph.channels.binop import BinaryOperatorAggregate @@ -33,14 +31,7 @@ from langgraph.channels.named_barrier_value import NamedBarrierValue from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.constants import NS_END, NS_SEP, TAG_HIDDEN from langgraph.errors import InvalidUpdateError -from langgraph.graph.graph import ( - END, - START, - Branch, - CompiledGraph, - Graph, - Send, -) +from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph, Send from langgraph.managed.base import ( ChannelKeyPlaceholder, ChannelTypePlaceholder, @@ -53,7 +44,7 @@ from langgraph.pregel.read import ChannelRead, PregelNode from langgraph.pregel.types import All, RetryPolicy from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry from langgraph.store.base import BaseStore -from langgraph.utils import RunnableCallable, coerce_to_runnable +from langgraph.utils import RunnableCallable, coerce_to_runnable, is_optional_type logger = logging.getLogger(__name__) @@ -498,7 +489,14 @@ class CompiledStateGraph(CompiledGraph): return create_model( # type: ignore[call-overload] self.get_name("Input"), **{ - k: (self.channels[k].UpdateType, None) + k: ( + self.channels[k].UpdateType, + ( + None + if is_optional_type(self.channels[k].UpdateType) + else ... + ), + ) for k in self.builder.schemas[self.builder.input] if isinstance(self.channels[k], BaseChannel) }, diff --git a/libs/langgraph/langgraph/utils.py b/libs/langgraph/langgraph/utils.py index 64cb21128..9aa1c257c 100644 --- a/libs/langgraph/langgraph/utils.py +++ b/libs/langgraph/langgraph/utils.py @@ -4,7 +4,15 @@ import inspect import sys from contextvars import copy_context from functools import partial, wraps -from typing import Any, AsyncIterator, Awaitable, Callable, Optional +from typing import ( + Any, + AsyncIterator, + Awaitable, + Callable, + Optional, + Union, + get_origin, +) from langchain_core.runnables.base import ( Runnable, @@ -34,8 +42,6 @@ except ImportError: class StrEnum(str, enum.Enum): """A string enum.""" - pass - class RunnableCallable(Runnable): """A much simpler version of RunnableLambda that requires sync and async functions.""" @@ -183,3 +189,20 @@ def coerce_to_runnable(thing: RunnableLike, *, name: str, trace: bool) -> Runnab f"Expected a Runnable, callable or dict." f"Instead got an unsupported type: {type(thing)}" ) + + +def is_optional_type(type_: Any) -> bool: + """Check if a type is Optional.""" + + if hasattr(type_, "__origin__") and hasattr(type_, "__args__"): + origin = get_origin(type_) + if origin is Optional: + return True + if origin is Union: + return any( + arg is type(None) or is_optional_type(arg) for arg in type_.__args__ + ) + return origin is None + if hasattr(type_, "__bound__") and type_.__bound__ is not None: + return is_optional_type(type_.__bound__) + return type_ is None diff --git a/libs/langgraph/tests/test_state.py b/libs/langgraph/tests/test_state.py index 4088e38c9..884895162 100644 --- a/libs/langgraph/tests/test_state.py +++ b/libs/langgraph/tests/test_state.py @@ -1,5 +1,5 @@ from typing import Annotated as Annotated2 -from typing import Any +from typing import Any, Optional import pytest from langchain_core.runnables import RunnableConfig @@ -86,3 +86,25 @@ def test_state_schema_with_type_hint(): for i, c in enumerate(graph.stream(input_state, stream_mode="updates")): node_name = actions[i].__name__ assert c[node_name] == output_state + + +def test_state_schema_optional_values(): + class InputState(TypedDict): + val1: str + val2: Optional[str] + + class State(InputState): + val4: dict + + builder = StateGraph(State, input=InputState) + builder.add_node("n", lambda x: x) + builder.add_edge("__start__", "n") + graph = builder.compile() + model = graph.input_schema + json_schema = model.schema() + expected_required = {"val1"} + expected_optional = {"val2"} + assert set(json_schema["required"]) == expected_required + assert ( + set(json_schema["properties"].keys()) == expected_required | expected_optional + ) diff --git a/libs/langgraph/tests/test_utils.py b/libs/langgraph/tests/test_utils.py index 1e3bf8ea1..d5bfb53c7 100644 --- a/libs/langgraph/tests/test_utils.py +++ b/libs/langgraph/tests/test_utils.py @@ -1,7 +1,18 @@ import functools import sys import uuid -from typing import TypedDict +from typing import ( + Any, + Callable, + Dict, + ForwardRef, + List, + Literal, + Optional, + TypedDict, + TypeVar, + Union, +) from unittest.mock import patch import langsmith @@ -9,7 +20,7 @@ import pytest from langgraph.graph import END, StateGraph from langgraph.graph.graph import CompiledGraph -from langgraph.utils import is_async_callable, is_async_generator +from langgraph.utils import is_async_callable, is_async_generator, is_optional_type def test_is_async() -> None: @@ -119,3 +130,51 @@ async def test_runnable_callable_tracing_nested_async(rt_graph: CompiledGraph) - with langsmith.tracing_context(enabled=True): res = await rt_graph.ainvoke({"foo": 1}) assert isinstance(res["node_run_id"], uuid.UUID) + + +def test_is_optional_type(): + assert is_optional_type(None) + assert not is_optional_type(type(None)) + assert is_optional_type(Optional[list]) + assert not is_optional_type(int) + assert is_optional_type(Optional[Literal[1, 2, 3]]) + assert not is_optional_type(Literal[1, 2, 3]) + assert is_optional_type(Optional[List[int]]) + assert is_optional_type(Optional[Dict[str, int]]) + assert not is_optional_type(List[Optional[int]]) + assert is_optional_type(Union[Optional[str], Optional[int]]) + assert is_optional_type( + Union[ + Union[Optional[str], Optional[int]], Union[Optional[float], Optional[dict]] + ] + ) + assert not is_optional_type(Union[Union[str, int], Union[float, dict]]) + + assert is_optional_type(Union[int, None]) + assert is_optional_type(Union[str, None, int]) + assert is_optional_type(Union[None, str, int]) + assert not is_optional_type(Union[int, str]) + + assert not is_optional_type(Any) # Do we actually want this? + assert is_optional_type(Optional[Any]) + + class MyClass: + pass + + assert is_optional_type(Optional[MyClass]) + assert not is_optional_type(MyClass) + assert is_optional_type(Optional[ForwardRef("MyClass")]) + assert not is_optional_type(ForwardRef("MyClass")) + + assert is_optional_type(Optional[Union[List[int], Dict[str, Optional[int]]]]) + assert not is_optional_type(Union[List[int], Dict[str, Optional[int]]]) + + assert is_optional_type(Optional[Callable[[int], str]]) + assert not is_optional_type(Callable[[int], Optional[str]]) + + T = TypeVar("T") + assert is_optional_type(Optional[T]) + assert not is_optional_type(T) + + U = TypeVar("U", bound=Optional[T]) + assert is_optional_type(U) From fcd0f094f9cebf0f737ca166b8e2161bbed157e4 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 09:57:41 -0700 Subject: [PATCH 197/287] Add test from harrison, always produce output when resuming graph --- libs/langgraph/Makefile | 8 +- libs/langgraph/langgraph/pregel/io.py | 10 +- libs/langgraph/langgraph/pregel/loop.py | 5 + .../tests/__snapshots__/test_pregel.ambr | 125 +++++ libs/langgraph/tests/test_pregel.py | 481 ++++++++++++++++-- libs/langgraph/tests/test_pregel_async.py | 75 ++- 6 files changed, 651 insertions(+), 53 deletions(-) diff --git a/libs/langgraph/Makefile b/libs/langgraph/Makefile index 1500c84ef..ccb8ef59a 100644 --- a/libs/langgraph/Makefile +++ b/libs/langgraph/Makefile @@ -18,20 +18,18 @@ start-postgres: docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait stop-postgres: - docker compose -f tests/compose-postgres.yml down + docker compose -f tests/compose-postgres.yml down -v TEST_PATH ?= . test: - make start-postgres; \ - poetry run pytest $(TEST_PATH); \ + make start-postgres && poetry run pytest $(TEST_PATH); \ EXIT_CODE=$$?; \ make stop-postgres; \ exit $$EXIT_CODE test_watch: - make start-postgres; \ - poetry run ptw . -- --ff -v -x -n auto --dist worksteal --snapshot-update --tb short $(TEST_PATH); \ + make start-postgres && poetry run ptw . -- --ff -v -x -n auto --dist worksteal --snapshot-update --tb short $(TEST_PATH); \ EXIT_CODE=$$?; \ make stop-postgres; \ exit $$EXIT_CODE diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/io.py index acc6574e8..a6e419592 100644 --- a/libs/langgraph/langgraph/pregel/io.py +++ b/libs/langgraph/langgraph/pregel/io.py @@ -73,15 +73,19 @@ class AddableValuesDict(AddableDict): def map_output_values( output_channels: Union[str, Sequence[str]], - pending_writes: Sequence[tuple[str, Any]], + pending_writes: Union[True, Sequence[tuple[str, Any]]], channels: Mapping[str, BaseChannel], ) -> Iterator[Union[dict[str, Any], Any]]: """Map pending writes (a sequence of tuples (channel, value)) to output chunk.""" if isinstance(output_channels, str): - if any(chan == output_channels for chan, _ in pending_writes): + if pending_writes is True or any( + chan == output_channels for chan, _ in pending_writes + ): yield read_channel(channels, output_channels) else: - if {c for c, _ in pending_writes if c in output_channels}: + if pending_writes is True or { + c for c, _ in pending_writes if c in output_channels + }: yield AddableValuesDict(read_channels(channels, output_channels)) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 6245a7ab0..13fe4c58b 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -401,6 +401,11 @@ class PregelLoop: if k in self.checkpoint["channel_versions"]: version = self.checkpoint["channel_versions"][k] self.checkpoint["versions_seen"][INTERRUPT][k] = version + # produce values output + self.stream.extend( + (self.config["configurable"].get("checkpoint_ns", ""), "values", v) + for v in map_output_values(self.output_keys, True, self.channels) + ) # map inputs to channel updates elif input_writes := deque(map_input(input_keys, self.input)): # discard any unfinished tasks from previous checkpoint diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index ba58cda8c..14cf25277 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -5099,6 +5099,131 @@ # name: test_state_graph_w_config_inherited_state_keys.2 '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' # --- +# name: test_weather_subgraph[memory] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres_pipe] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres_pool] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[sqlite] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- # name: test_xray_issue ''' %%{init: {'flowchart': {'curve': 'linear'}}}%% diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 4a69ef917..0ee645dbe 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -20,6 +20,7 @@ from typing import ( Tuple, TypedDict, Union, + cast, get_type_hints, ) @@ -2293,6 +2294,16 @@ def test_conditional_graph( ) assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "input": "what is weather in sf", + } + }, { "tools": { "input": "what is weather in sf", @@ -2503,6 +2514,16 @@ def test_conditional_graph( ) assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "input": "what is weather in sf", + }, + }, { "tools": { "input": "what is weather in sf", @@ -2668,6 +2689,14 @@ def test_conditional_graph( ) assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + }, + }, { "tools": { "input": "what is weather in sf", @@ -2706,6 +2735,26 @@ def test_conditional_graph( ] assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ] + ], + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, { "tools": { "input": "what is weather in sf", @@ -8396,17 +8445,12 @@ 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")] == [ - { - "my_key": "", - }, + {"my_key": ""}, ] assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "got here and there and parallel", - }, - { - "my_key": "got here and there and parallel and back again", - }, + {"my_key": ""}, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, ] # test interrupts BEFORE the parallel node @@ -8416,14 +8460,13 @@ def test_nested_graph_interrupts_parallel( {"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")] == [ - { - "my_key": "got here and there and parallel", - }, - { - "my_key": "got here and there and parallel and back again", - }, + {"my_key": ""}, + ] + assert [*app.stream(None, config, stream_mode="values")] == [ + {"my_key": ""}, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, ] # test interrupts AFTER the parallel node @@ -8433,12 +8476,12 @@ def test_nested_graph_interrupts_parallel( {"my_key": ""} ] assert [*app.stream(None, config, stream_mode="values")] == [ + {"my_key": ""}, {"my_key": "got here and there and parallel"}, ] assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "got here and there and parallel and back again", - }, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, ] @@ -8520,20 +8563,13 @@ 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")] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, + {"my_key": "my value"}, + {"my_key": "hi my value"}, ] assert [*app.stream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - { - "my_key": "hi my value here and there and back again", - }, + {"my_key": "hi my value"}, + {"my_key": "hi my value here and there"}, + {"my_key": "hi my value here and there and back again"}, ] @@ -10102,6 +10138,389 @@ def test_send_to_nested_graphs( assert actual_history == expected_history +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) +def test_weather_subgraph( + request: pytest.FixtureRequest, checkpointer_name: str, snapshot: SnapshotAssertion +) -> None: + from langchain_core.language_models.fake_chat_models import ( + FakeMessagesListChatModel, + ) + from langchain_core.messages import AIMessage, HumanMessage, ToolCall + from langchain_core.tools import tool + + from langgraph.graph import MessagesState + + checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) + + # setup subgraph + + @tool + def get_weather(city: str): + """Get the weather for a specific city""" + return f"I'ts sunny in {city}!" + + weather_model = FakeMessagesListChatModel( + responses=[ + AIMessage( + content="", + tool_calls=[ + ToolCall( + id="tool_call123", + name="get_weather", + args={"city": "San Francisco"}, + ) + ], + ) + ] + ) + + class SubGraphState(MessagesState): + city: str + + def model_node(state: SubGraphState): + result = weather_model.invoke(state["messages"]) + return {"city": cast(AIMessage, result).tool_calls[0]["args"]["city"]} + + def weather_node(state: SubGraphState): + result = get_weather.invoke({"city": state["city"]}) + return {"messages": [{"role": "assistant", "content": result}]} + + subgraph = StateGraph(SubGraphState) + subgraph.add_node(model_node) + subgraph.add_node(weather_node) + subgraph.add_edge(START, "model_node") + subgraph.add_edge("model_node", "weather_node") + subgraph.add_edge("weather_node", END) + subgraph = subgraph.compile(interrupt_before=["weather_node"]) + + # setup main graph + + class RouterState(MessagesState): + route: Literal["weather", "other"] + + class Router(TypedDict): + route: Literal["weather", "other"] + + router_model = FakeMessagesListChatModel( + responses=[ + AIMessage( + content="", + tool_calls=[ + ToolCall( + id="tool_call123", + name="router", + args={"dest": "weather"}, + ) + ], + ) + ] + ) + + def router_node(state: RouterState): + system_message = "Classify the incoming query as either about weather or not." + messages = [{"role": "system", "content": system_message}] + state["messages"] + route = router_model.invoke(messages) + return {"route": cast(AIMessage, route).tool_calls[0]["args"]["dest"]} + + def normal_llm_node(state: RouterState): + return {"messages": [AIMessage("Hello!")]} + + def route_after_prediction(state: RouterState): + if state["route"] == "weather": + return "weather_graph" + else: + return "normal_llm_node" + + graph = StateGraph(RouterState) + graph.add_node(router_node) + graph.add_node(normal_llm_node) + graph.add_node("weather_graph", subgraph) + graph.add_edge(START, "router_node") + graph.add_conditional_edges("router_node", route_after_prediction) + graph.add_edge("normal_llm_node", END) + graph.add_edge("weather_graph", END) + graph = graph.compile(checkpointer=checkpointer) + + assert graph.get_graph(xray=1).draw_mermaid() == snapshot + + config = {"configurable": {"thread_id": "1"}} + inputs = {"messages": [{"role": "user", "content": "what's the weather in sf"}]} + + # run until interrupt + assert [ + c + for c in graph.stream( + inputs, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ((), {"router_node": {"route": "weather"}}), + ((AnyStr("weather_graph:"),), {"model_node": {"city": "San Francisco"}}), + ] + + # check current state + state = graph.get_state(config) + assert state == StateSnapshot( + values={ + "messages": [HumanMessage(content="what's the weather in sf", id=AnyStr())], + "route": "weather", + }, + next=("weather_graph",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("weather_graph:"), + } + }, + ), + ), + ) + + # update + graph.update_state(state.tasks[0].state, {"city": "la"}) + + # run after update + assert [ + c + for c in graph.stream( + None, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ( + (AnyStr("weather_graph:"),), + { + "weather_node": { + "messages": [{"role": "assistant", "content": "I'ts sunny in la!"}] + } + }, + ), + ( + (), + { + "weather_graph": { + "messages": [ + HumanMessage(content="what's the weather in sf", id=AnyStr()), + AIMessage(content="I'ts sunny in la!", id=AnyStr()), + ] + } + }, + ), + ] + + # try updating acting as weather node + config = {"configurable": {"thread_id": "14"}} + inputs = {"messages": [{"role": "user", "content": "what's the weather in sf"}]} + assert [ + c + for c in graph.stream( + inputs, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ((), {"router_node": {"route": "weather"}}), + ((AnyStr("weather_graph:"),), {"model_node": {"city": "San Francisco"}}), + ] + state = graph.get_state(config, subgraphs=True) + assert state == StateSnapshot( + values={ + "messages": [HumanMessage(content="what's the weather in sf", id=AnyStr())], + "route": "weather", + }, + next=("weather_graph",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state=StateSnapshot( + values={ + "messages": [ + HumanMessage( + content="what's the weather in sf", id=AnyStr() + ) + ], + "city": "San Francisco", + }, + next=("weather_node",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("weather_graph:"): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": {"model_node": {"city": "San Francisco"}}, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="weather_node"),), + ), + ), + ), + ) + graph.update_state( + state.tasks[0].state.config, + {"messages": [{"role": "assistant", "content": "rainy"}]}, + as_node="weather_node", + ) + state = graph.get_state(config, subgraphs=True) + assert state == StateSnapshot( + values={ + "messages": [HumanMessage(content="what's the weather in sf", id=AnyStr())], + "route": "weather", + }, + next=("weather_graph",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state=StateSnapshot( + values={ + "messages": [ + HumanMessage( + content="what's the weather in sf", id=AnyStr() + ), + AIMessage(content="rainy", id=AnyStr()), + ], + "city": "San Francisco", + }, + next=(), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("weather_graph:"): AnyStr(), + } + ), + } + }, + metadata={ + "source": "update", + "step": 2, + "writes": { + "weather_node": { + "messages": [{"role": "assistant", "content": "rainy"}] + } + }, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + ), + ), + ) + assert [ + c + for c in graph.stream( + None, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ( + (), + { + "weather_graph": { + "messages": [ + HumanMessage(content="what's the weather in sf", id=AnyStr()), + AIMessage(content="rainy", id=AnyStr()), + ] + } + }, + ), + ] + + def test_repeat_condition(snapshot: SnapshotAssertion) -> None: class AgentState(TypedDict): hello: str diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 1403ee90c..8083ddd77 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -347,7 +347,7 @@ async def test_node_not_cancelled_on_other_node_interrupted( assert not inner_task_cancelled assert awhiles == 1 - assert await graph.ainvoke(None, thread, debug=True) is None + assert await graph.ainvoke(None, thread, debug=True) == {"hello": "world"} assert not inner_task_cancelled assert awhiles == 1 @@ -2572,6 +2572,16 @@ async def test_conditional_graph( ) assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "input": "what is weather in sf", + }, + }, { "tools": { "input": "what is weather in sf", @@ -2797,6 +2807,16 @@ async def test_conditional_graph( ) assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "input": "what is weather in sf", + }, + }, { "tools": { "input": "what is weather in sf", @@ -2973,6 +2993,14 @@ async def test_conditional_graph( ) assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + }, + }, { "tools": { "input": "what is weather in sf", @@ -3011,6 +3039,26 @@ async def test_conditional_graph( ] assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ] + ], + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, { "tools": { "input": "what is weather in sf", @@ -6891,6 +6939,7 @@ async def test_nested_graph_interrupts_parallel( {"my_key": ""}, ] assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": ""}, {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, ] @@ -6904,8 +6953,11 @@ async def test_nested_graph_interrupts_parallel( {"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")] == [ + {"my_key": ""}, + ] + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": ""}, {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, ] @@ -6919,9 +6971,11 @@ async def test_nested_graph_interrupts_parallel( {"my_key": ""}, ] assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": ""}, {"my_key": "got here and there and parallel"}, ] assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, ] @@ -7007,20 +7061,13 @@ async def test_doubly_nested_graph_interrupts( c async for c in app.astream({"my_key": "my value"}, config, stream_mode="values") ] == [ - { - "my_key": "my value", - }, - { - "my_key": "hi my value", - }, + {"my_key": "my value"}, + {"my_key": "hi my value"}, ] assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - { - "my_key": "hi my value here and there", - }, - { - "my_key": "hi my value here and there and back again", - }, + {"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 9fd64cd37b7e4d1e1d1802f3f70d05e9f9d92b97 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 10:19:20 -0700 Subject: [PATCH 198/287] Lint --- libs/langgraph/langgraph/pregel/io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/io.py index a6e419592..8568e8781 100644 --- a/libs/langgraph/langgraph/pregel/io.py +++ b/libs/langgraph/langgraph/pregel/io.py @@ -1,4 +1,4 @@ -from typing import Any, Iterator, Mapping, Optional, Sequence, TypeVar, Union +from typing import Any, Iterator, Literal, Mapping, Optional, Sequence, TypeVar, Union from langchain_core.runnables.utils import AddableDict @@ -73,7 +73,7 @@ class AddableValuesDict(AddableDict): def map_output_values( output_channels: Union[str, Sequence[str]], - pending_writes: Union[True, Sequence[tuple[str, Any]]], + pending_writes: Union[Literal[True], Sequence[tuple[str, Any]]], channels: Mapping[str, BaseChannel], ) -> Iterator[Union[dict[str, Any], Any]]: """Map pending writes (a sequence of tuples (channel, value)) to output chunk.""" From a07ec9d8c800be0283fa6332248751fa92ce1db5 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:21:57 -0700 Subject: [PATCH 199/287] Handle total=false --- libs/langgraph/langgraph/graph/state.py | 7 ++++++- libs/langgraph/tests/test_state.py | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 10e8ca5ff..31fdfb4e3 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -486,6 +486,10 @@ class CompiledStateGraph(CompiledGraph): __root__=(self.channels[keys[0]].UpdateType, None), ) else: + is_total_false = ( + hasattr(self.builder.input, "__total__") + and self.builder.input.__total__ is False + ) return create_model( # type: ignore[call-overload] self.get_name("Input"), **{ @@ -493,7 +497,8 @@ class CompiledStateGraph(CompiledGraph): self.channels[k].UpdateType, ( None - if is_optional_type(self.channels[k].UpdateType) + if is_total_false + or is_optional_type(self.channels[k].UpdateType) else ... ), ) diff --git a/libs/langgraph/tests/test_state.py b/libs/langgraph/tests/test_state.py index 884895162..a3a0d2806 100644 --- a/libs/langgraph/tests/test_state.py +++ b/libs/langgraph/tests/test_state.py @@ -88,8 +88,9 @@ def test_state_schema_with_type_hint(): assert c[node_name] == output_state -def test_state_schema_optional_values(): - class InputState(TypedDict): +@pytest.mark.parametrize("total_", [True, False]) +def test_state_schema_optional_values(total_: bool): + class InputState(TypedDict, total=total_): # type: ignore val1: str val2: Optional[str] @@ -102,9 +103,16 @@ def test_state_schema_optional_values(): graph = builder.compile() model = graph.input_schema json_schema = model.schema() - expected_required = {"val1"} - expected_optional = {"val2"} - assert set(json_schema["required"]) == expected_required + + if total_ is False: + expected_required = set() + expected_optional = {"val2", "val1"} + else: + expected_required = {"val1"} + + expected_optional = {"val2"} + + assert set(json_schema.get("required", set())) == expected_required assert ( set(json_schema["properties"].keys()) == expected_required | expected_optional ) From 33edc8e80885f8771b11207d4e2acc54f1b2493c Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 28 Aug 2024 12:49:02 -0700 Subject: [PATCH 200/287] sdk-py: Update signature for runs.join --- libs/sdk-py/langgraph_sdk/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index f252c829f..79ac0b3e8 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -1447,8 +1447,8 @@ class RunsClient: json=None, ) - async def join(self, thread_id: str, run_id: str) -> None: - """Block until a run is done. + async def join(self, thread_id: str, run_id: str) -> dict: + """Block until a run is done. Returns the final state of the thread. Args: thread_id: The thread ID to join. @@ -1459,7 +1459,7 @@ class RunsClient: Example Usage: - await client.runs.join( + result =await client.runs.join( thread_id="thread_id_to_join", run_id="run_id_to_join" ) From 6036b4a7d4c4b0b9ccf0e7ced9377b5e0b47ddae Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 10:11:32 -0700 Subject: [PATCH 201/287] Remove the need to fork a thread to replay a past state --- libs/langgraph/langgraph/pregel/algo.py | 12 ++--- libs/langgraph/langgraph/pregel/loop.py | 13 ++---- libs/langgraph/langgraph/pregel/types.py | 3 -- libs/langgraph/tests/test_pregel.py | 51 ++++----------------- libs/langgraph/tests/test_pregel_async.py | 55 ++++------------------- 5 files changed, 24 insertions(+), 110 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index e4e4b45eb..f3d82c835 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -50,7 +50,7 @@ from langgraph.pregel.io import read_channel, read_channels from langgraph.pregel.log import logger from langgraph.pregel.manager import ChannelsManager from langgraph.pregel.read import PregelNode -from langgraph.pregel.types import EXACT_MATCH, All, PregelExecutableTask, PregelTask +from langgraph.pregel.types import All, PregelExecutableTask, PregelTask class WritesProtocol(Protocol): @@ -357,10 +357,7 @@ def prepare_next_tasks( ), triggers, proc.retry_policy, - None - if task_checkpoint_ns - in configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}) - else EXACT_MATCH, + None, task_id, ) ) @@ -466,10 +463,7 @@ def prepare_next_tasks( ), triggers, proc.retry_policy, - None - if task_checkpoint_ns - in configurable.get(CONFIG_KEY_CHECKPOINT_MAP, {}) - else EXACT_MATCH, + None, task_id, ) ) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 13fe4c58b..d791a5539 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -122,6 +122,7 @@ class PregelLoop: output_keys: Union[str, Sequence[str]] stream_keys: Union[str, Sequence[str]] is_nested: bool + skip_done_tasks: bool checkpointer_get_next_version: Callable[[Optional[V]], V] checkpointer_put_writes: Optional[ @@ -180,6 +181,7 @@ class PregelLoop: self.output_keys = output_keys self.stream_keys = stream_keys self.is_nested = CONFIG_KEY_TASK_ID in self.config.get("configurable", {}) + self.skip_done_tasks = "checkpoint_id" not in config["configurable"] if CONFIG_KEY_STREAM in config["configurable"]: self.stream = DuplexStream( self.stream, config["configurable"][CONFIG_KEY_STREAM] @@ -340,18 +342,11 @@ class PregelLoop: return False # if there are pending writes from a previous loop, apply them - if self.checkpoint_pending_writes: + if self.skip_done_tasks and self.checkpoint_pending_writes: for tid, k, v in self.checkpoint_pending_writes: if k in (ERROR, INTERRUPT): continue - if task := next( - ( - t - for t in self.tasks - if t.id == tid and t.cache_policy is not None - ), - None, - ): + if task := next((t for t in self.tasks if t.id == tid), None): task.writes.append((k, v)) # print output for any tasks we applied previous writes to for task in self.tasks: diff --git a/libs/langgraph/langgraph/pregel/types.py b/libs/langgraph/langgraph/pregel/types.py index 8a5a37b37..6da463286 100644 --- a/libs/langgraph/langgraph/pregel/types.py +++ b/libs/langgraph/langgraph/pregel/types.py @@ -63,9 +63,6 @@ class CachePolicy(NamedTuple): pass -EXACT_MATCH = CachePolicy() - - class PregelTask(NamedTuple): id: str name: str diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 0ee645dbe..704824fe5 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -798,27 +798,13 @@ def test_invoke_two_processes_in_out_interrupt( ), ] - # re-running from any previous checkpoint w/out forking should do nothing + # re-running from any previous checkpoint should re-run nodes assert [c for c in app.stream(None, history[0].config, stream_mode="updates")] == [] assert [c for c in app.stream(None, history[1].config, stream_mode="updates")] == [ - {"two": {"output": 5}, "__metadata__": {"cached": True}}, + {"two": {"output": 5}}, ] assert [c for c in app.stream(None, history[2].config, stream_mode="updates")] == [ - {"one": {"inbox": 4}, "__metadata__": {"cached": True}}, - ] - - # forking and re-running from any prev checkpoint should re-run nodes - fork_config = app.update_state(history[0].config, None) - assert [c for c in app.stream(None, fork_config, stream_mode="updates")] == [] - - fork_config = app.update_state(history[1].config, None) - assert [c for c in app.stream(None, fork_config, stream_mode="updates")] == [ - {"two": {"output": 5}} - ] - - fork_config = app.update_state(history[2].config, None) - assert [c for c in app.stream(None, fork_config, stream_mode="updates")] == [ - {"one": {"inbox": 4}} + {"one": {"inbox": 4}}, ] @@ -987,25 +973,18 @@ def test_fork_always_re_runs_nodes( ), ] - # forking from any previous checkpoint w/out forking should do nothing + # forking from any previous checkpoint should re-run nodes assert [ c for c in graph.stream(None, history[0].config, stream_mode="updates") ] == [] assert [ c for c in graph.stream(None, history[1].config, stream_mode="updates") - ] == [{"add_one": 1, "__metadata__": {"cached": True}}] - - # forking and re-running from any prev checkpoint should re-run nodes - fork_config = graph.update_state(history[0].config, None) - assert [c for c in graph.stream(None, fork_config, stream_mode="updates")] == [] - - fork_config = graph.update_state(history[1].config, None) - assert [c for c in graph.stream(None, fork_config, stream_mode="updates")] == [ - {"add_one": 1} + ] == [ + {"add_one": 1}, ] - - fork_config = graph.update_state(history[2].config, None) - assert [c for c in graph.stream(None, fork_config, stream_mode="updates")] == [ + assert [ + c for c in graph.stream(None, history[2].config, stream_mode="updates") + ] == [ {"add_one": 1}, {"add_one": 1}, ] @@ -9813,18 +9792,6 @@ def test_doubly_nested_graph_state( assert [ c for c in app.stream(None, grandchild_history[2].config, subgraphs=True) ] == [ - ( - (AnyStr("child:"), AnyStr("child_1:")), - { - "grandchild_1": {"my_key": "hi my value here"}, - "__metadata__": {"cached": True}, - }, - ) - ] - - # fork and replay - fork = app.update_state(grandchild_history[2].config, None) - assert [c for c in app.stream(None, fork, subgraphs=True)] == [ ( (AnyStr("child:"), AnyStr("child_1:")), {"grandchild_1": {"my_key": "hi my value here"}}, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 8083ddd77..e5cda8e11 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1020,35 +1020,19 @@ async def test_invoke_two_processes_in_out_interrupt( ), ] - # forking from any previous checkpoint w/out forking should do nothing + # forking from any previous checkpoint should re-run nodes assert [ c async for c in app.astream(None, history[0].config, stream_mode="updates") ] == [] assert [ c async for c in app.astream(None, history[1].config, stream_mode="updates") ] == [ - {"two": {"output": 5}, "__metadata__": {"cached": True}}, + {"two": {"output": 5}}, ] assert [ c async for c in app.astream(None, history[2].config, stream_mode="updates") ] == [ - {"one": {"inbox": 4}, "__metadata__": {"cached": True}}, - ] - - # forking and re-running from any prev checkpoint should re-run nodes - fork_config = await app.aupdate_state(history[0].config, None) - assert [ - c async for c in app.astream(None, fork_config, stream_mode="updates") - ] == [] - - fork_config = await app.aupdate_state(history[1].config, None) - assert [c async for c in app.astream(None, fork_config, stream_mode="updates")] == [ - {"two": {"output": 5}} - ] - - fork_config = await app.aupdate_state(history[2].config, None) - assert [c async for c in app.astream(None, fork_config, stream_mode="updates")] == [ - {"one": {"inbox": 4}} + {"one": {"inbox": 4}}, ] @@ -1219,28 +1203,17 @@ async def test_fork_always_re_runs_nodes( ), ] - # forking from any previous checkpoint w/out forking should do nothing + # forking from any previous checkpoint should re-run nodes assert [ c async for c in graph.astream(None, history[0].config, stream_mode="updates") ] == [] assert [ c async for c in graph.astream(None, history[1].config, stream_mode="updates") - ] == [{"add_one": 1, "__metadata__": {"cached": True}}] - - # forking and re-running from any prev checkpoint should re-run nodes - fork_config = await graph.aupdate_state(history[0].config, None) + ] == [ + {"add_one": 1}, + ] assert [ - c async for c in graph.astream(None, fork_config, stream_mode="updates") - ] == [] - - fork_config = await graph.aupdate_state(history[1].config, None) - assert [ - c async for c in graph.astream(None, fork_config, stream_mode="updates") - ] == [{"add_one": 1}] - - fork_config = await graph.aupdate_state(history[2].config, None) - assert [ - c async for c in graph.astream(None, fork_config, stream_mode="updates") + c async for c in graph.astream(None, history[2].config, stream_mode="updates") ] == [ {"add_one": 1}, {"add_one": 1}, @@ -8340,18 +8313,6 @@ async def test_doubly_nested_graph_state( assert [ c async for c in app.astream(None, grandchild_history[2].config, subgraphs=True) ] == [ - ( - (AnyStr("child:"), AnyStr("child_1:")), - { - "grandchild_1": {"my_key": "hi my value here"}, - "__metadata__": {"cached": True}, - }, - ) - ] - - # fork and replay - fork = await app.aupdate_state(grandchild_history[2].config, None) - assert [c async for c in app.astream(None, fork, subgraphs=True)] == [ ( (AnyStr("child:"), AnyStr("child_1:")), {"grandchild_1": {"my_key": "hi my value here"}}, From c761b80970fef83fcc0f60f6ae26962f55445b0e Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Fri, 30 Aug 2024 14:41:29 -0400 Subject: [PATCH 202/287] docs: update subgraph how-to (#1520) --- examples/subgraph.ipynb | 1330 +++++++++++++++++++-------------------- 1 file changed, 648 insertions(+), 682 deletions(-) diff --git a/examples/subgraph.ipynb b/examples/subgraph.ipynb index ce8cfb3cd..e26b50f20 100644 --- a/examples/subgraph.ipynb +++ b/examples/subgraph.ipynb @@ -1,686 +1,652 @@ { - "cells": [ - { - "attachments": { - "71516aef-9c00-4730-a676-a54e90cb6472.png": { - "image/png": "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" - } - }, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# How to create subgraphs\n", - "\n", - "For more complex systems, sub-graphs are a useful design principle. Sub-graphs allow you to create and manage different states in different parts of your graph. This allows you build things like [multi-agent teams](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/), where each team can track its own separate state.\n", - "\n", - "![Screenshot 2024-07-11 at 1.01.28 PM.png](attachment:71516aef-9c00-4730-a676-a54e90cb6472.png)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph" - ] - }, - { - "attachments": { - "9145adc1-ce9d-4a22-8183-e13796d4a388.png": { - "image/png": "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gAACCCCAAAII1LMAQfp6BqZ6BBBAAIHGK6CBzaefftoO4Ne//nW5gVx99dU2QN+mTZty2/XD2rVr5bvvvhNNSRMeHi5Tp06VoKCgcsdt3brVBlg14K+B4/vuu8+mqXnhhRckICCg3LFVfcjJybGHxMfXfDFBDfLecsstkpJSuhiiVqQL4mrffVwWQHT6MGvWLHnggQckMzPTbho2bJhon9u2bescYuv685//bIP+ulHT7+gxNb2ZsWfPHvn0009l3759tv7zzjuvxkF1XVD3F7/4hXzxxRdl/dOUQDpGZ7Fd3aGGr776qlx66aXSvXv3smOdfXp8ZR76HXG2640ODfrr4r0Vi+tx+j45OfkUj4KCAjteTavUsmVLGTBggKhvxaL79Tulixc7Rcf0t7/9TZYtW2Zvrriul3DkyBEZO3as5Ofn2+/lmW6sOPXxigACVQuk52SImH+fYyIbdm2SyUMny5uzXpcFJlA/YcC4qjvqpUc4f/Z66fAYFgIIIIAAAgg0MQHS3TSxC85wEUAAAQSqL6ALsGogVNPdtGrVqtyJN9xwg00tUzEwrovLXnzxxfLEE0/Ic889J/fff79dxPXAgQPlztf9Wm9qaqrcfPPNsmXLFvn666/l448/LndcdT5oznwtsbE1T7fw3nvvlQXodUw6k37lypXyySefnHKzQJ8quO2222yA/sorr5S+ffva2em6gK1TNN3OFVdcYQP048aNk8mTJ9tUQepQk6Kz3s855xz561//Ks8//7w8++yzctNNN1nzmtSj43EC9PrkgS6w66QE6tq1a1lVOl5tZ/r06WXb9I0+NTB69GhxxqhPF+gNDb25oYFvHaMG+7VevYlw2WWX2fP1vCeffFLy8vJk7ty59smLP/zhD3afjunCCy+Uxx9/3H7WX1qfWukaB5pGSdv70Y9+ZK9D2UEn3/zjH/+w30udPa/fsUceecQ+raBPeujCxnpDoX379mWnqYHeVNGnIlxvppQdwBsEEKiVQGauCdKb0j70h3/falVRDU9q16adjOg/RpK3r5XktE01PNubDuevst50NRkLAggggAACTV2A/7Np6t8Axo8AAgggcFoBDbBqiYysXhoDDdBrwHTIkCE2aKsznnXBWU1L8tRTT5Vrx5ktf9VVV4neDNDjtKxatarccdX5oEH6083grur86667Tnr06GEPe/31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wASYABPQm0CJ+H80FVNTg4wn0Hud3JAJMAEmwASYABNgAkyACTABJsAEmIDBOenJsf7RRx+hd+/euPXWW2tcwdOnT2Pv3r2gCHvSrD948GCNNtUrzp49i65du1avNohje3t7LFy4EHPmzMGnn34KctRzYQJMgAkwASZg6AQUuRvOG2voV5LtZwJMgAkwASbABJgAE2ACTIAJMIH6CBick/6///6TUfTPPPMMjLQegSd9doqIP3PmTJU1BwUFSTkYqly+fDnc3Nzg7OwMikKn/uXl5fD396/Sx9AOHn74YZDcDSWQnTdvHszMzAxtCWwvE2ACTIAJMIEqBEpKy+Wxidb/66s04AMmwASYABNgAkyACTABJsAEmAATYALthIDBOekjIyMl+k6dOlW5BOSkJwe9nZ0dFixYgFGjRqFXr17iMXlTdO7cGba2tnjyySer9Kl+cPjwYezZswfZ2dno1q0bbr75ZlhZWVVvpvfx5cuXERsbix49esDGxqbWfiS7c/78eSlT4+HhUaVNaWkp9u/fj927d2PHjh04cOAAZsyYgffee69KO1rbY489JqVvdu3ahdGjR1c5zwdMgAkwASbABAyNQEmZKnGssamRoZnO9jIBJsAEmAATYAJMoFEELibmobNn7b6DRg3InZgAE2ACTMBgCBick56c8FSOHz8uk8cqpMmhTg72/v3713CskyOcoufrKr///nsN+ZzQ0FB89913GDBggOwaHx+PdevWITU1VY43YcIEUJvqJT8/X94Q+PLLLzWnfvnllxra96SpTzcO4uLiZLvrr78eP/30kxx78+bNmDZtmnxqQBnE3d0dhYWFymGV7fTp06WT/ueff2YnfRUyfKAQoCSMMSkF8HezgpmBaTzPXxOG5MzKz37fIAfMHh+sLE2z1bedpkMDdvKLyhCfXgA3ews42vDTKg1Ax02ZQKMIKIljzTiSvlH8uBMTYAJMgAkwASbQtgkkZRXh+KVMnLyUhRPidT42Wxo8oIszPp7Vr20bz9YxASbABJhAkxMwOCf94MGDJYTPPvsML730EpycnDRQhg8frtnX3qFEsRRRr6tQxDpFqFOZO3cuhg0bBorMX7ZsGQYOHIjo6GhcuHBBRucrY9DNglWrVmH9+vXo16/yf6BFRUW47bbbsGXLFhnVTw7+nTt3Soc73UhQpGjIgf/QQw/J4R54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AQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD6B/w+jDwOxnklQVQAAAABJRU5ErkJggg==" - } - }, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Simple example\n", - "\n", - "Let's consider a toy example: I have a system that accepts logs and perform two separate sub-tasks. First, it will summarize them. Second, it will summarize any failure modes captured in the logs. I want to perform these two operations in two different sub-graphs.\n", - "\n", - "The most important thing to recognize is the information transfer between the graphs. `Entry Graph` is the parent, and each of the two sub-graphs are defined as nodes in `Entry Graph`. Both subgraphs inherit state from the parent `Entry Graph`; I can access `docs` in each of the sub-graphs simply by specifying it in the sub-graph state (see diagram). Each subgraph can have its own private state. And any values that I want propagated back to the parent `Entry Graph` (for final reporting) simply need to be defined in my `Entry Graph` state (e.g., `summary report` and `failure report`).\n", - "\n", - "![Screenshot 2024-07-12 at 10.35.41 AM.png](attachment:9145adc1-ce9d-4a22-8183-e13796d4a388.png)" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from operator import add\n", - "from typing import List, TypedDict, Optional, Annotated, Dict\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "from langgraph.graph import StateGraph, START, END\n", - "\n", - "\n", - "# The structure of the logs\n", - "class Logs(TypedDict):\n", - " id: str\n", - " question: str\n", - " docs: Optional[List]\n", - " answer: str\n", - " grade: Optional[int]\n", - " grader: Optional[str]\n", - " feedback: Optional[str]\n", - "\n", - "\n", - "# Failure Analysis Sub-graph\n", - "class FailureAnalysisState(TypedDict):\n", - " docs: List[Logs]\n", - " failures: List[Logs]\n", - " fa_summary: str\n", - "\n", - "\n", - "def get_failures(state):\n", - " docs = state[\"docs\"]\n", - " failures = [doc for doc in docs if \"grade\" in doc]\n", - " return {\"failures\": failures}\n", - "\n", - "\n", - "def generate_summary(state):\n", - " failures = state[\"failures\"]\n", - " # Add fxn: fa_summary = summarize(failures)\n", - " fa_summary = \"Poor quality retrieval of Chroma documentation.\"\n", - " return {\"fa_summary\": fa_summary}\n", - "\n", - "\n", - "fa_builder = StateGraph(FailureAnalysisState)\n", - "fa_builder.add_node(\"get_failures\", get_failures)\n", - "fa_builder.add_node(\"generate_summary\", generate_summary)\n", - "fa_builder.add_edge(START, \"get_failures\")\n", - "fa_builder.add_edge(\"get_failures\", \"generate_summary\")\n", - "fa_builder.add_edge(\"generate_summary\", END)\n", - "\n", - "\n", - "# Summarization subgraph\n", - "class QuestionSummarizationState(TypedDict):\n", - " docs: List[Logs]\n", - " qs_summary: str\n", - " report: str\n", - "\n", - "\n", - "def generate_summary(state):\n", - " docs = state[\"docs\"]\n", - " # Add fxn: summary = summarize(docs)\n", - " summary = \"Questions focused on usage of ChatOllama and Chroma vector store.\"\n", - " return {\"qs_summary\": summary}\n", - "\n", - "\n", - "def send_to_slack(state):\n", - " qs_summary = state[\"qs_summary\"]\n", - " # Add fxn: report = report_generation(qs_summary)\n", - " report = \"foo bar baz\"\n", - " return {\"report\": report}\n", - "\n", - "\n", - "def format_report_for_slack(state):\n", - " report = state[\"report\"]\n", - " # Add fxn: formatted_report = report_format(report)\n", - " formatted_report = \"foo bar\"\n", - " return {\"report\": formatted_report}\n", - "\n", - "\n", - "qs_builder = StateGraph(QuestionSummarizationState)\n", - "qs_builder.add_node(\"generate_summary\", generate_summary)\n", - "qs_builder.add_node(\"send_to_slack\", send_to_slack)\n", - "qs_builder.add_node(\"format_report_for_slack\", format_report_for_slack)\n", - "qs_builder.add_edge(START, \"generate_summary\")\n", - "qs_builder.add_edge(\"generate_summary\", \"send_to_slack\")\n", - "qs_builder.add_edge(\"send_to_slack\", \"format_report_for_slack\")\n", - "qs_builder.add_edge(\"format_report_for_slack\", END)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that each sub-graph has its own state, `QuestionSummarizationState` and `FailureAnalysisState`.\n", - " \n", - "After defining each sub-graph, we put everything together." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "image/jpeg": "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", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Dummy logs\n", - "question_answer = Logs(\n", - " id=\"1\",\n", - " question=\"How can I import ChatOllama?\",\n", - " answer=\"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\",\n", - ")\n", - "\n", - "question_answer_feedback = Logs(\n", - " id=\"2\",\n", - " question=\"How can I use Chroma vector store?\",\n", - " answer=\"To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).\",\n", - " grade=0,\n", - " grader=\"Document Relevance Recall\",\n", - " feedback=\"The retrieved documents discuss vector stores in general, but not Chroma specifically\",\n", - ")\n", - "\n", - "\n", - "# Entry Graph\n", - "class EntryGraphState(TypedDict):\n", - " raw_logs: Annotated[List[Dict], add]\n", - " docs: Annotated[List[Logs], add] # This will be used in sub-graphs\n", - " fa_summary: str # This will be generated in the FA sub-graph\n", - " report: str # This will be generated in the QS sub-graph\n", - "\n", - "\n", - "def convert_logs_to_docs(state):\n", - " # Get logs\n", - " raw_logs = state[\"raw_logs\"]\n", - " docs = [question_answer, question_answer_feedback]\n", - " return {\"docs\": docs}\n", - "\n", - "\n", - "entry_builder = StateGraph(EntryGraphState)\n", - "entry_builder.add_node(\"convert_logs_to_docs\", convert_logs_to_docs)\n", - "entry_builder.add_node(\"question_summarization\", qs_builder.compile())\n", - "entry_builder.add_node(\"failure_analysis\", fa_builder.compile())\n", - "\n", - "entry_builder.add_edge(START, \"convert_logs_to_docs\")\n", - "entry_builder.add_edge(\"convert_logs_to_docs\", \"failure_analysis\")\n", - "entry_builder.add_edge(\"convert_logs_to_docs\", \"question_summarization\")\n", - "entry_builder.add_edge(\"failure_analysis\", END)\n", - "entry_builder.add_edge(\"question_summarization\", END)\n", - "\n", - "graph = entry_builder.compile()\n", - "\n", - "from IPython.display import Image, display\n", - "\n", - "# Setting xray to 1 will show the internal structure of the nested graph\n", - "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'raw_logs': [{'foo': 'bar'}, {'foo': 'baz'}],\n", - " 'docs': [{'id': '1',\n", - " 'question': 'How can I import ChatOllama?',\n", - " 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\n", - " {'id': '2',\n", - " 'question': 'How can I use Chroma vector store?',\n", - " 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',\n", - " 'grade': 0,\n", - " 'grader': 'Document Relevance Recall',\n", - " 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'},\n", - " {'id': '1',\n", - " 'question': 'How can I import ChatOllama?',\n", - " 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\n", - " {'id': '2',\n", - " 'question': 'How can I use Chroma vector store?',\n", - " 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',\n", - " 'grade': 0,\n", - " 'grader': 'Document Relevance Recall',\n", - " 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'},\n", - " {'id': '1',\n", - " 'question': 'How can I import ChatOllama?',\n", - " 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\n", - " {'id': '2',\n", - " 'question': 'How can I use Chroma vector store?',\n", - " 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',\n", - " 'grade': 0,\n", - " 'grader': 'Document Relevance Recall',\n", - " 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}],\n", - " 'fa_summary': 'Poor quality retrieval of Chroma documentation.',\n", - " 'report': 'foo bar'}" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "raw_logs = [{\"foo\": \"bar\"}, {\"foo\": \"baz\"}]\n", - "graph.invoke({\"raw_logs\": raw_logs}, debug=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Custom reducer functions to manage state\n", - "\n", - "Now, let's highlight a possible stumbling block when we use the same `State` across multiple sub-graphs.\n", - " \n", - "We will create two graphs: a parent graph with a few nodes and a child graph that is added as a node in the parent.\n", - "\n", - "We define a custom [reducer](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) function for our state." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Annotated\n", - "\n", - "from typing_extensions import TypedDict\n", - "\n", - "\n", - "def reduce_list(left: list | None, right: list | None) -> list:\n", - " if not left:\n", - " left = []\n", - " if not right:\n", - " right = []\n", - " return left + right\n", - "\n", - "\n", - "class ChildState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]\n", - "\n", - "\n", - "class ParentState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]\n", - "\n", - "\n", - "child_builder = StateGraph(ChildState)\n", - "\n", - "child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n", - "child_builder.add_edge(START, \"child_start\")\n", - "child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n", - "child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n", - "child_builder.add_edge(\"child_start\", \"child_middle\")\n", - "child_builder.add_edge(\"child_middle\", \"child_end\")\n", - "child_builder.add_edge(\"child_end\", END)\n", - "\n", - "builder = StateGraph(ParentState)\n", - "\n", - "builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n", - "builder.add_edge(START, \"grandparent\")\n", - "builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n", - "builder.add_node(\"child\", child_builder.compile())\n", - "builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n", - "builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n", - "\n", - "# Add connections\n", - "builder.add_edge(\"grandparent\", \"parent\")\n", - "builder.add_edge(\"parent\", \"child\")\n", - "builder.add_edge(\"parent\", \"sibling\")\n", - "builder.add_edge(\"child\", \"fin\")\n", - "builder.add_edge(\"sibling\", \"fin\")\n", - "builder.add_edge(\"fin\", END)\n", - "graph = builder.compile()" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "image/jpeg": "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", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "# Setting xray to 1 will show the internal structure of the nested graph\n", - "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[36;1m\u001b[1;3m[0:tasks]\u001b[0m \u001b[1mStarting step 0 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3m__start__\u001b[0m -> {'name': 'test'}\n", - "\u001b[36;1m\u001b[1;3m[0:writes]\u001b[0m \u001b[1mFinished step 0 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", - "\u001b[36;1m\u001b[1;3m[0:checkpoint]\u001b[0m \u001b[1mState at the end of step 0:\n", - "\u001b[0m{'name': 'test', 'path': []}\n", - "\u001b[36;1m\u001b[1;3m[1:tasks]\u001b[0m \u001b[1mStarting step 1 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mgrandparent\u001b[0m -> {'name': 'test', 'path': []}\n", - "\u001b[36;1m\u001b[1;3m[1:writes]\u001b[0m \u001b[1mFinished step 1 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent']\n", - "\u001b[36;1m\u001b[1;3m[1:checkpoint]\u001b[0m \u001b[1mState at the end of step 1:\n", - "\u001b[0m{'name': 'test', 'path': ['grandparent']}\n", - "\u001b[36;1m\u001b[1;3m[2:tasks]\u001b[0m \u001b[1mStarting step 2 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mparent\u001b[0m -> {'name': 'test', 'path': ['grandparent']}\n", - "\u001b[36;1m\u001b[1;3m[2:writes]\u001b[0m \u001b[1mFinished step 2 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['parent']\n", - "\u001b[36;1m\u001b[1;3m[2:checkpoint]\u001b[0m \u001b[1mState at the end of step 2:\n", - "\u001b[0m{'name': 'test', 'path': ['grandparent', 'parent']}\n", - "\u001b[36;1m\u001b[1;3m[3:tasks]\u001b[0m \u001b[1mStarting step 3 with 2 tasks:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mchild\u001b[0m -> {'name': 'test', 'path': ['grandparent', 'parent']}\n", - "- \u001b[32;1m\u001b[1;3msibling\u001b[0m -> {'name': 'test', 'path': ['grandparent', 'parent']}\n", - "\u001b[36;1m\u001b[1;3m[3:writes]\u001b[0m \u001b[1mFinished step 3 with writes to 2 channels:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", - "- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent', 'parent', 'child_start', 'child_middle', 'child_end'], ['sibling']\n", - "\u001b[36;1m\u001b[1;3m[3:checkpoint]\u001b[0m \u001b[1mState at the end of step 3:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': ['grandparent',\n", - " 'parent',\n", - " 'grandparent',\n", - " 'parent',\n", - " 'child_start',\n", - " 'child_middle',\n", - " 'child_end',\n", - " 'sibling']}\n", - "\u001b[36;1m\u001b[1;3m[4:tasks]\u001b[0m \u001b[1mStarting step 4 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mfin\u001b[0m -> {'name': 'test',\n", - " 'path': ['grandparent',\n", - " 'parent',\n", - " 'grandparent',\n", - " 'parent',\n", - " 'child_start',\n", - " 'child_middle',\n", - " 'child_end',\n", - " 'sibling']}\n", - "\u001b[36;1m\u001b[1;3m[4:writes]\u001b[0m \u001b[1mFinished step 4 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['fin']\n", - "\u001b[36;1m\u001b[1;3m[4:checkpoint]\u001b[0m \u001b[1mState at the end of step 4:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': ['grandparent',\n", - " 'parent',\n", - " 'grandparent',\n", - " 'parent',\n", - " 'child_start',\n", - " 'child_middle',\n", - " 'child_end',\n", - " 'sibling',\n", - " 'fin']}\n" - ] - }, - { - "data": { - "text/plain": [ - "{'name': 'test',\n", - " 'path': ['grandparent',\n", - " 'parent',\n", - " 'grandparent',\n", - " 'parent',\n", - " 'child_start',\n", - " 'child_middle',\n", - " 'child_end',\n", - " 'sibling',\n", - " 'fin']}" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "graph.invoke({\"name\": \"test\"}, debug=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Notice here that the `[\"grandparent\", \"parent\"]` sequence is duplicated! \n", - "\n", - "This is because our child state has received the full parent state and returns the full parent state once it terminates. \n", - "\n", - "To avoid duplication or conflicts in state, you typically would do one or more of the following:\n", - "\n", - "1. Handle duplicates in your `reducer` function.\n", - "2. Call the child graph from within a python function. In that function, handle the state as needed. \n", - "3. Update the child graph keys to avoid conflicts. You would still need to ensure the output can be interpreted by the parent, however.\n", - "\n", - "Let's re-implement the graph using technique (1) and add unique IDs for every value in the list. This is what is done in [`MessageGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.MessageGraph)." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [], - "source": [ - "import uuid\n", - "\n", - "\n", - "def reduce_list(left: list | None, right: list | None) -> list:\n", - " \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n", - " if not left:\n", - " left = []\n", - " if not right:\n", - " right = []\n", - " left_, right_ = [], []\n", - " for orig, new in [(left, left_), (right, right_)]:\n", - " for val in orig:\n", - " if not isinstance(val, dict):\n", - " val = {\"val\": val}\n", - " if \"id\" not in val:\n", - " val[\"id\"] = str(uuid.uuid4())\n", - " new.append(val)\n", - " # Merge the two lists\n", - " left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n", - " merged = left_.copy()\n", - " for val in right_:\n", - " if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n", - " merged[existing_idx] = val\n", - " else:\n", - " merged.append(val)\n", - " return merged\n", - "\n", - "\n", - "class ChildState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]\n", - "\n", - "\n", - "class ParentState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [], - "source": [ - "child_builder = StateGraph(ChildState)\n", - "\n", - "child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n", - "child_builder.add_edge(START, \"child_start\")\n", - "child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n", - "child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n", - "child_builder.add_edge(\"child_start\", \"child_middle\")\n", - "child_builder.add_edge(\"child_middle\", \"child_end\")\n", - "child_builder.add_edge(\"child_end\", END)\n", - "\n", - "builder = StateGraph(ParentState)\n", - "\n", - "builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n", - "builder.add_edge(START, \"grandparent\")\n", - "builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n", - "builder.add_node(\"child\", child_builder.compile())\n", - "builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n", - "builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n", - "\n", - "# Add connections\n", - "builder.add_edge(\"grandparent\", \"parent\")\n", - "builder.add_edge(\"parent\", \"child\")\n", - "builder.add_edge(\"parent\", \"sibling\")\n", - "builder.add_edge(\"child\", \"fin\")\n", - "builder.add_edge(\"sibling\", \"fin\")\n", - "builder.add_edge(\"fin\", END)\n", - "graph = builder.compile()" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "data": { - "image/jpeg": "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eR+Tqj8NyuwXrNr/ALR8YTq3S3OiIvtHxwiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgKg7Svh3FP29R+CVflBZRh9HlnYZqpqqnkpHufFJSS824Et3Tx07xXuUVRVTM5ZxMc4mF1muLdyKp6musowuwZvRxUmQ2WgvdLFJzscNfTtmYx+hG8A4EA6EjX61Wv8A4f8AZnpp0AxvTvexcP8AKto+1VQ+GL357/RPaqofDF789/osOPwqumMovfFrzjbM7ZpULG9lGF4dchcLFilms9eGGMVNDQxxSbp6xvNAOh0VrUl7VVD4Yvfnv9E9qqh8MXvz3+ijP4TNW2bscpSjHWo2REo1FrTlLUtbsvxrE62xXu6Rz3LKLdaagz1HOAwTPcJABpwOg4HuLbvtVUPhi9+e/wBF55H/APLHKXvlC1wlDVtFT3KiqKSrhjqaWojdFLDK0OZIxw0c1wPAggkEKjjYBszaQRgOOAjiCLXCCP8A2raPtVUPhi9+e/0T2qqHwxe/Pf6KUfhVVO69EfpKM46zVvpaxg2C7N6WeOaHA8dimjcHsey2QhzXA6gg7vAqd2gfEPI/J1R+G5XH2qqHwxe/Pf6LyrNj1ruFJNS1N0vM1PMx0ckbq3g5pGhB4d0K+z+GzReouV3c9GYndPVLzptmImIhe0RFqsIREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQc78tr4k7P/8AHVm/EcuiFzvy2viTs/8A8dWb8Ry6IQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREHO/La+JOz//AB1ZvxHLohc78tr4k7P/APHVm/EcuiEBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERARRl+yKgxqkbUV8/Nh7tyKJjS+SZ+mu6xg4uOgJ0A4AEngCVT5toN+q3E0VjpqOHhum41ZMpH1sja4D/AFn12026qoz3R79n+11Fqu55sNhota9M8u/RrJ/qmTpnl36NZP8AVMpaqO1HNb0S9wbKRa16Z5d+jWT/AFTJ0zy79Gsn+qZNVHajmdEvcHym5bWwo7DNuN0pqKnEOOXnW52vcboyON7jvwjuDm37zQP7u4T1ru//AKb2xObZlsWkyW4Rvhu+XvjrTE4+4pGBwp+HVq4Pe/XuiRveUlygtkM3KNtdhosjgtkHsRXtq4p6V0gkdGeEsBcRwY8BupHHVjT3FtSnyvKaSniggorFDDE0MZGznQ1rQNAAO4AE1UdqOZ0S9wbORa16Z5d+jWT/AFTJ0zy79Gsn+qZNVHajmdEvcGykWtemeXfo1k/1TL+jNMtbxNHZZP8AtEkzdf36H7k1UdqOZ0S9wbJRUm27S2MlbFfbe6zlx3W1UcnP0uv/AHP0BZ+t7QPr6ldlXVRVRvc9dFVE5VRkIiKCAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiIC8K6tgttFUVdTIIaanjdLLI7qYxo1JP6gCvdVHaw9zcAujR7mTmoZO9zb5WNfr9W6XKy1Tp100T1zCVMaUxCp0lTU3uc3m4McyrqW6xU73aikhOhETe4DwBeR7p3d0DQMxFpfaznV3o9pNDjEGX02z+2GyT3b2XqaeGXsqZkoZzI54Fgaxp33AdsQRoRxKprrm5VnL6f8tqmIiNjdCLmnCs12iZxU7L7W7JJ7FLe8XqrtdJ3UFPJO57ZYQx8YdHut4SDQFpG446gu0cPLa7tbynDLrfbljmQ3a+0ONy0sVxo2WWl9j43aR87HPUuc2QyuDt78iCGb7QWhQyQ10ZaWX3vdNoufsvyrOqq+7YpbTlvsRR4bDDVUFE23QSiYmgZO6OV72lxYXB3udHDfPbaANGfbs1y7a9l7rXj9+ZhtDa7Jb7nWTR0UVVNU1FYx72RgSghsTGsOpGjiTpqNEyS1sZ5ZNz2q70N+t8FfbK2nuNDON6KqpJWyxSDXTVrmkg8QepZa5P2M3TJ6zF9jmK2bJpsdorhj9yq6qWmo4JpHviqIgwt51jg3+0d3CNCeGuhFl9tbIpsSuFlrclqqbLLdlM+PwVdjs8NTV3cRwiYGOCQ81G7ckaXuPaN5t3VvDQhF6JjOYdFouYKPa5tAuuDWelbcTa8jZnzcWqa2toIOckpzG5+9LCwujDwHN1EbtCWcCASprNMyzq251S4DZblkF2noLULpXXi22+2SVs5lnkbGwsndFCxjQwgljC48OriSye66Ms8pdCr+PkbGAXuDQSGguOnEnQD/Nc+UuW7TrrednOOXaufh9yu0d4bcJW0VNLNNHTmE08zW70scUjmuOrdXtBc7ge10q2Y3bJc3xHGaS4ZHPBdLLtKjsTrlSUsDXVPNzkRVDmOY5oe0EHdA3SddQRoEyJvRlu+/uXVj2NkaWuAc1w0II1BCkMBur7VcnY7K8upDCZ7c57tSxrTpJD391u8wt+pxHAMChrVSz0NspaaprZblUxRNZJWTMYx87gNC9zWNa0Enjo0AceAC/jnuiyrFHs/tPZFzOriWmnm3h/lx/cumxtmaJ3TE+EZx98M0MTRFdqc+ptlERVvnBERAREQEREBERAREQEREBERAREQEREBERAREQFHZFZo8isNwtcrzEyrgfDzjfdMJGgcPrB0I+sKRRSpqmmYqjfBualtVVNUUxjrIxBcad3MVcAOvNygDUfqOoc091rmnurTnKCwG85bfbBW2yy3u7Mo4ZW71pudBDzL3OaQTFWRPYTw92whwHDQgrpjKMNbepm11DUC33Vjdzntzejnb3GSt4FwGpIIIc0k6HQuDqdPBkFucWVmOVMxBA563SxzxO75Gpa/wDzaFObWnOlby7s93Pf95t2jEW71GjXOUtebN9n15aMYybNa59TmtvtlTbZuYfGYXRSztkG/usbvSNbHE0ubutJ3uHELEyvk5Y/lsuRxz3i/wBFar/KamvtFDWtipZKjca3n9NwuDu0YdN7cJaNWlbH9kK/6OXrzT+qeyFf9HL15p/VedHu8F+lZyymY5qwNk1rLM252tuE0mXU7Ke4yvfHvNDaUU29HowBriwanUEb3UAOCia3YJaJK+019tvd/wAfuFBbYbTJVWmrZE+tpohoxs4LC1xHHRzQ1w3joRw0vvshX/Ry9eaf1T2Qr/o5evNP6p0e7we6dmeuGkark8VNtyjZ7bLDdr7a8dx+zXGjdeaSsgbWMkllhcxjt5hD94Nk6oyBujXQ6K3N5PeO0tgtFBb7heLXcLZXTXKG+01U11e+pma5s8kkkjHNeZA4h280jTTTTQKzZTtCo8JpaOpvtuulsgrKuKhp3z0ugknkOkcY49biOCmvZCv+jl680/qnR7vBGJsx1w1/auT5YLSIhHc71OI8giyY9lVTZXPrWRc25znOYXFr/dOGvX7ndHBS2cbI7dml/ob9HdbvjmQUkDqRt0sdQ2GaSnc7eMLw9j2vZvdsAW8DxBCtXshX/Ry9eaf1T2Qr/o5evNP6p0e7wS0rOWWcK9TbMLfBeMTuklwulZW43TVVNTS1dQJXTicMEjpnObvOd+TGhBHWeB4aRVw2FWC449eLS6rucIuN8dkTayCdrKikrDI14fC4M0AaW8A4O6zrqrt7IV/0cvXmn9UFdcX8GY1enO7xp2t/3c4D/dOj3eHwNOzxgsNqfZLRTUMlwrLq+Fu6ayvc108vEnV5a1o149wBSmJ0Lr3l7asAmitDXflAe1fUvaW7o+tkZdr+1b3jp/KDFcgvrgKqIY/REnf3ntlq3t7zd0lkZ/7iX/q7ov8Aa7XSWS3w0VDA2npYRoyNv69SSTxJJJJJ4kkk6kqUU6nOZnbPh8nDicTTNOroZaIipZAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiDnfltfEnZ/8A46s34jl0Qud+W18Sdn/+OrN+I5dEICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiDnfltfEnZ/8A46s34jl0Qud+W18Sdn/+OrN+I5dEICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICKOmyK1U7yyW50cTx+a+oYD/uV+OlVl8MUHnLPWrNXXPVL3KXzb5U3LjuV/u8OE3jZz7BXPFMoprhO4XoziZ1LI47jR2O3Rr9dQ/jwIOh1XX3JM5T1dyn7JkF3kwt+K2621EVLBUG49ltqpC1zpGj8lHulg5snr15wdWi5e/wCpFsNhynK8czrEOx7lcLpLHZ7lTUcjXvdL1U8ztD1EAxlxIA3Yx3V2TsIw/F9iGynH8PobxbpDQQA1NQ2pZ+XqHdtLJxOuheToD1DQdxNXX2ZMpbSRRfSqy+GKDzlnrWZSXClr2l1NUw1LR1mKQOA/yXk0VU7Zh4yERFAEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQRmQ3+mxu2urKhr5O2EcUEIBkmkPuWNBIGp75IAAJJABI1vcqeryhxkv03Pxu6rbA8ikjGvURwMp7hc/r46NaDopDLKo3TPHQOIMFqpGFje9NMXbzu9wY1gB6+3d1a8caurYbdRVFXUP5ungjdLI/Qnda0ak6DieA7iuqqmzlFOyd+ffuy/T6NrCWKYp1lW+WIzHLTGNGWuiaNddBTsH/wDC/XR+1+DaP7BvqVRxTbtg+a3K3UFpvTpam5QmehFRRVFM2rYG7x5p8sbWyEDiWtJI0OoGhXra9tmF3nLujNLetb0ZZYGQS0s0TJZI9ecZHK9gY9zd12oa4nge8qNZc7UtDSo6phaej9r8G0f2DfUnR+1+DaP7BvqVate2TEb1klTYaG6SVVzpamakqWR0U5jp5Yg4vbLLubkfuHaFzgHadqSvDHtueD5Vdm222X1k9U9kkkIfTzRMqWsGr3QSPYGzADj+TLuHHqTWV9qTSo4wtnR+1+DaP7BvqXjJilnfM2Ztugp6hp1bUUzeZlafqezRw/cVV8W264NmtyttDZr4Kue5xGWic6lniiqQG7zmxyPYGOe0a7zAd5uhBAIOmFsj24W3axW36kpqOsoqi219TTME1FUsZJDFIGCQySRNa17idTFrvt7o4Er2LtynbFU83mlROzPe2tjuV1liqoKG71MlfQTOEcNwlDechcfcslIABaeAD9NddA7XXeWwlquto4bjRz0tQwSQTMMcjD3WkaEK47PbvPesQoJ6qTnauLnKWeT+/JDI6J7v3lhP71dM6yjT642T+u74MfGWItzFVO6VjREVLOEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERBq++05odoV2DgQK2lp6mN2nA7u9G4frG6wn/AMgofNviZf8AyfUfhuWxczxh9/pqeoozHHdqJxfTPlJDHA6B8TyNSGuAHEA6ENdod3Q0N0tLfKestlXC+GYxuiq6Co7SaNrgQQ4A9R46OaSD1tJGhVt2JriLkcIifdls+/e3sJdiu3odcOZ8CrrhtKwnYTYLZjV7pG2A2y6116r6J0FIyGClLdIpXcJTKXADc17UknRYHYOWXy+YXcL9as2rsotuWx1V35yGYWmjpucliaaaMHm5Ghr4zvsD3bu+XOHELqnH7DQ4tYrdZrXB2LbbfTx0lNBvufzcTGhrG7ziSdAANSSe+s9c2a7U7IzlojHNn94u2y/bRY4aWa03O/Xu9ijkqY3Q88JW7sUgJHFjuADhqNOpQWzXFbReRYaa4YptCo7/AGWkdNpfK2ult1HVMhMZERkmMUgO88M5sEbp7nUulEXmaeqjY5wx7E71TbLuTvTus1fFW2q6Uj6+F1K8SUbBRVLXmUaaxjec0Eu04kDuq47FZK7G8mzjGrlZLrSz1GQ3C709wdRvNDPTzSNewtnHab3baFmuo3TwW3kR7Tb0ZiYkJAGp4BWDZXTOhwmkmcC3syaormhw0O5NO+RnD/xc1VW32855I+jpHB9m1MdbXMd2rh1OhicOtx6nEHtRr+doFtaONkMbWMaGMaA1rWjQADqAC68pt29CrfM58s/jmy8bdirKiOp+kRFSyxERAREQEREBERAREQEREBERAREQEREBERAREQEREBEWjdpnK3xPDL4cXxqlrNoudO1azH8bbz7o3DgeflGrYgD7rXUt6y3RBvEkNBJOgHEkrQG0Dlh49a76/Fdn1rrNqebcW+xmP9tT056tZ6nQsY0Hr03tD17qrntHbVOUIRUbYMm6JYtJxGC4nNu843+7V1XEv+trdWnrBaVv3ANmmLbLLEyzYnYqOxW5uhMVJHoZCPznuPbPd/3OJP1oNabHsN2xV2X9MtpuWUlFE+nfDBhNiiBo6cP0O9LKdTJINBxBOnHR2hIW279itpyZjG3KiZUOj15uUEslj7+69pDm9zqI6lLIpU1TTOdM5S9iZjbClP2U20uJjuV5iaTrutr3uH/u1K/PtUUHhe9ee/0V3RW6+5xW6652pUj2qKDwvevPf6J7VFB4XvXnv9Fd0Xuvucfga652pUj2qKDwvevPf6L2g2U2ASB9W2tumh15uvrJJYj+uPXcP72lXFF5r7vVU8m7cnZNUou+2Z90xq4WmhrZbJJUUklLBWUQaJKQuYWtfGCNAW6gjucAubW5ttx5OH5PMrS7bFg8P/6hsMIju9LGO7PTdUmgHW098uf3F1MioVKJsp24YRtrtJr8Qv8ATXTcAM9JrzdTTnvSRO0c3jw1I0PcJV7Wl9qnJQwvaTdhkVEKrC82jJkhybG5exaoP78m72smvd3hvEcA4KkN2m7ZOTz+R2jWE7TsOh4DLsXgDa+Bg/OqqPu6DiXM4ADUlxQdPoqhs02uYftgsYu2H3+kvdINOcED9JYSepskZ0cw/U4BW9AREQEREBERAREQEREBERAREQEREBERAREQFpzaryqMN2aXUY9SmqzHNZTuQYxjkXZVWX96Td4RDq13jrpxDSqpymqi/ZVta2S7MqDKLpi1hy0XV11nsr2xVUrKaCOVjGylpLAdXtOnXvcddNFtXZXsSwrYvajQ4jYaa184AJ6vQyVNSe/LK7V7uPHQnQa8AEGmjsw2xcoj8rtHvp2ZYbNx6IYvOHV1Qw/m1VZ1DUcC1nAg8Q0rd+zTZHh+x+xi04fYKSyUmg5wwM1lmI6nSSHV7z9biVb0QEREBERAREQEREBERAREQEREGk9pPJPxPM74cox6er2e5y3V0eRY2/mJHuPH8vGNGTNJ90CAXDhvaKpDbZtP2BEU+1/G+lOLx8BneJwF4jb/AHqykHGP63M7UdQDiumV/CAQQRqCgr+DbQsa2mWGK9Yte6O+2yTqno5Q7dP91w62O77XAEd0KwrljlK7B7Fs5xLKNrGz2prNnuZ2ijfWvmsDxFTV+7x3KinIMbweJ9yNTxO8uiMAu9TkGB43dKxwfWV1tpqmZzW7oL3xNc4gDq4koJ9ERAREQEREBERAREQEREBERAREQEREHO22b54XJ0/ZZH/wmLolc7bZvnhcnT9lkf8AwmLolAREQEREBERAREQEREBERAREQEREBERBqHldfNl2k+RZ/uVy2S/JVhnkWi/AYqbyuvmy7SfIs/3K5bJfkqwzyLRfgMQWxERAREQEREGLdKs0Ftq6lrQ90ML5A09RIaTp/sq5Q3HKa+hp6lrLO1s0bZA0mXUAjXT/AHU5kfxeunisvoFYuPfAFs8Vi9ALDx1y5TdppoqmIy6u9bTEZMTn8r71m/ipz+V96zfxVNouDWXvaSllHBCc/lfes38VOfyvvWb+KptE1l72kmUcEJz+V96zfxU5/K+9Zv4qm0TWXvaSZRwQnP5X3rN/FTn8r71m/iqbRNZe9pJlHBCc/lfes38VYVZkN6t1dQUVXW47TVle90dJTzTPZJUuawvc2NpOryGtc4ga6AE9QVP2y5hkdPmOB4Li9xisNwyiWskmvUlM2odS09LE17xFG/tTI8yMALtQBvHQqhbVcWyylzbYnaZM3fWX598uXNZBNa4GyRRm2z6jmWaRueGhwDt3TUglpA0N1M3pyzuzt++DzZwR23W/XGw8qXYDX3mtsltZGy/NjqaiZ0VO0upY26Pc4jQkua1vfJA7q39R5DerjW11HSVuO1VXQPbFV08Mz3yU73ND2tkaDqwlrmuAOmoIPUV8+OUftDymDatgFuvVypb7eMVyi42hlzqLbTltZTyR217XSQOYYt8NqHN1a0aFrXDQ8V0hcdolRspr+UtlFHTR1ldQ3W2NpoZiRGZpLfSRRl+nHdD5Gk/UDxCvri9o05XJzy57e73vIy4N7zZDeqa60tsmrcdiuVVG+Wno3zPE0zGbu+5jCdXBu83UgaDeGvWFm8/lfes38VaGteM5ZjnKc2cjK8ydl9TPj14c1xt0NI2nfv0nONZzYGrDq3QO1cN06uOvDpRc1VV2nLK5P3+iWUcEJz+V96zfxU5/K+9Zv4qm0Vesve0kyjghOfyvvWb+KnP5X3rN/FU2iay97STKOCE5/K+9Zv4qc/lfes38VTaJrL3tJMo4ITn8r71m/ipz+V96zfxVNomsve0kyjgwcVu9VeKGofWxwx1EFTLTuEBJYdx2mo14qZVcwn3rdfKdT6asa+gwlVVeHoqqnOZhVVskREXWiIiICIiDUPK6+bLtJ8iz/crlsl+SrDPItF+AxU3ldfNl2k+RZ/uVy2S/JVhnkWi/AYgtiIiAiIgIiII7I/i9dPFZfQKxce+ALZ4rF6AWVkfxeunisvoFYuPfAFs8Vi9ALAx/p6e75raNyQREXCmIiICIq7fc7ttkqn0bW1FyuDAC6joI+cezXq3ySGM17m84a9xWW7Vd2rRtxnL2ImqcoWJFRX7RLmT+Txefd1/+5WxNP+xP3r8+2Jd/os/z+P1Lt8n4jhH7qfqu1FzssnaVsqtO0+nthraq4Wm52qoNVbrxaKjmKyjkLS1xY8gghzSWua5paR1jgFXbjyfaK62+0R1mXZVV3K1S109Ld569pqmyVVJJTOdvtY3c3GyFzBHubrgD39Zn2xLv9Fn+fx+pPbEu/wBFn+fx+pTjBYqNkZfup+rzUXOy+MG1eHNcR2kXmy5XebnV5Dari8y1NVWSyPdNowCdr3HXV7I4SH9Za1neGn0/5O3JifT7KaybaJeL7kN7za1tGRUF1rnSxuc4Dmid7WRs8UYZHvb/AALeA4N0rO1jYBBtT2/4jtHqceEEVs3XXS3mpjd7IOiO9Tknq4HRrtQdWNa1dDe2Jd/os/z+P1Ltv2MRXRTFMR7/AM1P1RjD3InzURhvJ+oMSy+0ZJPluVZJcbTRT2+kF9rop2MhlMZcO1iaSRzTe211P5xdoNNpqh+2Jd/os/z+P1J7Yl3+iz/P4/UuGcDiat8R+6n6pai52V8RUQbRLtrxxeTT6q6IlZlHtLoDK2K6UlXY3OIaJawNMBJ6hzrHOa3vdtu8f3a11YHEUxno590xPhEy8m1cpjOYW9ERcCoREQEREEPhPvW6+U6n01Y1XMJ963XynU+mrGt/A+rW+5TV50iIi7kRERAREQah5XXzZdpPkWf7lctkvyVYZ5FovwGKm8rr5su0nyLP9yuWyX5KsM8i0X4DEFsREQEREBERBHZH8Xrp4rL6BWLj3wBbPFYvQCysj+L108Vl9ArFx74AtnisXoBYGP8AT093zW0bkgiIuFMREQVXNcgnpJKW02+Uw19W10j6hrQ7seFpAc7Q8N4khrde+52h3CDX6Khgt1O2CmjEUYJOg4lxJ1LiTxLidSSeJJJPFfmskNVnuRyP91AKakYT/cEfOelK5Y2T3+mxPG7te63e7DttJLWz7g1dzcbC92n16NK+mt24s2qaI64iZ98zGfhE5f7ltYaiKLelxSSLR8182h5Vsrvl9vsGO0FgueO1dVHbqQTuraZr6dzog+Uu3HnQje0Y3Q66aqN2T5xmeP0Oy6w3mnsTrVklkEVskohMZqWSGjbKzny46SBzGnXdDdDw49akt1sZxGToJFytYq+W42TZhLU0dDR1jdpVfHUttrZWwyStbXtfI1sskjhvEF2m9oNeGgXrt1zzL9oGyjarPZqay0uF2nsq0yOrRK6trHwkNnkjLSGRtDtQ0ODi7dPudQmSOujRzy+8nUiLxovecH7Nv3LU2SbVMiodtdPiEL7DY7a+Kmlp5b42YS3ffc4TMpZGuDA+MAdo4OJJHADiC6qqKd7b6LSGyy4ZlUbWtqfshe7bUWKhurIxTy082/E00cT4hG8zFsbAHAvG6d52+4bu9oMLZjt9vORbSLZjd1qLFeaO7UtTNS3DH6Sshhjkg3S5glnG5UNIcdHxHrbxA1CZIRdjZn1t+L+OaHtLXAOaRoQeorlnJNo+0vNuS5kWayVdjsEE9rqHRR26CoFW3cm3DI2XngGbzWv0GjiNWnU8Wro7EYr3FYqduQ1VBW3PiXS22mfTwlv5ujHySHXTrO9x+pHtNyK52R705id0ON3Sms7yTaavVlECOFLKAXc1r/ccAd0fmlpaODmNbsBajy6Q01kfVNGstLNDUx9/eZK1w0/y0+vXRbcWbj7cflvddWcT3xlt8fn1srFURRXnHWIiLJcYiIgh8J963XynU+mrGq5hPvW6+U6n01Y1v4H1a33KavOkREXciIiICIiDUPK6+bLtJ8iz/crlsl+SrDPItF+AxU3ldfNl2k+RZ/uVy2S/JVhnkWi/AYgtiIiAiIgIiII7I/i9dPFZfQKxce+ALZ4rF6AWVkfxeunisvoFYuPfAFs8Vi9ALAx/p6e75raNyQREXCmIiINe5RRutOaCq3T2LdoGx7wHBtRFvcCe+6M8P2Lvq1wr1aKTIbNX2uviE9DXU8lNPETpvxvaWuH7wSth3e0Ut9t8lFWx87BJoSA4tc1wILXNI4hwIBBHEEAqgV9DecbcW1VHNeKMe5rqCLfk0/8AyQjtt762BwOhOjdQ1fQYa9F6iKJnKqNnfHVl7+rL47ctTD36dHQrassmx3KrXjc+M1O0F9xxpttntlLTTWiMVDY3RGOPnZg/8pzYI6msLtOJUxS7JOxp9mEnsrvdCoHw6djadm71GabX3f5Pr3vzu99atb8ttkZIkknicDpuyUsrD/kWr89MbR+kv+wk/lXfqLs/8J5S64i3x8Wu5NgMseK2+3UeSPo7pbcmqMloriKJr2skllmcYnxF/bN3J3t13gTwPDqUXl3Jyu94t+aWWyZy6yYzlM01XVWya1MqnQ1E2hldHKZGkNe4bxYQes6EarbHTG0fpL/sJP5U6Y2j9Jf9hJ/KmovdieUvJptTs+aCrM+u1qq5aKLZ9lFxip3GJtXTOt4imA4b7d+ra7Q9Y3mg98BVvNdmd92xw0vsje6iwY1PLTVM2O1dsp31kMkMoeNypZK4RlxaNSN/QHgRqVeZtoePU1ZTUktzZFVVO9zED2PD5d0au3Wkau0HE6dSyOmNo/SX/YSfypqL3YnlL2dGrZNSlVuxmrmy/LK6myN1NjuVxbl3sxog9739jdj78U++DHq0MJBa7i3rGqjsW2G3yy5DhVzuOatukeKQS0VFRx2hlPG6mfCIiHESE852kZ3wd3tNNwakrY3TG0fpL/sJP5U6Y2j9Jf8AYSfypqL3YnlJlbzzz8VOtOxKkpdhUmzSuuUlZSy0M9E+viiET/yjnODwwucAWlw4EnXRWvB7NfbDYm0mQ36LI65ju1rYqEUY3A1oDSwPfqdQSXa8d7qGi9xmFpPAVEjj3hTyE+isukqbheniO02mql101qa2J9LA0d/V43nfqa08e9xIjVarpjOuMo9+z4mlbo257n8raN17udss8YLufqGVE501DYInte/XvbxDWd/t/q1G0lC4zjEePQyyPl7MuNTumprC3dMmmu61rdTusbvHdbqdNSSS5znGaWDi79N2qKaPNp8Z65++HUyL93W1ZxuERFwOcREQQ+E+9br5TqfTVjVcwn3rdfKdT6asa38D6tb7lNXnSIiLuREREBERBqHldfNl2k+RZ/uVy2S/JVhnkWi/AYqbyuvmy7SfIs/3K5bJfkqwzyLRfgMQWxERAREQEREEdkfxeunisvoFYuPfAFs8Vi9ALKyP4vXTxWX0CsXHvgC2eKxegFgY/wBPT3fNbRuSCIi4UxERAREQEREBERBz1tk+d7yd/wBlkX/CYuhVz1tk+d7yd/2WRf8ACYuhVbX5tHd85eQIiKp6IiICIiAiIgIiIIfCfet18p1PpqxquYT71uvlOp9NWNb+B9Wt9ymrzpERF3IiIiAiIg1Dyuvmy7SfIs/3K5bJfkqwzyLRfgMVN5XXzZdpPkWf7lctkvyVYZ5FovwGILYiIgIiICIiCOyP4vXTxWX0CsXHvgC2eKxegFlZH8Xrp4rL6BWLj3wBbPFYvQCwMf6enu+a2jckERFwpiIiAiIgIiICIiDnrbJ873k7/ssi/wCExdCrnrbJ873k7/ssi/4TF0Kra/No7vnLyBERVPRERAREQEREBERBD4T71uvlOp9NWNVzCfet18p1PpqxrfwPq1vuU1edIiIu5EREQEREGoeV182XaT5Fn+5XLZL8lWGeRaL8BipvK6+bLtJ8iz/crlsl+SrDPItF+AxBbEREBERAREQR2R/F66eKy+gVi498AWzxWL0AsrI/i9dPFZfQKxce+ALZ4rF6AWBj/T093zW0bkgiIuFMREQEREBERAUfkV5GOY/c7s6kqq9tBSy1RpKJgfPMGMLtyNpIBe7TQAkAkjiFIIg+cmfcvvZ9lW3fZVmtJZ8ljtWKMura2GalpxPIamnbFHzQE5adHNO9vOboOrXqXbew7bTadveDNyyx2u7Wy1yVMlPCLvDHHJNuaBz2COR4LN4ubrrrqx3Dhx+avKW5Jtxs3KuocSxqkENqzOqbVWstZrHTte7/AOYaQOpsR33aDqj3V9TcFwy27O8Ns2M2iLmrbaqWOkhB90Q0abzu+5x1JPdJJXfiItRRTofcIRnntTqIi4ExERAREQEREBERBD4T71uvlOp9NWNVzCfet18p1PpqxrfwPq1vuU1edIiIu5EREQEREGoeV182XaT5Fn+5XLZL8lWGeRaL8BipvK6+bLtJ8iz/AHK5bJfkqwzyLRfgMQWxERAREQEREEdkfxeunisvoFYuPfAFs8Vi9ALKyP4vXTxWX0CsXHvgC2eKxegFgY/09Pd81tG5IIiLhTFiXW60tkt89dWyiGmhbvPfoXHvAADUuJJADQCSSAASVlrX+YVbrpl9NbyT2NbYG1j2g8HTSF7Gaju7rWvPe1cD1gadWGsxfuaM7o2z3f53LLdGsqiljV90vOSkumqZ7JQuHaUVI8NqNO/JK0nQ6fmxkAce2dwKjX4jbJXF0kdRM49bpauZ5/zLyphV47QcbkudZaaa/wBqq73Sse+S1xV0bqlu6CSDGCXDq7oX0VFc24yt/lj3bP8Af6tum3bojLJ79DbR+jP84k/mTobaP0Z/nEn8yr2yzbBY9p+O2arhrbfSXqvoI6+Wxx17Jqmma9oPbNGjtOI4loUOducMlPbKqkoqC50VxypmOU9VbrxFVRujcwuFQXRhwa7gQYiQRwJPEKevvduecmdvLNeehto/Rn+cSfzJ0NtH6M/ziT+ZRO0nanYdmlhuVXcLlbmXOnoJ62ltVTXRwTVhjY5wYwO1J1LdNQ09fUVP43eOkOO2u68z2P2dSxVPNb29ub7A7d10GumumugTX3u3POUsqM8smBJgNhmqIaiSgD54Q4RSuleXR73B26d7Ua6DXTrXt0NtH6M/ziT+Zfm/53jWKSOjveRWqzyNax5bX1sUBDXlwYSHuHBxY8A90tdp1Ff2351jd3ukltochtVbcY4hO+jp62OSZkZAIeWBxIboQddNOITX3u3POTKjdlD+9DbR+jP84k/mTobaP0Z/nEn8yicU2mWzIcaueQVNZaaKy0lZPBHcYLvBU0skLHaNmdKw7rN4EEsJ1broViYTtdtWcX7MKWhlo5LTj7qbdu9PWsmgqWSwCVzwQN1obqWntj1E8OpNfe7c85ef09myNqw9DbR+jP8AOJP5kGHWkdUErT321MoP+e8teX3lG2akrMqZY20OTUdixiXIjWW+5MfHM5j5Gmm1Y1wafyYO9qdN73Pfv1RnNltGN2+9Xy50FgpKuKN4kuFWyGMOe0ODN95AJ+/RNfe7c85I1c7sklSQ3OxuD7TdagNBGtJcJH1ULh3tXHfb+trtB3j1K8Y1ksOR00hEbqWtp3BlTSSHV0TiOBB/Oaetrh1/UQQKdQ11Nc6OGro6iKrpZmh8U8Dw9j2nqLXDgR9YXi+rNkv9pujCWtM7KCoG9oHRTPa1uo7pbJzZB7g3u+VyXrMYmJiY/N1Tx908c3NfsUzTNVMbWz0RF82yBERBD4T71uvlOp9NWNVzCfet18p1PpqxrfwPq1vuU1edIiIu5EREQEREGoeV182XaT5Fn+5XLZL8lWGeRaL8BipvK6+bLtJ8iz/crlsl+SrDPItF+AxBbEREBERAREQR2R/F66eKy+gVi498AWzxWL0AsrI/i9dPFZfQKxce+ALZ4rF6AWBj/T093zW0bkgiIuFMWtrvGabaHdw88aijppo9e6AZGHT9RA/1fWtkqsZtj9RcY6a429nOXKh3i2DeDeyY3Dt4tTwBOjXNJ0G80AkAkrQwVym3cmKpyiqMvhMeML7FcUXImVPyqG4VOL3iG0v5u6SUczKR+um7MWEMOvc7bRaJ2F5Rs6gwXG8Zpaamos5pLa+KsoZ6Bza6CrbA7sl0rizVpc4SduSA7eABOoC6Coa+G404mgcS3Utc17Sx7HDgWuadC1wPAggEd1ZC2ZiaZyltTTpTFUOTsIsNtsuzHk1XO32+lorjLdqaOSqp4Wske2ahqjKHOA1O+QCdevReNbPT4/Z6+7TMNPaLPtffV1k0cRLKaAM3TI4NHBoL26nuarrdEzV6nKMolyLnd4xaw+31Q5tFA3L7rFUy2eWupTIamh7BaKZtO7dI0ZIH6hvuXcT9XTGzf5O8W8lUv4LVYlTLtsptV5uVRXTXXJoZZ3l7o6TJK+niae82Nkwa0fUAAvEqaJonOFJulmt935T13FdQ09aG4NCxoqImv0DqyoDgNR3RwPfWttmlqordhHJkq6WkgpqqSqlY+aKMNe9slDUukBI694gE69ZC6jsVmhx61QW+nmq6iGHXdkrquSqmOrie2lkc57uvhqToNB1ALPR5qs5z+98T8nHbhQ2qnoq+/U3OYDbtpl9feIxCZIIjvTNpZJWAH8myUt11GgO7r3FjZDVWjIfbXr8dgFfhfSPHqm7Q2unIbUW5sURqt1jQC5p0LnaDtgHHiDquzUXuaOp2ZZ/eWTkfJK/Gc7ynad7X8dJX01TszlgYLVTbjZpucnAa0Bo3nabjeGvcHc0X8yi82y65ds+ye55PU4/hM2K9iUN6Zb4KiCGvbLpNFIKiCRsTnMa0B2jSeac3XrC65ReGpz6/vmoOw7HrNjuz6mZj90qrxaauonrYamrp2U5Jkkc525EyONrGb28QAwDQ8OBCtORxmppqKlafytRcKSNg7v8AbscT+5rXO/cpSSRsTHPe4MY0Euc46ADvlZGH2qS93SG+TRvit9M1woWP4GdzgAZ9O4A3eazXrD3HTQtK904sxraur49UffVtLtUWreS+IiL5ZhiIiCHwn3rdfKdT6asarmE+9br5TqfTVjW/gfVrfcpq86RERdyIiIgIiINQ8rr5su0nyLP9yuWyX5KsM8i0X4DFTeV182XaT5Fn+5XLZL8lWGeRaL8BiC2IiICIiAiIgjsj+L108Vl9ArFx74AtnisXoBZWR/F66eKy+gVi498AWzxWL0AsDH+np7vmto3JBERcKYiIgr99we232pdVkTUNwLQ01tE/m5XAdQdwIeB3A4HRQr9nVzDjzWUVG53BLRwuI/eA37lekXZbxl+3GjFWz3xE/GJWU3K6YyiVE9ru7/Sh/mEfrT2u7v8ASh/mEfrV7RW+UMRxj9tP0T19ztKJ7Xd3+lD/ADCP1p7Xd3+lD/MI/Wr2ieUMRxj9tP0Nfc7TnnOLpkOKba9mWERXlk9LlrLm6eqfRsD4OxYGyt3QDod4u0Oq2X7Xd3+lD/MI/Wta7ZPne8nf9lkX/CYuhVZXj78RTlMbuzTxn3PNfd7Sie13d/pQ/wAwj9ae13d/pQ/zCP1q9oq/KGI4x+2n6Pdfc7Sie13d/pQ/zCP1oNnd1PA5RJp/20MQP++qvaJ5QxHGP20/Q19ztKlQbNrfFKya51FTfZWOD2try3mmuHURExrWajrBIJB7qtqIuS7euXpzuTn99UdSqqqapzmRERUoiIiCHwn3rdfKdT6asarmE+9br5TqfTVjW/gfVrfcpq86RERdyIiIgIiINQ8rr5su0nyLP9yuWyX5KsM8i0X4DFTeV182XaT5Fn+5XLZL8lWGeRaL8BiC2IiICIiAiIgjsj+L108Vl9ArFx74AtnisXoBZWR/F66eKy+gVi498AWzxWL0AsDH+np7vmto3JBERcKYiIgIiICIiAiIg562yfO95O/7LIv+ExdCrnrbJ873k7/ssi/4TF0Kra/No7vnLyBERVPRERAREQEREBERBD4T71uvlOp9NWNVzCfet18p1PpqxrfwPq1vuU1edIiIu5EREQEREGoeV182XaT5Fn+5XLZL8lWGeRaL8BipvK6+bLtJ8iz/AHK5bJfkqwzyLRfgMQWxERAREQEREEdkfxeunisvoFYuPfAFs8Vi9ALKyP4vXTxWX0CsXHvgC2eKxegFgY/09Pd81tG5IIiLhTEREBERAREQERR+RWg5Bj9ztba2qtzq2llphWUUpjngL2FvORvHFr266gjiCAUGitsnzveTv+yyL/hMXQq+Hu0/LtquC7T6m15Jm2Sy5PjNTPS09bNd6l0tPvjdc6GRz95jZGbp1BG80jXgvrLyVMOyfDtiljjzO93a+5NcG+yNZJeKuWplp3SBu7ADI4loYwNBaDpv757q7r9nV0Uzn970InOW3URFwpiIiAiIgIiICIiCHwn3rdfKdT6asarmE+9br5TqfTVjW/gfVrfcpq86RERdyIiIgIiINQ8rr5su0nyLP9yuWyX5KsM8i0X4DF8+P+pdBtCwjaGLhS5bkLcEyqkbEbVFcpm0MU0TGslhMIduBrmhknEdsXv7xVt/6Z1Bn+c3G65dkWY5JX4pZoha7dbKy61ElLJOWje/JueW7sUe6ANNAZGkabqD6DIiICIiAiIgjsj+L108Vl9ArFx74AtnisXoBZWR/F66eKy+gVi498AWzxWL0AsDH+np7vmto3JBERcKYiIgLDut4orFRPrLhVRUdMwgGSZwaNT1Ad8k8ABxJ4Be9VVRUNLNUTyCKCFhkke7qa0DUk/uWtIqifI6qO8V7HMe4b1JSSDTsSMjq0/vke6P17o4Dj3YbDa6Zqq2Ux4+6PvZyib7Nqbs5Qmptpb5nkW/HrlVRa6Ceo5uma79TXu3/wDNgXj7Yl3+iz/P4/UvBFrRZw8bNXE981fKYaUYS29/bEu/0Wf5/H6k9sS7/RZ/n8fqXgi91WH9lHOr+R0S20BtY2AQbU9v+I7R6nHhBFbN110t5qY3eyDojvU5J6uB0a7UHVjWtXQ3tiXf6LP8/j9S8EU6qLNURE242e+r+R0S09/bEu/0Wf5/H6k9sS7/AEWf5/H6l4IoarD+yjnV/I6JbZTNo9dFoajF63c7ppqiGQj9xc3X9ysVgyq25KyTsKcmeLTnqaZjopotereY4BwB0Oh00OnAlVNYtZROmkiqaaXsS4wamnq2tBdGT1gj85p7rTwP1EAiqvC2LkZUxozxjPL9YnPw8VdeEpy/JO1s1FEYrfxkdnZVOj5ipY90NRBrrzcrTo4fWO6D3QQe6pdYVdFVuqaKt8MuYynKRERQeCIiCHwn3rdfKdT6asarmE+9br5TqfTVjW/gfVrfcpq86RERdyIoq/ZNbcahifX1AidMd2GFjS+WZ3eYxupd9eg4dZ0CZPf48Zss9e+J1Q9ujIoGHR00jiGsYO9qSOPcGpPALXlLSymolrq6UVd1qAOfqd3QaDqYwfmxt14N/WSS4ucbIimKdOvd8fv793Xh8PN6c53JuTafVyHWkxiufGeIdUzwxE/uDnEfvXn7Zd2+isnn8fqWEia6n2ceP1anQrTXPKVxWp5Q+yi44nUY52FWl7Kq310lXG8U1Qzqdp3i0vYfqee7op7YvBLsW2Y2DDrZi75YbZThklR2bG0zzEl0spHc3nlx01OgIHcVoRe66n2cf+31OhWmb7Zd2+isnn8fqXrFtQnicDW41cIouGslNJFPu/WWhwcR+oE/Uo1F5rqfZx4/UnBWl+sl/t+R0fZVuqm1MIduO0Ba6N3da9pAc1w/uuAKkFqeRtRbaz2Vte6y5RtDXN4BtVGDrzT/AKjx0d1tJ1HAuDtl2W709/tNJcaUuNPUxiRgeNHN1/NcO44dRHcIISqImNOjd8GVfsTZn3M1ERVuVHZH8Xrp4rL6BWLj3wBbPFYvQCysj+L108Vl9ArFx74AtnisXoBYGP8AT093zW0bkgiIuFMREQVLapIW4XURdbKmppKSQHqMctTHG8H9bXEfvUSrTmdlkyHGK+hgLW1T2CSnLuoTMcHxk/Vvtaqbba9lzoYapjXxiRupjkGjmHqLXDuEHUEd8LfwcxOGyjfFU+MRl8JamDmMphrnKM6yy67RqnDsKgs8M9tt8VwuNyvbJZYmc697YoWRxuaS4iN7i4u0A04EqlXbp27lDubZDj0V/OD0jq2SvE8lKHisqNWxtaWvILuAc4jQDXQngr9l2yy612b9LcUyjoveJ6NlBXNmoG1tPVxMc50ZMZewte0vfo4O6joQVnWHZzV2zPmZVXXx10rTj8FkmDqVsRmfHNJKZyWnQbxk03A3QadfcXS6Zpqqnbxa1t+0es2hZHsMvlRarbHR3p9aXRSCZ1TRVcdJUb7ontkDCw7hbo9juB14HTSP2Z59ccN2EYTb7DR09fkmQXu4W63x1r3Np43dmVUkkspb2xYxjHEhvEnQDvq+YrsJ6M0ezaD2c7J6HVFbUb3Ym52Z2RFMzT3Z5vd57XXttd3ua8I5vJ2npMVp7RQ5ZPST2i9yXrHq4UTHPtxkdI58Mg3tJ2HnpQdd06OA7i9Qim5v6/8AX0l4bFG35m2fau3JZLdNdRFZw+S1Mkjge3mZt0hry5zTp1jeP61fdrm0SLZXgFyyN9K6ulpzHDBSt3vys0sjY429qHHTeeNdAToDoCeCrGP4lf8AZfeskyi4TV+0K65C6jhlp7PQU9GacQRyNDgJahrS07wHui4HTrB4ZOSU7ttWPXHEr3h+TY1RVkYe251MlEOYlje18b2GKokcHh7Wkdrpw4rxOM6aJpjftUWj5QGYUlDkxrrVT3A0WP112prjT2G52+mgngj32wTtqmt3w7uOY8E7jgQ3UFWC0bXsns1+xkZfDZW2bI7RVXSB1qZMJaEwRMmcyRz3ESgsee2a1nFvUp4bNctu+IZPYsmztl7Zd7VNbIZIrNHStpzJG5hmc1shMju24gOa3hwA1WTXbIYLlcsGnqa8S02N2+qt8tMYOFa2anZCTrvdpoGE6dtrrpqNNV68iLn33tQ37Ks2zsbIMlvNJZbdjV3yqirLfQ0/OuroY3wTuhMshO44uYSSGtbukganiuoFpG0cnu/W1mH22fPX1+NYrc4a620E9pYJxHEx7I4ZJxIN7da/QODRwHEHhpu5eSnaiqM5qe2z+QxZVk1K3hE6OkrDp/6jxLGT+vdgZ/kFfVTNm1G6WG53p7S0XKZvY+o0PY8bd1h/U53OPB/uvarmsbHzE4irLqiI/WIiJ8WNemJuTMCIi4FIiIgh8J963XynU+mrGq5hPvW6+U6n01Y1v4H1a33KavOkREXcioO06UyXbFqVw1iNTNUEHqLmQuDfxCf3a9wKPU5tPoJH2mjusTS91pqOyZGtbvOdCWOZJoPqDt/hx7T9xgmuD2hzSHNI1BHUVZd20UTHCY/XOZ+Ew3cFMTbyadsu1PJ75thvmKvlx6xwUE746e2XKOcXGtgEIc2rhdvBkkZedC1o1aGu1cDprqqnrrzcrBs2kt1LZaDIBtBukTtxlR2E6dra1skpY6R8nbEOduh+mp0G6OrdV92R3jKs9s95vGWNq7LZ7n7KW+1x2uOKeGQMcwRmpDtXR9sSW7oJ4AngFEy8n6thstmprdlXYFwtWR1uRU9Ybc2VpfUGc806MycWt58gnXU7vDdJ1HOtmmud/wB7lG2gZ9fb7banFcqpKCDIseyzHXyVFqL+xaqCerY6KRjXkuYe0e1zSToW8CQVO7Qc6uuCbTNo91ZQWiprbTg7LpbqkxTiQxtmm0gnHO7jhzjHu1Y1h0cBrwU5Vcnia7Y/kPsnlc9Xl95rqK4Pv8dEyNsElI5rqZsdPqW82zQ9qXEnfcS7U8JDNdiE2bVmSVdTf2w1F7xNuMyObRaiNwkleagDnBrqZf7Phpp7riho17/vrRR2v5RiF9p4sto7TUUFxx+tvtI2zNlEsBpmxvkgeXuIk1bKNHhrOI9zxWNgu1XObrm2C0d+ZjzbRlloqLvTstMUz5qcMZE5scj3Sbp4TN/KBuhII3RwKueQbNX1mQY1fo7i9r8ftFdQNpo6VsjqgzshAcN52gLeZ9yQQ7e0JGnHSPJyx++YdndspKDG6k2qamlgudwuuLS2meiY1pfG2OV9RI2QOl0BjiG5x3hpoF4TNdNUR1f6dWqW2USkWy9Uo4RUt1mZGB1APayZ3/ulcoWqqYqOmlqJniOGJhe95/NaBqSrXs5tE9pxiN9XE6GtrZZK2eJ40dGZHatY4d9rNxp+tpXVb2W6pnry5/fxUY6Y0IhZ0RFUxEdkfxeunisvoFYuPfAFs8Vi9ALKyP4vXTxWX0CsXHvgC2eKxegFgY/09Pd81tG5IIiLhTEREBU7I8QqWVc1zsbYjUTHeqaKZ5ZHOdAN9rtDuP0He0dw1090Liivs3q7FWlT/iU6a5onSpaomyBlC8x3CguVtlB0Laiikc390jA6M/ucV49MbR+kv+wk/lW3UWnGPt9duf0q/wAT8XZGMq64ai6Y2j9Jf9hJ/KnTG0fpL/sJP5Vt1F70+17Of3R/F70yrg01NtDx2mrKaklubIqqp3uYgex4fLujV260jV2g4nTqWR0xtH6S/wCwk/lVW2yfO+5O/wCyyL/hMXQynVjbNMROrnb/AHRx/wCp02rg1F0xtH6S/wCwk/lTpjaP0l/2En8q26ih0+17Of3R/E6ZVwalZlNFNwpo62seeplLQzSE/wCTOH71MWzFrjkrgbtSm2Wg+6o3vBqKkd5+6SGM74BJd1Hd4g7CRVV4/Z/SpynjM5z+myPvdkrrxVdcZRsfxjGxsa1rQ1rRoGgaABf1EWS4xERAREQQ+E+9br5TqfTVjVcwn3rdfKdT6asa38D6tb7lNXnSIiLuRFru74XXY9I6Sx04rrUTr7GtcGSU3fEJPBzO6GEjd4hpI3WN2IinTXo7JjOJ6ltu7VanSpackyakpju1cNdQyd1lTQzMI/eW6H9xK8+mFp/SJPN5P5VudFP+j2Z5x/F39Pq7LSVZnlit9LLU1VeKanibvSTTRPYxg75JboAv3Dm1mqImSxVZkie0OY9kMha4HiCDu8QszldfNl2k+RZ/uVy2S/JVhnkWi/AYn9Dszzj+J0+rsqJ0wtP6RJ5vJ/KvWLIoashlFSXC4SHQBlNQyuB/W4tDQPrJAW4kT+j2Z5x9Hk4+vqpUPHsMrLjUw11+hZTwxObLBa2vDyHg6tfM4HdcQQNGN1aCNdXHd3b4iKFVc1e6ODguXKrlWlVIiIoK0dkfxeunisvoFYuPfAFs8Vi9ALKyP4vXTxWX0CoawX+2MsVta640jXCmjBBnbqO1H1r578Qqim/TnPVPxXUbk+ij+kNr8JUf27PWnSG1+EqP7dnrWfrKOMJ5JBFH9IbX4So/t2etOkNr8JUf27PWmso4wZJBFH9IbX4So/t2etOkNr8JUf27PWmso4wZJBFH9IbX4So/t2etOkNr8JUf27PWmso4wZJBFH9IbX4So/t2etOkNr8JUf27PWmso4wZNE7ZPnfcnf8AZZF/wmLoZc3bYbxQScrjk9ysrqZ0UcWQ77xM0tbrRM01OvDVdA9IbX4So/t2etXXK6dGjb1fOUYSCKP6Q2vwlR/bs9adIbX4So/t2etU6yjjCWSQRR/SG1+EqP7dnrTpDa/CVH9uz1prKOMGSQRR/SG1+EqP7dnrTpDa/CVH9uz1prKOMGSQRR/SG1+EqP7dnrTpDa/CVH9uz1prKOMGSQRR/SG1+EqP7dnrTpDa/CVH9uz1prKOMGTHwn3rdfKdT6asarOBzR1FDc5Intkjdcqktew6gjf7hVmX0eB9Wt9yirzpERF3IiIiAiIg1Dyuvmy7SfIs/wByuWyX5KsM8i0X4DFTeV182XaT5Fn+5XLZL8lWGeRaL8BiC2IiICIiAiIg/jmh7S1wDmkaEEcCFH9HLT4LovN2epSKKFVFNXnRmI7o5afBdF5uz1J0ctPgui83Z6lIoo6q32Y5Pc5R3Ry0+C6LzdnqTo5afBdF5uz1KRRNVb7McjOUd0ctPgui83Z6k6OWnwXRebs9SkUTVW+zHIzlHdHLT4LovN2epOjlp8F0Xm7PUpFE1VvsxyM5R3Ry0+C6LzdnqTo5afBdF5uz1KRRNVb7McjOXNW2Oy2+PlecnmFlBTNiliyLfjbC0NfpRM01GnHRdCdHLT4LovN2epaH2zfPC5On7LI/+ExdEr3VUT/xjkZyjujlp8F0Xm7PUnRy0+C6LzdnqUii81VvsxyM5R3Ry0+C6LzdnqTo5afBdF5uz1KRRNVb7McjOUd0ctPgui83Z6k6OWnwXRebs9SkUTVW+zHIzlHdHLT4LovN2epOjlp8F0Xm7PUpFE1VvsxyM5R3Ry0+C6LzdnqTo5afBdF5uz1KRRNVb7McjOXlTUsNHEIqeGOCMcQyNoaP8gvVEVkRERlDwREXoIiICIiDUPK6+bLtJ8iz/crlsl+SrDPItF+AxU3ldfNl2k+RZ/uVy2S/JVhnkWi/AYgtiIiAiIgIiICIiAiIgIiICIiAiIgIiIOdts3zwuTp+yyP/hMXRK1ztg2G2XbCy11VTcLtj+Q2fnjar/Yq19LV0RlaGybpadC1waA4EHUajhqVqvp7tm5PI5vObO7azhcPAZNjcAjulMwd2opNdH6Drcw8ACXOJ4IOmkVN2ZbYcO2x2X2Uw+/0l5p26c7HE7dmgJ/Nkido9h/8gNe4rkgIiICIiAiIgIiICIiAiIgIiICItG7QuVnjePX5+K4bQVm0zOOIFkx3SRkB6taio4siaDwJ4lvdAQSnK6+bLtJ8iz/crlsl+SrDPItF+AxaSbsB2h7d3Nq9tWT+xuPvIc3AsUmdDTFv92qqQd+Y99rTugjVrh1LpC2W6ms9upKCjiEFHSxMghibroxjQGtaNe8AAgyUREBERAREQEREBERAREQEREBERAREQEREGmNpvJVxHPb10mtElZgmcMJdFkuNydjTucf/AFmjtZgeGu8NSOG8Aqb7b21fk/nmNqmPHOsTi4DNsTp/y0LP71XR/m98uZ2oHVvFdMogrOz/AGmYttUsTLzid9o77bnaAyUsmroyfzXsOjmO/wC1wB+pWZaM2gckzG79fn5VhdwrNmWccXC9Y7pHHOdddKin4RytJ4kcC7ukqts297Q9hTm0u2jF/ZPH2HdbnmJwumpg3+9V0wG/D9bmjd1OjWlB0uireP7ScVyrEn5RacittdjscTp5bnHUs5iFjW7zzI4nRm6NS7e03dDrooTYjtvxjb/g8WUYtLP2IZXwTUtY1rKimkb1ska1zgCQWuGhIIcEF/REQEREBERARa+x3bxhuV7Wr/s3tdydV5PZKNtZWRsicYWjf3HsEnUXxl0W8O5zrQCS2QMhdq/KdwrZVcWWR89TkuYTndpsYx+I1dfK7TgCxvCPv9uRw4gFBttaa2n8qnENn156N2xtXnGbyEtixnG4+yqkO/8AylvaxAcNd46gcd0ql9Bds/KE/KZteHbJMLl49G8cnEl2qYz3Kir00j1HW1g4gkOaDxW5dmGxvDdjVm9jMQsNLZ4Xac9LG3enqCO7LK7V7z/5E6dzRBpr2qNrfKB/K7T7/wC19iEvHoXitRrUzsP5lXWd3XqLY+1IP5pW8dnuzHFdlNhZZsSsVHYre3QujpY9HSEfnSPOrnu/7nEn61aEQEREBERAREQEREBERAREQEREBERAREQEREBERAREQF+XsbI0tcA5pGhBGoIX6RB83/8AqK2LCNizbezBHVWJZhlIlbd6GwVnY1JVW7dc1/ZNOxw05x7gG6N3Xhk+upC5w5I/KTr+TjtKhrnvknxa5OZT3miZx3otTpK0f349SR3wXN4b2o+1FXQ01fGI6qniqYwd4MlYHAHv6H9ZWJ0btHgui83Z6kGHVZtaIMbor5DUivoa+OOShNJo91YHt32CIfnbze214ANBcSGgkU6sveSXp5dJcBZKc67tNb2tkk07m9LI06nu9q0d7U9Z8KivdkWSXC5SEugppZKCjaRwYxjt2Rw+t72nj3Qxne1OFleT27CsbuV+u03Y9tt0D6mokDS4hjRqdAOJPcA7pV1VWpnRpjb19f6fp8W1h8NRFGncexoLgTqckvRPf7KA+5q/TKe705DqfKLvE8dRkfFMD+sPjKgLJn8t4dZWy4rkNrNzfMwdm0sYFKI2b4dOWSOEYf1N7pPAgK2KHSLvHwh2RatTHmxySNmzyst07KfIhTmmeQ1l1pwY2Bx6hKwk7mvVvhxBJ4hvDXXHLT5SrOTrsx3red7Lb4JKa0MLdWxFobzs7teGkYe3QcdXObwI3tLpLEyaN8cjGyRvBa5jhqHA9YIUtgLoLrQ1douUEddNaZubhkqYw8mB4Dozx16uLNe7zepUs4uUzVEZTG/6svFYeLcadG58OcWzS5Y1mVPkUdxucVcJnvqKu31pp6yRkgLZgychxY97HvG+Q7Te10PUvtlsG2V7OcDw23XPZ9aoGUN5pIq1t3kY51XXRStEjZJJJBzh3g4O3TpoT7kdSvnRu0eC6LzdnqUkqmaIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiINNYox0Vn5p/9rFU1Ecn/AJtmeHa/XqCtZ8r2z0V25PGYOraWKpNJTCpgMrdeblDgA9veIDiNfrK3LlFsOL36esI3bRc5Q4yaaNp6k6NIce4JO13SeG/qNdXsBwrtaKG/W6e33Oip7jQVDdyalq4myxSN7zmuBBH61Zf23JudVW377n0luYvWco4ZNMZJhNhxLbrsbbZrTSWxsUF1pGCmjDN2FtOXtjGn5odI86d9xWr8Kxi22TZdsoy+ipzBks+XwUU1yEjjNJTyV00D4CSf7Pc0G57kaa6arreptFDWV9HXVFFTz1tHv9jVMkTXSQb43X7jiNW7w4HTrHWsWPErHDbqO3x2W3soKOdtVTUraWMRQTB5eJGN00a8OJcHDjqSetc2b2bWczMfe76JVZWzxjnZrkkrf7IUVDESO68PqXEfua9v+YUbXVrKCHnHtfI5xDI4Ym70krz1Na3uk/8A+4K6YPj0uP2mQ1e6bjWTOqqos6g4gBrAe6Gsaxuvd3de6uq3+WiqqevZ4xPyc+NriLeh1ysSIiqYQiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIg8qmmhrKeWnqImTwSsLJIpWhzXtI0IIPAgjuKkVuzWppXl1kvDqaEkkUlwiNTG36mu3mvA+olw7g0Gml8RWU3KqNkbuayi5VbnOmcnPW2DJsi2P2ix19XFa7ky6XqksrGwc4wsfO4tDzrrwGnEK/MwbKZyGyXS0Uje6+OllmP7gXtH+ev7+pa+5bXxJ2f8A+OrN+I5dEKet4UxyX9LvcVbxzBqKw1PZss09zue7u9mVZGrAesRsaA1g/UNTw1J0VkRFXVVNc51OaqqapzqkREUERERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQc78tr4k7P8A/HVm/EcuiFzvy2viTs//AMdWb8Ry6IQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREHO/La+JOz/APx1ZvxHLohfInlcbZttWJbTrhguV5bJc6CyXWK7WmWS2UcXOBhL6afVkLd4gO4ji3eDgQdF2tyB882o7VdnN2y/aLfvZiir6psFmjNDT05YyLfE0msLG7wc8ho110MLu/xDqBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQUWn2vWurhbNT2y8Twv4tkZR6tcO+OK9PbWofA978y/qqfgvxRtX7EfeVPLmv4ym1dqtxb3TMb56pfLXfxi7RcqoiiNkzxSXtrUPge9+Zf1T21qHwPe/Mv6qNRUeUI9nHOVXlq72I8XNfLa2MxcpA4rc7BbLhQ3+31Ao6uoqqMta6hedXHgSXOjdq5reGoe/jroujsOyrH8ExS047Z7Beqa2WumjpKePsLUhjGgAk68SdNSe6SSvZE8oR7OOcnlq72I8Ul7a1D4HvfmX9U9tah8D3vzL+qjUTyhHs45yeWrvYjxSXtrUPge9+Zf1UnjWc0OUV9TRwU1bS1NPEyZzKyDm9WuLgCOPHi0qtL9YT8oN18l0/4sy6sPiacRNVOhllGe+fc78F+JV4q7q6qYjm2MiIrm+IiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiDTOC/FG1fsR95U8oHBfijav2I+8qBn22YzTTyRPhyIvjcWO3MXubhqDpwIpyCPrHBYeLiZxNzLtT8X5zeoqqvV6MZ7Z+K+LRu0HlR23D8svFkoqaz1r7KGivNzyKmtsrnlgfzdPFJqZnBpGpO43U7upIOl0ftvxiN7mmHIyWnQ7uLXQj/ADFNxVOgwTMLXk1/yLBH2CssuWPiuUlNlFPUwT0NRzTWOe1gZvODmtaTG/cII01CpopiJ/PCVq3TTMzdp7s848e5lR7frjkl17Ew3EOkEbrBR5DHPVXJtGHQ1HO6Rkc28iT8mNBxBJdq5mg3vcbf3ZDSYjHhuOS5HecitnsyKKesbSR0VJ2rS+aUtfod924A1p1IPUBqrDacDuFDtZvuUSS0nYFfY6O2xxRFwkbLFJO55LdNAzSVumjieB4Du64w/Ydm+zajwW5Y/WWGqv8AabD0futJcJJm0lRDzvOsfFK2Mva5ri7rZoQ49WilGhKyIsT1cOueHX+vct3Jtv12yTBLpWXp9Sa/pBdI3Q1VSZ3U7W1cgEIfqQWsA3Rpw0HDgtrLTmz6pOxDHJ7Vlz56q63G6V91L8ftFfXU4bPUPkA3o4XbpG97lx1+s9asft5Yv/6OSf8A7Vun/wDWUK6ZmqZpjYpu0VV3KqqKdmfU2Av1hPyg3XyXT/izKu4pnVrzM1QtrLkzsbd5z2QtNVQ672um7z8TN/3J13ddOGumo1sWE/KDdfJdP+LMtDARMXKs+zPxho/hUTTispjqlsZERab7UREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREGjcKvdugxW2xyV9LHI2IBzHzNBB1PWNVNdILX4So/t2+tbEdi9mc4k2ihJPEk0zOP+ydFbL4HoPNmepU3cLYu3KrkzO2Znq63z9z8HouVzXpztnPd/lrvpBa/CVH9u31p0gtfhKj+3b61sTorZfA9B5sz1J0Vsvgeg82Z6lV0Kx2p8FfkSj2k8v8ALXfSC1+EqP7dvrTpBa/CVH9u31rYnRWy+B6DzZnqTorZfA9B5sz1J0Kx2p8DyJR7SeX+Wu+kFr8JUf27fWnSC1+EqP7dvrWxOitl8D0HmzPUnRWy+B6DzZnqToVjtT4HkSj2k8v8td9ILX4So/t2+tZmz+tp67P7s+mniqGi2U4LonhwB52bhwV46K2XwPQebM9SyaK00NtLzR0VPSl+gcYIms3v16BdFmxasTNVMzMzGXU68L+G04W5rIqz/RloiKbZEREBERAREQEREBERB//Z", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from IPython.display import Image, display\n", - "\n", - "# Setting xray to 1 will show the internal structure of the nested graph\n", - "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[36;1m\u001b[1;3m[0:tasks]\u001b[0m \u001b[1mStarting step 0 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3m__start__\u001b[0m -> {'name': 'test'}\n", - "\u001b[36;1m\u001b[1;3m[0:writes]\u001b[0m \u001b[1mFinished step 0 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", - "\u001b[36;1m\u001b[1;3m[0:checkpoint]\u001b[0m \u001b[1mState at the end of step 0:\n", - "\u001b[0m{'name': 'test', 'path': []}\n", - "\u001b[36;1m\u001b[1;3m[1:tasks]\u001b[0m \u001b[1mStarting step 1 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mgrandparent\u001b[0m -> {'name': 'test', 'path': []}\n", - "\u001b[36;1m\u001b[1;3m[1:writes]\u001b[0m \u001b[1mFinished step 1 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent']\n", - "\u001b[36;1m\u001b[1;3m[1:checkpoint]\u001b[0m \u001b[1mState at the end of step 1:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'}]}\n", - "\u001b[36;1m\u001b[1;3m[2:tasks]\u001b[0m \u001b[1mStarting step 2 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mparent\u001b[0m -> {'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'}]}\n", - "\u001b[36;1m\u001b[1;3m[2:writes]\u001b[0m \u001b[1mFinished step 2 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['parent']\n", - "\u001b[36;1m\u001b[1;3m[2:checkpoint]\u001b[0m \u001b[1mState at the end of step 2:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'}]}\n", - "\u001b[36;1m\u001b[1;3m[3:tasks]\u001b[0m \u001b[1mStarting step 3 with 2 tasks:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mchild\u001b[0m -> {'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'}]}\n", - "- \u001b[32;1m\u001b[1;3msibling\u001b[0m -> {'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'}]}\n", - "\u001b[36;1m\u001b[1;3m[3:writes]\u001b[0m \u001b[1mFinished step 3 with writes to 2 channels:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", - "- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n", - " {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n", - " {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n", - " {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'}], ['sibling']\n", - "\u001b[36;1m\u001b[1;3m[3:checkpoint]\u001b[0m \u001b[1mState at the end of step 3:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n", - " {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n", - " {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n", - " {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'},\n", - " {'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718', 'val': 'sibling'}]}\n", - "\u001b[36;1m\u001b[1;3m[4:tasks]\u001b[0m \u001b[1mStarting step 4 with 1 task:\n", - "\u001b[0m- \u001b[32;1m\u001b[1;3mfin\u001b[0m -> {'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n", - " {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n", - " {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n", - " {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'},\n", - " {'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718', 'val': 'sibling'}]}\n", - "\u001b[36;1m\u001b[1;3m[4:writes]\u001b[0m \u001b[1mFinished step 4 with writes to 1 channel:\n", - "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['fin']\n", - "\u001b[36;1m\u001b[1;3m[4:checkpoint]\u001b[0m \u001b[1mState at the end of step 4:\n", - "\u001b[0m{'name': 'test',\n", - " 'path': [{'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49', 'val': 'grandparent'},\n", - " {'id': '2a6f0263-3949-4e47-a210-57f817e6097d', 'val': 'parent'},\n", - " {'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088', 'val': 'child_start'},\n", - " {'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783', 'val': 'child_middle'},\n", - " {'id': '669dd810-360f-4694-a9f3-49597f23376a', 'val': 'child_end'},\n", - " {'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718', 'val': 'sibling'},\n", - " {'id': 'a4328c5f-845a-43de-b3d7-53a39208e316', 'val': 'fin'}]}\n" - ] - }, - { - "data": { - "text/plain": [ - "{'name': 'test',\n", - " 'path': [{'val': 'grandparent', 'id': '79a81f03-d16d-4d12-94a6-4ba29fc9ce49'},\n", - " {'val': 'parent', 'id': '2a6f0263-3949-4e47-a210-57f817e6097d'},\n", - " {'val': 'child_start', 'id': 'd1c1bab0-6e19-4846-a470-e9cc2eb85088'},\n", - " {'val': 'child_middle', 'id': 'e0fcb647-1e9e-4ae4-b560-0046515d5783'},\n", - " {'val': 'child_end', 'id': '669dd810-360f-4694-a9f3-49597f23376a'},\n", - " {'val': 'sibling', 'id': '137dbc2f-b33c-4ea4-8b04-a62215ba9718'},\n", - " {'val': 'fin', 'id': 'a4328c5f-845a-43de-b3d7-53a39208e316'}]}" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "graph.invoke({\"name\": \"test\"}, debug=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.8" + "cells": [ + { + "attachments": { + "71516aef-9c00-4730-a676-a54e90cb6472.png": { + "image/png": "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" } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to create subgraphs\n", + "\n", + "For more complex systems, subgraphs are a useful design principle. Subgraphs allow you to create and manage different states in different parts of your graph. This allows you build things like [multi-agent teams](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/), where each team can track its own separate state.\n", + "\n", + "![Screenshot 2024-07-11 at 1.01.28 PM.png](attachment:71516aef-9c00-4730-a676-a54e90cb6472.png)" + ] }, - "nbformat": 4, - "nbformat_minor": 4 + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langgraph" + ] + }, + { + "attachments": { + "9145adc1-ce9d-4a22-8183-e13796d4a388.png": { + "image/png": "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gAACCCCAAAII1LMAQfp6BqZ6BBBAAIHGK6CBzaefftoO4Ne//nW5gVx99dU2QN+mTZty2/XD2rVr5bvvvhNNSRMeHi5Tp06VoKCgcsdt3brVBlg14K+B4/vuu8+mqXnhhRckICCg3LFVfcjJybGHxMfXfDFBDfLecsstkpJSuhiiVqQL4mrffVwWQHT6MGvWLHnggQckMzPTbho2bJhon9u2bescYuv685//bIP+ulHT7+gxNb2ZsWfPHvn0009l3759tv7zzjuvxkF1XVD3F7/4hXzxxRdl/dOUQDpGZ7Fd3aGGr776qlx66aXSvXv3smOdfXp8ZR76HXG2640ODfrr4r0Vi+tx+j45OfkUj4KCAjteTavUsmVLGTBggKhvxaL79Tulixc7Rcf0t7/9TZYtW2Zvrriul3DkyBEZO3as5Ofn2+/lmW6sOPXxigACVQuk52SImH+fYyIbdm2SyUMny5uzXpcFJlA/YcC4qjvqpUc4f/Z66fAYFgIIIIAAAgg0MQHS3TSxC85wEUAAAQSqL6ALsGogVNPdtGrVqtyJN9xwg00tUzEwrovLXnzxxfLEE0/Ic889J/fff79dxPXAgQPlztf9Wm9qaqrcfPPNsmXLFvn666/l448/LndcdT5oznwtsbE1T7fw3nvvlQXodUw6k37lypXyySefnHKzQJ8quO2222yA/sorr5S+ffva2em6gK1TNN3OFVdcYQP048aNk8mTJ9tUQepQk6Kz3s855xz561//Ks8//7w8++yzctNNN1nzmtSj43EC9PrkgS6w66QE6tq1a1lVOl5tZ/r06WXb9I0+NTB69GhxxqhPF+gNDb25oYFvHaMG+7VevYlw2WWX2fP1vCeffFLy8vJk7ty59smLP/zhD3afjunCCy+Uxx9/3H7WX1qfWukaB5pGSdv70Y9+ZK9D2UEn3/zjH/+w30udPa/fsUceecQ+raBPeujCxnpDoX379mWnqYHeVNGnIlxvppQdwBsEEKiVQGauCdKb0j70h3/falVRDU9q16adjOg/RpK3r5XktE01PNubDuevst50NRkLAggggAACTV2A/7Np6t8Axo8AAgggcFoBDbBqiYysXhoDDdBrwHTIkCE2aKsznnXBWU1L8tRTT5Vrx5ktf9VVV4neDNDjtKxatarccdX5oEH6083grur86667Tnr06GEPe/31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wASYABPQm0CJ+H80FVNTg4wn0Hud3JAJMAEmwASYABNgAkyACTABJsAEmIDBOenJsf7RRx+hd+/euPXWW2tcwdOnT2Pv3r2gCHvSrD948GCNNtUrzp49i65du1avNohje3t7LFy4EHPmzMGnn34KctRzYQJMgAkwASZg6AQUuRvOG2voV5LtZwJMgAkwASbABJgAE2ACTIAJMIH6CBick/6///6TUfTPPPMMjLQegSd9doqIP3PmTJU1BwUFSTkYqly+fDnc3Nzg7OwMikKn/uXl5fD396/Sx9AOHn74YZDcDSWQnTdvHszMzAxtCWwvE2ACTIAJMIEqBEpKy+Wxidb/66s04AMmwASYABNgAkyACTABJsAEmAATYALthIDBOekjIyMl+k6dOlW5BOSkJwe9nZ0dFixYgFGjRqFXr17iMXlTdO7cGba2tnjyySer9Kl+cPjwYezZswfZ2dno1q0bbr75ZlhZWVVvpvfx5cuXERsbix49esDGxqbWfiS7c/78eSlT4+HhUaVNaWkp9u/fj927d2PHjh04cOAAZsyYgffee69KO1rbY489JqVvdu3ahdGjR1c5zwdMgAkwASbABAyNQEmZKnGssamRoZnO9jIBJsAEmAATYAJMoFEELibmobNn7b6DRg3InZgAE2ACTMBgCBick56c8FSOHz8uk8cqpMmhTg72/v3713CskyOcoufrKr///nsN+ZzQ0FB89913GDBggOwaHx+PdevWITU1VY43YcIEUJvqJT8/X94Q+PLLLzWnfvnllxra96SpTzcO4uLiZLvrr78eP/30kxx78+bNmDZtmnxqQBnE3d0dhYWFymGV7fTp06WT/ueff2YnfRUyfKAQoCSMMSkF8HezgpmBaTzPXxOG5MzKz37fIAfMHh+sLE2z1bedpkMDdvKLyhCfXgA3ews42vDTKg1Ax02ZQKMIKIljzTiSvlH8uBMTYAJMgAkwASbQtgkkZRXh+KVMnLyUhRPidT42Wxo8oIszPp7Vr20bz9YxASbABJhAkxMwOCf94MGDJYTPPvsML730EpycnDRQhg8frtnX3qFEsRRRr6tQxDpFqFOZO3cuhg0bBorMX7ZsGQYOHIjo6GhcuHBBRucrY9DNglWrVmH9+vXo16/yf6BFRUW47bbbsGXLFhnVTw7+nTt3Soc73UhQpGjIgf/QQw/J4R54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AQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD4BQfry9akWESBAgAABAgQIECBAgAABAgQIECBAgEBBBATpC9JRqkmAAAECBAgQIECAAAECBAgQIECAAAEC5RMQpC9fn2oRAQIECBAgQIAAAQIECBAgQIAAAQIECBREQJC+IB2lmgQIECBAgAABAgQIECBAgAABAgQIECBQPgFB+vL1qRYRIECAAAECBAgQIECAAAECBAgQIECAQEEEBOkL0lGqSYAAAQIECBAgQIAAAQIECBAgQIAAAQLlExCkL1+fahEBAgQIECBAgAABAgQIECBAgAABAgQIFERAkL4gHaWaBAgQIECAAAECBAgQIECAAAECBAgQIFA+AUH68vWpFhEgQIAAAQIECBAgQIAAAQIECBAgQIBAQQQE6QvSUapJgAABAgQIECBAgAABAgQIECBAgAABAuUTEKQvX59qEQECBAgQIECAAAECBAgQIECAAAECBAgURECQviAdpZoECBAgQIAAAQIECBAgQIAAAQIECBAgUD6B/w+jDwOxnklQVQAAAABJRU5ErkJggg==" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Simple example\n", + "\n", + "Let's consider a toy example: a system that accepts logs and perform two separate sub-tasks. First, it will summarize them. Second, it will summarize any failure modes captured in the logs. These two operations will be performed by two different subgraphs.\n", + "\n", + "The most important thing to recognize is the information transfer between the graphs. `Entry Graph` is the parent, and each of the two subgraphs are defined as nodes in `Entry Graph`. Both subgraphs inherit state from the parent `Entry Graph`; I can access `docs` in each of the subgraphs simply by specifying it in the subgraph state (see diagram). Each subgraph can have its own private state. And any values that I want propagated back to the parent `Entry Graph` (for final reporting) simply need to be defined in my `Entry Graph` state (e.g., `summary report` and `failure report`).\n", + "\n", + "![Screenshot 2024-07-12 at 10.35.41 AM.png](attachment:9145adc1-ce9d-4a22-8183-e13796d4a388.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define subgraphs" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Optional, Annotated\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import StateGraph, START, END\n", + "\n", + "\n", + "# The structure of the logs\n", + "class Logs(TypedDict):\n", + " id: str\n", + " question: str\n", + " answer: str\n", + " grade: Optional[int]\n", + " feedback: Optional[str]\n", + "\n", + "\n", + "# Define custom reducer (see more on this in the \"Custom reducer\" section below)\n", + "def add_logs(left: list[Logs], right: list[Logs]) -> list[Logs]:\n", + " if not left:\n", + " left = []\n", + " \n", + " if not right:\n", + " right = []\n", + "\n", + " logs = left.copy()\n", + " left_id_to_idx = {log[\"id\"]: idx for idx, log in enumerate(logs)}\n", + " # update if the new logs are already in the state, otherwise append\n", + " for log in right:\n", + " idx = left_id_to_idx.get(log[\"id\"])\n", + " if idx is not None:\n", + " logs[idx] = log\n", + " else:\n", + " logs.append(log)\n", + " return logs\n", + "\n", + "\n", + "# Failure Analysis Subgraph\n", + "class FailureAnalysisState(TypedDict):\n", + " # keys shared with the parent graph (EntryGraphState)\n", + " logs: Annotated[list[Logs], add_logs]\n", + " failure_report: str\n", + " # subgraph key\n", + " failures: list[Logs]\n", + "\n", + "\n", + "def get_failures(state: FailureAnalysisState):\n", + " failures = [log for log in state[\"logs\"] if log[\"grade\"] == 0]\n", + " return {\"failures\": failures}\n", + "\n", + "\n", + "def generate_summary(state: FailureAnalysisState):\n", + " failures = state[\"failures\"]\n", + " # NOTE: you can implement custom summarization logic here\n", + " failure_ids = [log[\"id\"] for log in failures]\n", + " fa_summary = f\"Poor quality of retrieval for document IDs: {', '.join(failure_ids)}\"\n", + " return {\"failure_report\": fa_summary}\n", + "\n", + "\n", + "fa_builder = StateGraph(FailureAnalysisState)\n", + "fa_builder.add_node(\"get_failures\", get_failures)\n", + "fa_builder.add_node(\"generate_summary\", generate_summary)\n", + "fa_builder.add_edge(START, \"get_failures\")\n", + "fa_builder.add_edge(\"get_failures\", \"generate_summary\")\n", + "fa_builder.add_edge(\"generate_summary\", END)\n", + "\n", + "\n", + "# Summarization subgraph\n", + "class QuestionSummarizationState(TypedDict):\n", + " # keys that are shared with the parent graph (EntryGraphState)\n", + " summary_report: str\n", + " logs: Annotated[list[Logs], add_logs]\n", + " # subgraph keys\n", + " summary: str\n", + "\n", + "\n", + "def generate_summary(state: QuestionSummarizationState):\n", + " docs = state[\"logs\"]\n", + " # NOTE: you can implement custom summarization logic here\n", + " summary = \"Questions focused on usage of ChatOllama and Chroma vector store.\"\n", + " return {\"summary\": summary}\n", + "\n", + "\n", + "def send_to_slack(state: QuestionSummarizationState):\n", + " summary = state[\"summary\"]\n", + " # NOTE: you can implement custom logic here, for example sending the summary generated in the previous step to Slack\n", + " return {\"summary_report\": summary}\n", + "\n", + "\n", + "qs_builder = StateGraph(QuestionSummarizationState)\n", + "qs_builder.add_node(\"generate_summary\", generate_summary)\n", + "qs_builder.add_node(\"send_to_slack\", send_to_slack)\n", + "qs_builder.add_edge(START, \"generate_summary\")\n", + "qs_builder.add_edge(\"generate_summary\", \"send_to_slack\")\n", + "qs_builder.add_edge(\"send_to_slack\", END)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that each subgraph has its own state, `QuestionSummarizationState` and `FailureAnalysisState`.\n", + " \n", + "After defining each subgraph, we put everything together." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Define parent graph" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Dummy logs\n", + "dummy_logs = [\n", + " Logs(\n", + " id=\"1\",\n", + " question=\"How can I import ChatOllama?\",\n", + " grade=1,\n", + " answer=\"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\",\n", + " ),\n", + " Logs(\n", + " id=\"2\",\n", + " question=\"How can I use Chroma vector store?\",\n", + " answer=\"To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).\",\n", + " grade=0,\n", + " feedback=\"The retrieved documents discuss vector stores in general, but not Chroma specifically\",\n", + " ),\n", + " Logs(\n", + " id=\"3\",\n", + " question=\"How do I create react agent in langgraph?\",\n", + " answer=\"from langgraph.prebuilt import create_react_agent\",\n", + " )\n", + "]\n", + "\n", + "\n", + "# Entry Graph\n", + "class EntryGraphState(TypedDict):\n", + " raw_logs: Annotated[list[Logs], add_logs]\n", + " logs: Annotated[list[Logs], add_logs] # This will be used in subgraphs\n", + " failure_report: str # This will be generated in the FA subgraph\n", + " summary_report: str # This will be generated in the QS subgraph\n", + "\n", + "\n", + "def select_logs(state):\n", + " return {\"logs\": [log for log in state[\"raw_logs\"] if \"grade\" in log]}\n", + "\n", + "\n", + "entry_builder = StateGraph(EntryGraphState)\n", + "entry_builder.add_node(\"select_logs\", select_logs)\n", + "entry_builder.add_node(\"question_summarization\", qs_builder.compile())\n", + "entry_builder.add_node(\"failure_analysis\", fa_builder.compile())\n", + "\n", + "entry_builder.add_edge(START, \"select_logs\")\n", + "entry_builder.add_edge(\"select_logs\", \"failure_analysis\")\n", + "entry_builder.add_edge(\"select_logs\", \"question_summarization\")\n", + "entry_builder.add_edge(\"failure_analysis\", END)\n", + "entry_builder.add_edge(\"question_summarization\", END)\n", + "\n", + "graph = entry_builder.compile()\n", + "\n", + "from IPython.display import Image, display\n", + "\n", + "# Setting xray to 1 will show the internal structure of the nested graph\n", + "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'raw_logs': [{'id': '1',\n", + " 'question': 'How can I import ChatOllama?',\n", + " 'grade': 1,\n", + " 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\n", + " {'id': '2',\n", + " 'question': 'How can I use Chroma vector store?',\n", + " 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',\n", + " 'grade': 0,\n", + " 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'},\n", + " {'id': '3',\n", + " 'question': 'How do I create react agent in langgraph?',\n", + " 'answer': 'from langgraph.prebuilt import create_react_agent'}],\n", + " 'logs': [{'id': '1',\n", + " 'question': 'How can I import ChatOllama?',\n", + " 'grade': 1,\n", + " 'answer': \"To import ChatOllama, use: 'from langchain_community.chat_models import ChatOllama.'\"},\n", + " {'id': '2',\n", + " 'question': 'How can I use Chroma vector store?',\n", + " 'answer': 'To use Chroma, define: rag_chain = create_retrieval_chain(retriever, question_answer_chain).',\n", + " 'grade': 0,\n", + " 'feedback': 'The retrieved documents discuss vector stores in general, but not Chroma specifically'}],\n", + " 'failure_report': 'Poor quality of retrieval for document IDs: 2',\n", + " 'summary_report': 'Questions focused on usage of ChatOllama and Chroma vector store.'}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph.invoke({\"raw_logs\": dummy_logs}, debug=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Custom reducer functions to manage state\n", + "\n", + "You might have noticed that we defined a custom [reducer]([reducer](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers) function (`add_logs`) or the `logs` key in `EntryGraphState`. It is necessary to provide a reducer when using shared state keys across multiple subgraphs.\n", + "\n", + "Let's take a look at implementing a custom reducer. We will create two graphs: a parent graph with a few nodes and a child graph that is added as a node in the parent. We'll also define a custom reducer function (`reduce_list`) for our state. This is functionally equivalent to simply using `operator.add`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Annotated\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "# define a simple reducer\n", + "def reduce_list(left: list, right: list) -> list:\n", + " if not left:\n", + " left = []\n", + " if not right:\n", + " right = []\n", + " return left + right\n", + "\n", + "# define parent and child state\n", + "class ChildState(TypedDict):\n", + " name: str\n", + " path: Annotated[list[str], reduce_list]\n", + "\n", + "\n", + "class ParentState(TypedDict):\n", + " name: str\n", + " path: Annotated[list[str], reduce_list]\n", + "\n", + "\n", + "# define a helper to build the graph\n", + "def make_graph(parent_schema, child_schema):\n", + " child_builder = StateGraph(child_schema)\n", + " \n", + " child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n", + " child_builder.add_edge(START, \"child_start\")\n", + " child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n", + " child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n", + " child_builder.add_edge(\"child_start\", \"child_middle\")\n", + " child_builder.add_edge(\"child_middle\", \"child_end\")\n", + " child_builder.add_edge(\"child_end\", END)\n", + " \n", + " builder = StateGraph(parent_schema)\n", + " \n", + " builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n", + " builder.add_edge(START, \"grandparent\")\n", + " builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n", + " builder.add_node(\"child\", child_builder.compile())\n", + " builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n", + " builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n", + " \n", + " # Add connections\n", + " builder.add_edge(\"grandparent\", \"parent\")\n", + " builder.add_edge(\"parent\", \"child\")\n", + " builder.add_edge(\"parent\", \"sibling\")\n", + " builder.add_edge(\"child\", \"fin\")\n", + " builder.add_edge(\"sibling\", \"fin\")\n", + " builder.add_edge(\"fin\", END)\n", + " graph = builder.compile()\n", + " return graph\n", + "\n", + "\n", + "graph = make_graph(ParentState, ChildState)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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qGMrKWrlllkcWO5vd3f00jd1zSN06HU8V0Wz291ptVHRPrKm4Op4WxGrrHB0026NN95AALj0kgDj4FrO3Om8WX77Eq/wl6blL6w81QWW8VFS7gxk9vmpY9fjdJK1oA+M8T8QJ4JwNcarGbLa7Le8eg/4k/8APkUrWpxSyOx3HqG3vkE0sLP0kjRoHPJLnED4t4lbZefj1RXi11RyTM+aTyiIi4EEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBV35WnfTsG/vAofMkViFXflad9Owb+8Ch8yRBYhERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBV35WnfTsG/vAofMkViFXflad9Owb+8Ch8yRBYhERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEXh80cZ0e9rT8ROi89lQ/7aP+IK2kfVflrtx/pAKzM8qw+C4bNn2GvwrJo7rUUkl6510skG+x0B/0du4dSe67rTToK/UXsqH/AG0f8QX5nf0hXJprZ9tePZJilKKlubVMdvmij9zHcODQXEcGiRmjtfjjkcUtIuJyUuUhcuUzi94yCfDDilrpKltJSym5dl9lv3S6XQc1HuhmsfHjqXEcN0ruShOx/Z7aNj2zTH8PtcsRprXStifKNGmeU91LKR8b3lzv3qY9lQ/7aP8AiCWkfVF8uyof9tH/ABBfVLAiIoCIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAtBnV1qLNjNRPSSCGpkkhpo5SAebdLMyIPAIIJG/qNQRqOI0W/UT2od6Q/tG3emwLsZPEVY1ETyXjzWOWGpGA445v6eyUNbKTq6esgbPLI7hq5z3guc46DUkklPa+xb5tWfyCL1VsL/AH2hxexXK83Ofsa226mkq6qfcc/m4o2l73brQSdGgnQAk+AKJ4ftxwnO7zBarNeXS3CopzVU0NTR1FL2TCNCXwmWNolaAQSWE6L0OHxI/nO8vO1vPa+xb5tWfyCL1U9r7Fvm1Z/IIvVWhxXbtg2a5L2v2i+c/dnNkfFBNSTwCcM92YnyMa2Xd8O4XaDilg264PlFbV01svRqTR9kdlzmjqGU9KYHObKJZnRiOMt3SdHOBI0cNQQTOHxOnO8vO1vva+xb5tWf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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "# Setting xray to 1 will show the internal structure of the nested graph\n", + "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'path': []}\n", + "\u001b[36;1m\u001b[1;3m[0:tasks]\u001b[0m \u001b[1mStarting step 0 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3m__start__\u001b[0m -> {'name': 'test'}\n", + "\u001b[36;1m\u001b[1;3m[0:writes]\u001b[0m \u001b[1mFinished step 0 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test', 'path': []}\n", + "\u001b[36;1m\u001b[1;3m[1:tasks]\u001b[0m \u001b[1mStarting step 1 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mgrandparent\u001b[0m -> {'name': 'test', 'path': []}\n", + "\u001b[36;1m\u001b[1;3m[1:writes]\u001b[0m \u001b[1mFinished step 1 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test', 'path': ['grandparent']}\n", + "\u001b[36;1m\u001b[1;3m[2:tasks]\u001b[0m \u001b[1mStarting step 2 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mparent\u001b[0m -> {'name': 'test', 'path': ['grandparent']}\n", + "\u001b[36;1m\u001b[1;3m[2:writes]\u001b[0m \u001b[1mFinished step 2 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['parent']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test', 'path': ['grandparent', 'parent']}\n", + "\u001b[36;1m\u001b[1;3m[3:tasks]\u001b[0m \u001b[1mStarting step 3 with 2 tasks:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mchild\u001b[0m -> {'name': 'test', 'path': ['grandparent', 'parent']}\n", + "- \u001b[32;1m\u001b[1;3msibling\u001b[0m -> {'name': 'test', 'path': ['grandparent', 'parent']}\n", + "\u001b[36;1m\u001b[1;3m[3:writes]\u001b[0m \u001b[1mFinished step 3 with writes to 2 channels:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", + "- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent', 'parent', 'child_start', 'child_middle', 'child_end'], ['sibling']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test',\n", + " 'path': ['grandparent',\n", + " 'parent',\n", + " 'grandparent',\n", + " 'parent',\n", + " 'child_start',\n", + " 'child_middle',\n", + " 'child_end',\n", + " 'sibling']}\n", + "\u001b[36;1m\u001b[1;3m[4:tasks]\u001b[0m \u001b[1mStarting step 4 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mfin\u001b[0m -> {'name': 'test',\n", + " 'path': ['grandparent',\n", + " 'parent',\n", + " 'grandparent',\n", + " 'parent',\n", + " 'child_start',\n", + " 'child_middle',\n", + " 'child_end',\n", + " 'sibling']}\n", + "\u001b[36;1m\u001b[1;3m[4:writes]\u001b[0m \u001b[1mFinished step 4 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['fin']\n" + ] + }, + { + "data": { + "text/plain": [ + "{'name': 'test',\n", + " 'path': ['grandparent',\n", + " 'parent',\n", + " 'grandparent',\n", + " 'parent',\n", + " 'child_start',\n", + " 'child_middle',\n", + " 'child_end',\n", + " 'sibling',\n", + " 'fin']}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph.invoke({\"name\": \"test\"}, debug=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Notice here that the `[\"grandparent\", \"parent\"]` sequence is duplicated! \n", + "\n", + "This is because our child state has received the full parent state and returns the full parent state once it terminates. \n", + "\n", + "To avoid duplication or conflicts in state, you typically would do one or more of the following:\n", + "\n", + "1. Handle duplicates in your `reducer` function.\n", + "2. Call the child graph from within a python function. In that function, handle the state as needed. \n", + "3. Update the child graph keys to avoid conflicts. You would still need to ensure the output can be interpreted by the parent, however.\n", + "\n", + "Let's re-implement the graph using technique (1) and add unique IDs for every value in the list. This is what is done in [`MessageGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.MessageGraph)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "import uuid\n", + "\n", + "def reduce_list(left: list | None, right: list | None) -> list:\n", + " \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n", + " if not left:\n", + " left = []\n", + " if not right:\n", + " right = []\n", + " left_, right_ = [], []\n", + " for orig, new in [(left, left_), (right, right_)]:\n", + " for val in orig:\n", + " if not isinstance(val, dict):\n", + " val = {\"val\": val}\n", + " if \"id\" not in val:\n", + " val[\"id\"] = str(uuid.uuid4())\n", + " new.append(val)\n", + " # Merge the two lists\n", + " left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n", + " merged = left_.copy()\n", + " for val in right_:\n", + " if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n", + " merged[existing_idx] = val\n", + " else:\n", + " merged.append(val)\n", + " return merged\n", + "\n", + "\n", + "class ChildState(TypedDict):\n", + " name: str\n", + " # note the updated reducer here\n", + " path: Annotated[list[str], reduce_list]\n", + "\n", + "\n", + "class ParentState(TypedDict):\n", + " name: str\n", + " # note the updated reducer here\n", + " path: Annotated[list[str], reduce_list]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since our graph topology hasn't changed, we can just reuse the same `make_graph` helper function we defined previously and pass new schema for the parent and child graphs." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'path': []}\n", + "\u001b[36;1m\u001b[1;3m[0:tasks]\u001b[0m \u001b[1mStarting step 0 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3m__start__\u001b[0m -> {'name': 'test'}\n", + "\u001b[36;1m\u001b[1;3m[0:writes]\u001b[0m \u001b[1mFinished step 0 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test', 'path': []}\n", + "\u001b[36;1m\u001b[1;3m[1:tasks]\u001b[0m \u001b[1mStarting step 1 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mgrandparent\u001b[0m -> {'name': 'test', 'path': []}\n", + "\u001b[36;1m\u001b[1;3m[1:writes]\u001b[0m \u001b[1mFinished step 1 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['grandparent']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}]}\n", + "\u001b[36;1m\u001b[1;3m[2:tasks]\u001b[0m \u001b[1mStarting step 2 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mparent\u001b[0m -> {'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'}]}\n", + "\u001b[36;1m\u001b[1;3m[2:writes]\u001b[0m \u001b[1mFinished step 2 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['parent']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}]}\n", + "\u001b[36;1m\u001b[1;3m[3:tasks]\u001b[0m \u001b[1mStarting step 3 with 2 tasks:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mchild\u001b[0m -> {'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}]}\n", + "- \u001b[32;1m\u001b[1;3msibling\u001b[0m -> {'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'}]}\n", + "\u001b[36;1m\u001b[1;3m[3:writes]\u001b[0m \u001b[1mFinished step 3 with writes to 2 channels:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mname\u001b[0m -> 'test'\n", + "- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'},\n", + " {'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf', 'val': 'child_start'},\n", + " {'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3', 'val': 'child_middle'},\n", + " {'id': '052b5578-6939-4dc0-8e24-0a13548a937e', 'val': 'child_end'}], ['sibling']\n", + "\u001b[36;1m\u001b[1;3m[-2:checkpoint]\u001b[0m \u001b[1mState at the end of step -2:\n", + "\u001b[0m{'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'},\n", + " {'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf', 'val': 'child_start'},\n", + " {'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3', 'val': 'child_middle'},\n", + " {'id': '052b5578-6939-4dc0-8e24-0a13548a937e', 'val': 'child_end'},\n", + " {'id': 'ff5e852c-3c71-4133-87a1-ec2e0b3a5b29', 'val': 'sibling'}]}\n", + "\u001b[36;1m\u001b[1;3m[4:tasks]\u001b[0m \u001b[1mStarting step 4 with 1 task:\n", + "\u001b[0m- \u001b[32;1m\u001b[1;3mfin\u001b[0m -> {'name': 'test',\n", + " 'path': [{'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7', 'val': 'grandparent'},\n", + " {'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e', 'val': 'parent'},\n", + " {'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf', 'val': 'child_start'},\n", + " {'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3', 'val': 'child_middle'},\n", + " {'id': '052b5578-6939-4dc0-8e24-0a13548a937e', 'val': 'child_end'},\n", + " {'id': 'ff5e852c-3c71-4133-87a1-ec2e0b3a5b29', 'val': 'sibling'}]}\n", + "\u001b[36;1m\u001b[1;3m[4:writes]\u001b[0m \u001b[1mFinished step 4 with writes to 1 channel:\n", + "\u001b[0m- \u001b[33;1m\u001b[1;3mpath\u001b[0m -> ['fin']\n" + ] + }, + { + "data": { + "text/plain": [ + "{'name': 'test',\n", + " 'path': [{'val': 'grandparent', 'id': 'a3b7abbe-1083-40af-aa6f-23b39d6b5ab7'},\n", + " {'val': 'parent', 'id': 'ce8522d8-5c45-4d0c-8e9f-42b11e9e5c6e'},\n", + " {'val': 'child_start', 'id': '2c3d0366-9744-4ece-b3d5-95fa9727e5bf'},\n", + " {'val': 'child_middle', 'id': 'b5920f7a-d722-43f2-86fa-cb9cb0dfdcc3'},\n", + " {'val': 'child_end', 'id': '052b5578-6939-4dc0-8e24-0a13548a937e'},\n", + " {'val': 'sibling', 'id': 'ff5e852c-3c71-4133-87a1-ec2e0b3a5b29'},\n", + " {'val': 'fin', 'id': '82dc42d5-799b-4fad-8fbd-b12f32c179d2'}]}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph = make_graph(ParentState, ChildState)\n", + "graph.invoke({\"name\": \"test\"}, debug=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can see that that now the path values are no longer duplicated thanks to the updated reducer we introduced above." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "langgraph", + "language": "python", + "name": "langgraph" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 } From 8154ab784e53891992bf437064d5177a99ec6613 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 29 Aug 2024 20:15:20 -0700 Subject: [PATCH 203/287] For input checkpoint use filtered input writes in checkpoint metadata - if we use the raw input we can end up saving keys that were actually ignored (because not one of the input keys), which is incosistent --- 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 d791a5539..b2eaac472 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -422,7 +422,7 @@ class PregelLoop: self.checkpointer_get_next_version, ), "Can't write to SharedValues in graph input" # save input checkpoint - self._put_checkpoint({"source": "input", "writes": self.input}) + self._put_checkpoint({"source": "input", "writes": dict(input_writes)}) else: raise EmptyInputError(f"Received no input for {input_keys}") # done with input From 6379cdc36d09a388056e2f7a37de07ee287b85a5 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 13:24:29 -0700 Subject: [PATCH 204/287] Fix tests --- libs/langgraph/tests/test_pregel.py | 57 +++++++++++++++++------ libs/langgraph/tests/test_pregel_async.py | 57 +++++++++++++++++------ 2 files changed, 84 insertions(+), 30 deletions(-) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 704824fe5..5c521f45c 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -707,7 +707,12 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"parents": {}, "source": "input", "step": 4, "writes": 3}, + metadata={ + "parents": {}, + "source": "input", + "step": 4, + "writes": {"input": 3}, + }, created_at=AnyStr(), parent_config=history[3].config, ), @@ -742,7 +747,12 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"parents": {}, "source": "input", "step": 2, "writes": 20}, + metadata={ + "parents": {}, + "source": "input", + "step": 2, + "writes": {"input": 20}, + }, created_at=AnyStr(), parent_config=history[5].config, ), @@ -792,7 +802,12 @@ def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"parents": {}, "source": "input", "step": -1, "writes": 2}, + metadata={ + "parents": {}, + "source": "input", + "step": -1, + "writes": {"input": 2}, + }, created_at=AnyStr(), parent_config=None, ), @@ -967,7 +982,12 @@ def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"parents": {}, "source": "input", "step": -1, "writes": 1}, + metadata={ + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": 1}, + }, created_at=AnyStr(), parent_config=None, ), @@ -1566,7 +1586,12 @@ def test_pending_writes_resume( }, "channel_values": {"__start__": {"value": 1}}, }, - metadata={"parents": {}, "step": -1, "source": "input", "writes": {"value": 1}}, + metadata={ + "parents": {}, + "step": -1, + "source": "input", + "writes": {"__start__": {"value": 1}}, + }, parent_config=None, pending_writes=UnsortedSequence( (AnyStr(), "value", 1), @@ -6557,7 +6582,7 @@ def test_dynamic_interrupt( "parents": {}, "source": "input", "step": -1, - "writes": {"my_key": "value ⛰️", "market": "DE"}, + "writes": {"__start__": {"my_key": "value ⛰️", "market": "DE"}}, }, ] assert tool_two.get_state(thread1) == StateSnapshot( @@ -6654,7 +6679,7 @@ def test_start_branch_then( "parents": {}, "source": "input", "step": -1, - "writes": {"my_key": "value ⛰️", "market": "DE"}, + "writes": {"__start__": {"my_key": "value ⛰️", "market": "DE"}}, }, ] assert tool_two.get_state(thread1) == StateSnapshot( @@ -6837,7 +6862,7 @@ def test_branch_then( "parents": {}, "source": "input", "step": -1, - "writes": {"my_key": "value", "market": "DE"}, + "writes": {"__start__": {"my_key": "value", "market": "DE"}}, }, "next": ["__start__"], "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], @@ -8793,7 +8818,7 @@ def test_nested_graph_state( metadata={ "parents": {}, "source": "input", - "writes": {"my_key": "my value"}, + "writes": {"__start__": {"my_key": "my value"}}, "step": -1, }, created_at=AnyStr(), @@ -8881,7 +8906,9 @@ def test_nested_graph_state( }, metadata={ "source": "input", - "writes": {"my_key": "hi my value", "other_parent_key": None}, + "writes": { + "__start__": {"my_key": "hi my value", "other_parent_key": None} + }, "step": -1, "parents": {"": AnyStr()}, }, @@ -9048,7 +9075,7 @@ def test_nested_graph_state( metadata={ "parents": {}, "source": "input", - "writes": {"my_key": "my value"}, + "writes": {"__start__": {"my_key": "my value"}}, "step": -1, }, created_at=AnyStr(), @@ -9532,7 +9559,7 @@ def test_doubly_nested_graph_state( }, metadata={ "source": "input", - "writes": {"my_key": "my value"}, + "writes": {"__start__": {"my_key": "my value"}}, "step": -1, "parents": {}, }, @@ -9628,7 +9655,7 @@ def test_doubly_nested_graph_state( }, metadata={ "source": "input", - "writes": {"my_key": "hi my value"}, + "writes": {"__start__": {"my_key": "hi my value"}}, "step": -1, "parents": {"": AnyStr()}, }, @@ -9773,7 +9800,7 @@ def test_doubly_nested_graph_state( }, metadata={ "source": "input", - "writes": {"my_key": "hi my value"}, + "writes": {"__start__": {"my_key": "hi my value"}}, "step": -1, "parents": AnyDict( { @@ -10095,7 +10122,7 @@ def test_send_to_nested_graphs( metadata={ "parents": {}, "source": "input", - "writes": {"subjects": ["cats", "dogs"]}, + "writes": {"__start__": {"subjects": ["cats", "dogs"]}}, "step": -1, }, created_at=AnyStr(), diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index e5cda8e11..764c31117 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -282,7 +282,7 @@ async def test_dynamic_interrupt( "parents": {}, "source": "input", "step": -1, - "writes": {"my_key": "value ⛰️", "market": "DE"}, + "writes": {"__start__": {"my_key": "value ⛰️", "market": "DE"}}, }, ] tup = await tool_two.checkpointer.aget_tuple(thread1) @@ -929,7 +929,12 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"parents": {}, "source": "input", "step": 4, "writes": 3}, + metadata={ + "parents": {}, + "source": "input", + "step": 4, + "writes": {"input": 3}, + }, created_at=AnyStr(), parent_config=history[3].config, ), @@ -964,7 +969,12 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"parents": {}, "source": "input", "step": 2, "writes": 20}, + metadata={ + "parents": {}, + "source": "input", + "step": 2, + "writes": {"input": 20}, + }, created_at=AnyStr(), parent_config=history[5].config, ), @@ -1014,7 +1024,12 @@ async def test_invoke_two_processes_in_out_interrupt( "checkpoint_id": AnyStr(), } }, - metadata={"parents": {}, "source": "input", "step": -1, "writes": 2}, + metadata={ + "parents": {}, + "source": "input", + "step": -1, + "writes": {"input": 2}, + }, created_at=AnyStr(), parent_config=None, ), @@ -1197,7 +1212,12 @@ async def test_fork_always_re_runs_nodes( "checkpoint_id": AnyStr(), } }, - metadata={"parents": {}, "source": "input", "step": -1, "writes": 1}, + metadata={ + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": 1}, + }, created_at=AnyStr(), parent_config=None, ), @@ -1785,7 +1805,12 @@ async def test_pending_writes_resume( }, "channel_values": {"__start__": {"value": 1}}, }, - metadata={"parents": {}, "step": -1, "source": "input", "writes": {"value": 1}}, + metadata={ + "parents": {}, + "step": -1, + "source": "input", + "writes": {"__start__": {"value": 1}}, + }, parent_config=None, pending_writes=UnsortedSequence( (AnyStr(), "value", 1), @@ -5191,7 +5216,7 @@ async def test_start_branch_then( "parents": {}, "source": "input", "step": -1, - "writes": {"my_key": "value", "market": "DE"}, + "writes": {"__start__": {"my_key": "value", "market": "DE"}}, }, ] assert await tool_two.aget_state(thread1) == StateSnapshot( @@ -5387,7 +5412,7 @@ async def test_branch_then( "parents": {}, "source": "input", "step": -1, - "writes": {"my_key": "value", "market": "DE"}, + "writes": {"__start__": {"my_key": "value", "market": "DE"}}, }, "next": ["__start__"], "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], @@ -5621,7 +5646,7 @@ async def test_branch_then( "parents": {}, "source": "input", "step": -1, - "writes": {"my_key": "value", "market": "DE"}, + "writes": {"__start__": {"my_key": "value", "market": "DE"}}, }, "next": ["__start__"], "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], @@ -7290,7 +7315,7 @@ async def test_nested_graph_state( metadata={ "parents": {}, "source": "input", - "writes": {"my_key": "my value"}, + "writes": {"__start__": {"my_key": "my value"}}, "step": -1, }, created_at=AnyStr(), @@ -7378,7 +7403,9 @@ async def test_nested_graph_state( }, metadata={ "source": "input", - "writes": {"my_key": "hi my value", "other_parent_key": None}, + "writes": { + "__start__": {"my_key": "hi my value", "other_parent_key": None} + }, "step": -1, "parents": {"": AnyStr()}, }, @@ -7550,7 +7577,7 @@ async def test_nested_graph_state( metadata={ "parents": {}, "source": "input", - "writes": {"my_key": "my value"}, + "writes": {"__start__": {"my_key": "my value"}}, "step": -1, }, created_at=AnyStr(), @@ -8147,7 +8174,7 @@ async def test_doubly_nested_graph_state( }, metadata={ "source": "input", - "writes": {"my_key": "hi my value"}, + "writes": {"__start__": {"my_key": "hi my value"}}, "step": -1, "parents": {"": AnyStr()}, }, @@ -8294,7 +8321,7 @@ async def test_doubly_nested_graph_state( }, metadata={ "source": "input", - "writes": {"my_key": "hi my value"}, + "writes": {"__start__": {"my_key": "hi my value"}}, "step": -1, "parents": AnyDict( { @@ -8554,7 +8581,7 @@ async def test_send_to_nested_graphs( metadata={ "parents": {}, "source": "input", - "writes": {"subjects": ["cats", "dogs"]}, + "writes": {"__start__": {"subjects": ["cats", "dogs"]}}, "step": -1, }, created_at=AnyStr(), From 5db13dffbee160cd2b879634fb3cbdd3bbfa234e Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 28 Aug 2024 12:26:52 -0700 Subject: [PATCH 205/287] checkpoint: Avoid changes to pydantic models breaking deserialization while still relying on pydantic's type coercion --- libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py index e39a66e76..501fdc9e4 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py @@ -51,13 +51,9 @@ class JsonPlusSerializer(SerializerProtocol): if isinstance(obj, Serializable): return obj.to_json() elif hasattr(obj, "model_dump") and callable(obj.model_dump): - return self._encode_constructor_args( - obj.__class__, method="model_construct", kwargs=obj.model_dump() - ) + return self._encode_constructor_args(obj.__class__, kwargs=obj.model_dump()) elif hasattr(obj, "dict") and callable(obj.dict): - return self._encode_constructor_args( - obj.__class__, method="construct", kwargs=obj.dict() - ) + return self._encode_constructor_args(obj.__class__, kwargs=obj.dict()) elif isinstance(obj, pathlib.Path): return self._encode_constructor_args(pathlib.Path, args=obj.parts) elif isinstance(obj, re.Pattern): @@ -150,7 +146,7 @@ class JsonPlusSerializer(SerializerProtocol): return method(**value["kwargs"]) else: return method() - except (ImportError, AttributeError, TypeError): + except Exception: return None return LC_REVIVER(value) From af1f218e00f649e0bf9ceff0625e5bc0ecf2da0a Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 13:35:33 -0700 Subject: [PATCH 206/287] Try init, then construct --- .../langgraph/checkpoint/serde/jsonplus.py | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py index 501fdc9e4..e777a8176 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py @@ -51,9 +51,13 @@ class JsonPlusSerializer(SerializerProtocol): if isinstance(obj, Serializable): return obj.to_json() elif hasattr(obj, "model_dump") and callable(obj.model_dump): - return self._encode_constructor_args(obj.__class__, kwargs=obj.model_dump()) + return self._encode_constructor_args( + obj.__class__, method=[None, "model_construct"], kwargs=obj.model_dump() + ) elif hasattr(obj, "dict") and callable(obj.dict): - return self._encode_constructor_args(obj.__class__, kwargs=obj.dict()) + return self._encode_constructor_args( + obj.__class__, method=[None, "construct"], kwargs=obj.dict() + ) elif isinstance(obj, pathlib.Path): return self._encode_constructor_args(pathlib.Path, args=obj.parts) elif isinstance(obj, re.Pattern): @@ -132,20 +136,29 @@ class JsonPlusSerializer(SerializerProtocol): # Import class cls = getattr(mod, name) # Instantiate class - if value["method"] is not None: - method = getattr(cls, value["method"]) + if isinstance(value["method"], str): + methods = [getattr(cls, value["method"])] + elif isinstance(value["method"], list): + methods = [ + cls if method is None else getattr(cls, method) + for method in value["method"] + ] else: - method = cls - if isclass(method) and issubclass(method, BaseException): - return None - if value["args"] and value["kwargs"]: - return method(*value["args"], **value["kwargs"]) - elif value["args"]: - return method(*value["args"]) - elif value["kwargs"]: - return method(**value["kwargs"]) - else: - return method() + methods = [cls] + for method in methods: + try: + if isclass(method) and issubclass(method, BaseException): + return None + if value["args"] and value["kwargs"]: + return method(*value["args"], **value["kwargs"]) + elif value["args"]: + return method(*value["args"]) + elif value["kwargs"]: + return method(**value["kwargs"]) + else: + return method() + except Exception: + continue except Exception: return None From c5c10a41e5176fc7420f50e008a5ee482599504e Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 13:59:36 -0700 Subject: [PATCH 207/287] Remove None default for missing keys when generating input for each node --- libs/langgraph/langgraph/pregel/algo.py | 24 +++++++++++------------ libs/langgraph/tests/test_pregel.py | 24 ++++------------------- libs/langgraph/tests/test_pregel_async.py | 24 ++++------------------- 3 files changed, 20 insertions(+), 52 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index f3d82c835..f6301a379 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -384,7 +384,7 @@ def prepare_next_tasks( try: val = next( _proc_input( - step, name, proc, managed, channels, for_execution=for_execution + step, proc, managed, channels, for_execution=for_execution ) ) except StopIteration: @@ -474,7 +474,6 @@ def prepare_next_tasks( def _proc_input( step: int, - name: str, proc: PregelNode, managed: ManagedValueMapping, channels: Mapping[str, BaseChannel], @@ -485,16 +484,17 @@ def _proc_input( # then invoke the process with the values of all non-empty channels if isinstance(proc.channels, dict): try: - val: dict = { - k: read_channel( - channels, - chan, - catch=chan not in proc.triggers, - ) - if chan in channels - else managed[k](step) - for k, chan in proc.channels.items() - } + val: dict[str, Any] = {} + for k, chan in proc.channels.items(): + if chan in proc.triggers: + val[k] = read_channel(channels, chan, catch=False) + elif chan in channels: + try: + val[k] = read_channel(channels, chan, catch=False) + except EmptyChannelError: + continue + else: + val[k] = managed[k](step) except EmptyChannelError: return elif isinstance(proc.channels, list): diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 5c521f45c..31484cfe3 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -294,7 +294,6 @@ def test_node_schemas_custom_output() -> None: def node_b(state: StateForB): assert state == { "bye": "world", - "now": None, } return { "now": 123, @@ -6364,11 +6363,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", "name": "rewrite_query", - "input": { - "query": "what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "what is weather in sf", "docs": []}, "triggers": ["start:rewrite_query"], }, }, @@ -6399,11 +6394,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", "name": "retriever_one", - "input": { - "query": "query: what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], }, }, @@ -6417,11 +6408,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", "name": "retriever_two", - "input": { - "query": "query: what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], }, }, @@ -6482,7 +6469,6 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "name": "qa", "input": { "query": "query: what is weather in sf", - "answer": None, "docs": ["doc1", "doc2", "doc3", "doc4"], }, "triggers": ["retriever_one", "retriever_two"], @@ -8906,9 +8892,7 @@ def test_nested_graph_state( }, metadata={ "source": "input", - "writes": { - "__start__": {"my_key": "hi my value", "other_parent_key": None} - }, + "writes": {"__start__": {"my_key": "hi my value"}}, "step": -1, "parents": {"": AnyStr()}, }, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 764c31117..2f03a546e 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -570,7 +570,6 @@ async def test_node_schemas_custom_output() -> None: async def node_b(state: StateForB): assert state == { "bye": "world", - "now": None, } return { "now": 123, @@ -4987,11 +4986,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", "name": "rewrite_query", - "input": { - "query": "what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "what is weather in sf", "docs": []}, "triggers": ["start:rewrite_query"], }, }, @@ -5022,11 +5017,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", "name": "retriever_one", - "input": { - "query": "query: what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], }, }, @@ -5040,11 +5031,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", "name": "retriever_two", - "input": { - "query": "query: what is weather in sf", - "answer": None, - "docs": [], - }, + "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], }, }, @@ -5105,7 +5092,6 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "name": "qa", "input": { "query": "query: what is weather in sf", - "answer": None, "docs": ["doc1", "doc2", "doc3", "doc4"], }, "triggers": ["retriever_one", "retriever_two"], @@ -7403,9 +7389,7 @@ async def test_nested_graph_state( }, metadata={ "source": "input", - "writes": { - "__start__": {"my_key": "hi my value", "other_parent_key": None} - }, + "writes": {"__start__": {"my_key": "hi my value"}}, "step": -1, "parents": {"": AnyStr()}, }, From e744db169a4f4b58900148fb1ad8e172cc48e3c7 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 14:07:46 -0700 Subject: [PATCH 208/287] postgres/sqlite: Sort pending writes when fetching from db --- .../checkpoint-postgres/langgraph/checkpoint/postgres/base.py | 4 ++-- .../checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py | 4 ++-- libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py index 582206dd5..6cfbc5108 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/base.py @@ -76,14 +76,14 @@ select ) as channel_values, ( select - array_agg(array[cw.task_id::text::bytea, cw.channel::bytea, cw.type::bytea, cw.blob]) + array_agg(array[cw.task_id::text::bytea, cw.channel::bytea, cw.type::bytea, cw.blob] order by cw.task_id, cw.idx) from checkpoint_writes cw where cw.thread_id = checkpoints.thread_id and cw.checkpoint_ns = checkpoints.checkpoint_ns and cw.checkpoint_id = checkpoints.checkpoint_id ) as pending_writes, ( - select array_agg(array[cw.type::bytea, cw.blob]) + select array_agg(array[cw.type::bytea, cw.blob] order by cw.idx) from checkpoint_writes cw where cw.thread_id = checkpoints.thread_id and cw.checkpoint_ns = checkpoints.checkpoint_ns diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py index 4767bfb70..04493a381 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py @@ -244,7 +244,7 @@ class SqliteSaver(BaseCheckpointSaver): } # find any pending writes cur.execute( - "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?", + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx", ( str(config["configurable"]["thread_id"]), checkpoint_ns, @@ -331,7 +331,7 @@ class SqliteSaver(BaseCheckpointSaver): metadata, ) in cur: wcur.execute( - "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?", + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx", (thread_id, checkpoint_ns, checkpoint_id), ) yield CheckpointTuple( diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py index 85869d3f0..dc68a0403 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py @@ -278,7 +278,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver): } # find any pending writes await cur.execute( - "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?", + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx", ( str(config["configurable"]["thread_id"]), checkpoint_ns, @@ -348,7 +348,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver): metadata, ) in cur: await wcur.execute( - "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ?", + "SELECT task_id, channel, type, value FROM writes WHERE thread_id = ? AND checkpoint_ns = ? AND checkpoint_id = ? ORDER BY task_id, idx", (thread_id, checkpoint_ns, checkpoint_id), ) yield CheckpointTuple( From 39436372026f9b2421a00d80588ddec1af2faf05 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 14:18:35 -0700 Subject: [PATCH 209/287] Bump them all --- libs/checkpoint-postgres/pyproject.toml | 2 +- libs/checkpoint-sqlite/pyproject.toml | 2 +- libs/checkpoint/pyproject.toml | 2 +- libs/langgraph/pyproject.toml | 2 +- libs/sdk-py/pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml index 381f2f063..077f8dd09 100644 --- a/libs/checkpoint-postgres/pyproject.toml +++ b/libs/checkpoint-postgres/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint-postgres" -version = "1.0.4" +version = "1.0.5" description = "Library with a Postgres implementation of LangGraph checkpoint saver." authors = [] license = "MIT" diff --git a/libs/checkpoint-sqlite/pyproject.toml b/libs/checkpoint-sqlite/pyproject.toml index 28dd851fd..536895f77 100644 --- a/libs/checkpoint-sqlite/pyproject.toml +++ b/libs/checkpoint-sqlite/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint-sqlite" -version = "1.0.0" +version = "1.0.1" description = "Library with a SQLite implementation of LangGraph checkpoint saver." authors = [] license = "MIT" diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml index c6541e5ec..b4c21265f 100644 --- a/libs/checkpoint/pyproject.toml +++ b/libs/checkpoint/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint" -version = "1.0.6" +version = "1.0.7" description = "Library with base interfaces for LangGraph checkpoint savers." authors = [] license = "MIT" diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index de8d1fcd7..36cdb7fdc 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.14" +version = "0.2.15" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml index bcba0feec..378c58777 100644 --- a/libs/sdk-py/pyproject.toml +++ b/libs/sdk-py/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-sdk" -version = "0.1.29" +version = "0.1.30" description = "SDK for interacting with LangGraph API" authors = [] license = "MIT" From 770132dd79ff86a6f0687064e37e573d95635e3c Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 14:23:10 -0700 Subject: [PATCH 210/287] sdk-py: Add values to get/list threads --- libs/sdk-py/langgraph_sdk/client.py | 15 +++++++++------ libs/sdk-py/langgraph_sdk/schema.py | 12 ++++++------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index 79ac0b3e8..5d6779c5d 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -27,7 +27,7 @@ from langgraph_sdk.schema import ( Cron, DisconnectMode, GraphSchema, - Metadata, + Json, MultitaskStrategy, OnCompletionBehavior, OnConflictBehavior, @@ -414,7 +414,7 @@ class AssistantsClient: graph_id: Optional[str], config: Optional[Config] = None, *, - metadata: Metadata = None, + metadata: Json = None, assistant_id: Optional[str] = None, if_exists: Optional[OnConflictBehavior] = None, ) -> Assistant: @@ -462,7 +462,7 @@ class AssistantsClient: *, graph_id: Optional[str] = None, config: Optional[Config] = None, - metadata: Metadata = None, + metadata: Json = None, ) -> Assistant: """Update an assistant. @@ -524,7 +524,7 @@ class AssistantsClient: async def search( self, *, - metadata: Metadata = None, + metadata: Json = None, graph_id: Optional[str] = None, limit: int = 10, offset: int = 0, @@ -600,7 +600,7 @@ class ThreadsClient: async def create( self, *, - metadata: Metadata = None, + metadata: Json = None, thread_id: Optional[str] = None, if_exists: Optional[OnConflictBehavior] = None, ) -> Thread: @@ -675,7 +675,8 @@ class ThreadsClient: async def search( self, *, - metadata: Metadata = None, + metadata: Json = None, + values: Json = None, status: Optional[ThreadStatus] = None, limit: int = 10, offset: int = 0, @@ -708,6 +709,8 @@ class ThreadsClient: } if metadata: payload["metadata"] = metadata + if values: + payload["values"] = values if status: payload["status"] = status return await self.http.post( diff --git a/libs/sdk-py/langgraph_sdk/schema.py b/libs/sdk-py/langgraph_sdk/schema.py index 7007a9ad1..5f58f02c2 100644 --- a/libs/sdk-py/langgraph_sdk/schema.py +++ b/libs/sdk-py/langgraph_sdk/schema.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Any, Literal, Optional, Sequence, TypedDict, Union -Metadata = Optional[dict[str, Any]] +Json = Optional[dict[str, Any]] RunStatus = Literal["pending", "running", "error", "success", "timeout", "interrupted"] @@ -70,7 +70,7 @@ class Assistant(TypedDict): """The time the assistant was created.""" updated_at: datetime """The last time the assistant was updated.""" - metadata: Metadata + metadata: Json """The assistant metadata.""" @@ -81,11 +81,11 @@ class Thread(TypedDict): """The time the thread was created.""" updated_at: datetime """The last time the thread was updated.""" - metadata: Metadata + metadata: Json """The thread metadata.""" status: ThreadStatus """The status of the thread, one of 'idle', 'busy', 'interrupted'.""" - values: dict + values: Json """The current state of the thread.""" @@ -97,7 +97,7 @@ class ThreadState(TypedDict): received.""" checkpoint_id: str """The ID of the checkpoint.""" - metadata: Metadata + metadata: Json """Metadata for this state""" created_at: Optional[str] """Timestamp of state creation""" @@ -118,7 +118,7 @@ class Run(TypedDict): """The last time the run was updated.""" status: RunStatus """The status of the run. One of 'pending', 'running', "error", 'success', "timeout", "interrupted".""" - metadata: Metadata + metadata: Json """The run metadata.""" multitask_strategy: MultitaskStrategy """Strategy to handle concurrent runs on the same thread.""" From cf4de9a1a5b60a614beed7ce96437dff0641472e Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Fri, 30 Aug 2024 17:45:35 -0400 Subject: [PATCH 211/287] cli: remove jupyter from examples pyproject (#1554) --- libs/cli/examples/poetry.lock | 2084 +----------------------------- libs/cli/examples/pyproject.toml | 1 - 2 files changed, 4 insertions(+), 2081 deletions(-) diff --git a/libs/cli/examples/poetry.lock b/libs/cli/examples/poetry.lock index ec32912f9..936020585 100644 --- a/libs/cli/examples/poetry.lock +++ b/libs/cli/examples/poetry.lock @@ -22,197 +22,6 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] -[[package]] -name = "appnope" -version = "0.1.4" -description = "Disable App Nap on macOS >= 10.9" -optional = false -python-versions = ">=3.6" -files = [ - {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, - {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, -] - -[[package]] -name = "argon2-cffi" -version = "23.1.0" -description = "Argon2 for Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, - {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, -] - -[package.dependencies] -argon2-cffi-bindings = "*" - -[package.extras] -dev = ["argon2-cffi[tests,typing]", "tox (>4)"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"] -tests = ["hypothesis", "pytest"] -typing = ["mypy"] - -[[package]] -name = "argon2-cffi-bindings" -version = "21.2.0" -description = "Low-level CFFI bindings for Argon2" -optional = false -python-versions = ">=3.6" -files = [ - {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, - {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, - {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, - {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, - {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, -] - -[package.dependencies] -cffi = ">=1.0.1" - -[package.extras] -dev = ["cogapp", "pre-commit", "pytest", "wheel"] -tests = ["pytest"] - -[[package]] -name = "arrow" -version = "1.3.0" -description = "Better dates & times for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, - {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, -] - -[package.dependencies] -python-dateutil = ">=2.7.0" -types-python-dateutil = ">=2.8.10" - -[package.extras] -doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] -test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] - -[[package]] -name = "asttokens" -version = "2.4.1" -description = "Annotate AST trees with source code positions" -optional = false -python-versions = "*" -files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, -] - -[package.dependencies] -six = ">=1.12.0" - -[package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] - -[[package]] -name = "async-lru" -version = "2.0.4" -description = "Simple LRU cache for asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "async-lru-2.0.4.tar.gz", hash = "sha256:b8a59a5df60805ff63220b2a0c5b5393da5521b113cd5465a44eb037d81a5627"}, - {file = "async_lru-2.0.4-py3-none-any.whl", hash = "sha256:ff02944ce3c288c5be660c42dbcca0742b32c3b279d6dceda655190240b99224"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - -[[package]] -name = "babel" -version = "2.15.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, - {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, -] - -[package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] - -[[package]] -name = "beautifulsoup4" -version = "4.12.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, -] - -[package.dependencies] -soupsieve = ">1.2" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "bleach" -version = "6.1.0" -description = "An easy safelist-based HTML-sanitizing tool." -optional = false -python-versions = ">=3.8" -files = [ - {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, - {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, -] - -[package.dependencies] -six = ">=1.9.0" -webencodings = "*" - -[package.extras] -css = ["tinycss2 (>=1.1.0,<1.3)"] - [[package]] name = "certifi" version = "2024.7.4" @@ -224,169 +33,6 @@ files = [ {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - [[package]] name = "click" version = "8.1.7" @@ -412,76 +58,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "comm" -version = "0.2.2" -description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." -optional = false -python-versions = ">=3.8" -files = [ - {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, - {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, -] - -[package.dependencies] -traitlets = ">=4" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "debugpy" -version = "1.8.1" -description = "An implementation of the Debug Adapter Protocol for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741"}, - {file = "debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e"}, - {file = "debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0"}, - {file = "debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd"}, - {file = "debugpy-1.8.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb"}, - {file = "debugpy-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"}, - {file = "debugpy-1.8.1-cp311-cp311-win32.whl", hash = "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146"}, - {file = "debugpy-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8"}, - {file = "debugpy-1.8.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539"}, - {file = "debugpy-1.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace"}, - {file = "debugpy-1.8.1-cp312-cp312-win32.whl", hash = "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0"}, - {file = "debugpy-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98"}, - {file = "debugpy-1.8.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39"}, - {file = "debugpy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7"}, - {file = "debugpy-1.8.1-cp38-cp38-win32.whl", hash = "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9"}, - {file = "debugpy-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234"}, - {file = "debugpy-1.8.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42"}, - {file = "debugpy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703"}, - {file = "debugpy-1.8.1-cp39-cp39-win32.whl", hash = "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23"}, - {file = "debugpy-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3"}, - {file = "debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242"}, - {file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"}, -] - -[[package]] -name = "decorator" -version = "5.1.1" -description = "Decorators for Humans" -optional = false -python-versions = ">=3.5" -files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] - -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - [[package]] name = "exceptiongroup" version = "1.2.1" @@ -496,45 +72,6 @@ files = [ [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "executing" -version = "2.0.1" -description = "Get the currently executing AST node of a frame, and other information" -optional = false -python-versions = ">=3.5" -files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, -] - -[package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] - -[[package]] -name = "fastjsonschema" -version = "2.20.0" -description = "Fastest Python implementation of JSON schema" -optional = false -python-versions = "*" -files = [ - {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, - {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, -] - -[package.extras] -devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] - -[[package]] -name = "fqdn" -version = "1.5.1" -description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers" -optional = false -python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4" -files = [ - {file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"}, - {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, -] - [[package]] name = "h11" version = "0.14.0" @@ -613,499 +150,9 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] -[[package]] -name = "importlib-metadata" -version = "7.1.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, - {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] - -[[package]] -name = "ipykernel" -version = "6.29.4" -description = "IPython Kernel for Jupyter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "ipykernel-6.29.4-py3-none-any.whl", hash = "sha256:1181e653d95c6808039c509ef8e67c4126b3b3af7781496c7cbfb5ed938a27da"}, - {file = "ipykernel-6.29.4.tar.gz", hash = "sha256:3d44070060f9475ac2092b760123fadf105d2e2493c24848b6691a7c4f42af5c"}, -] - -[package.dependencies] -appnope = {version = "*", markers = "platform_system == \"Darwin\""} -comm = ">=0.1.1" -debugpy = ">=1.6.5" -ipython = ">=7.23.1" -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -matplotlib-inline = ">=0.1" -nest-asyncio = "*" -packaging = "*" -psutil = "*" -pyzmq = ">=24" -tornado = ">=6.1" -traitlets = ">=5.4.0" - -[package.extras] -cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] -pyqt5 = ["pyqt5"] -pyside6 = ["pyside6"] -test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "ipython" -version = "8.18.1" -description = "IPython: Productive Interactive Computing" -optional = false -python-versions = ">=3.9" -files = [ - {file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"}, - {file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -prompt-toolkit = ">=3.0.41,<3.1.0" -pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5" -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} - -[package.extras] -all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] -black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pickleshare", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio (<0.22)", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] -kernel = ["ipykernel"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["ipywidgets", "notebook"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pandas", "pickleshare", "pytest (<7.1)", "pytest-asyncio (<0.22)", "testpath", "trio"] - -[[package]] -name = "ipywidgets" -version = "8.1.3" -description = "Jupyter interactive widgets" -optional = false -python-versions = ">=3.7" -files = [ - {file = "ipywidgets-8.1.3-py3-none-any.whl", hash = "sha256:efafd18f7a142248f7cb0ba890a68b96abd4d6e88ddbda483c9130d12667eaf2"}, - {file = "ipywidgets-8.1.3.tar.gz", hash = "sha256:f5f9eeaae082b1823ce9eac2575272952f40d748893972956dc09700a6392d9c"}, -] - -[package.dependencies] -comm = ">=0.1.3" -ipython = ">=6.1.0" -jupyterlab-widgets = ">=3.0.11,<3.1.0" -traitlets = ">=4.3.1" -widgetsnbextension = ">=4.0.11,<4.1.0" - -[package.extras] -test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] - -[[package]] -name = "isoduration" -version = "20.11.0" -description = "Operations with ISO 8601 durations" -optional = false -python-versions = ">=3.7" -files = [ - {file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"}, - {file = "isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9"}, -] - -[package.dependencies] -arrow = ">=0.15.0" - -[[package]] -name = "jedi" -version = "0.19.1" -description = "An autocompletion tool for Python that can be used for text editors." -optional = false -python-versions = ">=3.6" -files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, -] - -[package.dependencies] -parso = ">=0.8.3,<0.9.0" - -[package.extras] -docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] - -[[package]] -name = "jinja2" -version = "3.1.4" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "json5" -version = "0.9.25" -description = "A Python implementation of the JSON5 data format." -optional = false -python-versions = ">=3.8" -files = [ - {file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, - {file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, -] - -[[package]] -name = "jsonpointer" -version = "3.0.0" -description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, - {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, -] - -[[package]] -name = "jsonschema" -version = "4.22.0" -description = "An implementation of JSON Schema validation for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jsonschema-4.22.0-py3-none-any.whl", hash = "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802"}, - {file = "jsonschema-4.22.0.tar.gz", hash = "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} -jsonschema-specifications = ">=2023.03.6" -referencing = ">=0.28.4" -rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} -rpds-py = ">=0.7.1" -uri-template = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} -webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format-nongpl\""} - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] - -[[package]] -name = "jsonschema-specifications" -version = "2023.12.1" -description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, - {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, -] - -[package.dependencies] -referencing = ">=0.31.0" - -[[package]] -name = "jupyter" -version = "1.0.0" -description = "Jupyter metapackage. Install all the Jupyter components in one go." -optional = false -python-versions = "*" -files = [ - {file = "jupyter-1.0.0-py2.py3-none-any.whl", hash = "sha256:5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78"}, - {file = "jupyter-1.0.0.tar.gz", hash = "sha256:d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f"}, - {file = "jupyter-1.0.0.zip", hash = "sha256:3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7"}, -] - -[package.dependencies] -ipykernel = "*" -ipywidgets = "*" -jupyter-console = "*" -nbconvert = "*" -notebook = "*" -qtconsole = "*" - -[[package]] -name = "jupyter-client" -version = "8.6.2" -description = "Jupyter protocol implementation and client libraries" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"}, - {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -python-dateutil = ">=2.8.2" -pyzmq = ">=23.0" -tornado = ">=6.2" -traitlets = ">=5.3" - -[package.extras] -docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] - -[[package]] -name = "jupyter-console" -version = "6.6.3" -description = "Jupyter terminal console" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485"}, - {file = "jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539"}, -] - -[package.dependencies] -ipykernel = ">=6.14" -ipython = "*" -jupyter-client = ">=7.0.0" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -prompt-toolkit = ">=3.0.30" -pygments = "*" -pyzmq = ">=17" -traitlets = ">=5.4" - -[package.extras] -test = ["flaky", "pexpect", "pytest"] - -[[package]] -name = "jupyter-core" -version = "5.7.2" -description = "Jupyter core package. A base package on which Jupyter projects rely." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, - {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, -] - -[package.dependencies] -platformdirs = ">=2.5" -pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} -traitlets = ">=5.3" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] -test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] - -[[package]] -name = "jupyter-events" -version = "0.10.0" -description = "Jupyter Event System library" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_events-0.10.0-py3-none-any.whl", hash = "sha256:4b72130875e59d57716d327ea70d3ebc3af1944d3717e5a498b8a06c6c159960"}, - {file = "jupyter_events-0.10.0.tar.gz", hash = "sha256:670b8229d3cc882ec782144ed22e0d29e1c2d639263f92ca8383e66682845e22"}, -] - -[package.dependencies] -jsonschema = {version = ">=4.18.0", extras = ["format-nongpl"]} -python-json-logger = ">=2.0.4" -pyyaml = ">=5.3" -referencing = "*" -rfc3339-validator = "*" -rfc3986-validator = ">=0.1.1" -traitlets = ">=5.3" - -[package.extras] -cli = ["click", "rich"] -docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"] -test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] - -[[package]] -name = "jupyter-lsp" -version = "2.2.5" -description = "Multi-Language Server WebSocket proxy for Jupyter Notebook/Lab server" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001"}, - {file = "jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jupyter-server = ">=1.1.2" - -[[package]] -name = "jupyter-server" -version = "2.14.1" -description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_server-2.14.1-py3-none-any.whl", hash = "sha256:16f7177c3a4ea8fe37784e2d31271981a812f0b2874af17339031dc3510cc2a5"}, - {file = "jupyter_server-2.14.1.tar.gz", hash = "sha256:12558d158ec7a0653bf96cc272bc7ad79e0127d503b982ed144399346694f726"}, -] - -[package.dependencies] -anyio = ">=3.1.0" -argon2-cffi = ">=21.1" -jinja2 = ">=3.0.3" -jupyter-client = ">=7.4.4" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -jupyter-events = ">=0.9.0" -jupyter-server-terminals = ">=0.4.4" -nbconvert = ">=6.4.4" -nbformat = ">=5.3.0" -overrides = ">=5.0" -packaging = ">=22.0" -prometheus-client = ">=0.9" -pywinpty = {version = ">=2.0.1", markers = "os_name == \"nt\""} -pyzmq = ">=24" -send2trash = ">=1.8.2" -terminado = ">=0.8.3" -tornado = ">=6.2.0" -traitlets = ">=5.6.0" -websocket-client = ">=1.7" - -[package.extras] -docs = ["ipykernel", "jinja2", "jupyter-client", "myst-parser", "nbformat", "prometheus-client", "pydata-sphinx-theme", "send2trash", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-openapi (>=0.8.0)", "sphinxcontrib-spelling", "sphinxemoji", "tornado", "typing-extensions"] -test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0,<9)", "pytest-console-scripts", "pytest-jupyter[server] (>=0.7)", "pytest-timeout", "requests"] - -[[package]] -name = "jupyter-server-terminals" -version = "0.5.3" -description = "A Jupyter Server Extension Providing Terminals." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa"}, - {file = "jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269"}, -] - -[package.dependencies] -pywinpty = {version = ">=2.0.3", markers = "os_name == \"nt\""} -terminado = ">=0.8.3" - -[package.extras] -docs = ["jinja2", "jupyter-server", "mistune (<4.0)", "myst-parser", "nbformat", "packaging", "pydata-sphinx-theme", "sphinxcontrib-github-alt", "sphinxcontrib-openapi", "sphinxcontrib-spelling", "sphinxemoji", "tornado"] -test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>=0.5.3)", "pytest-timeout"] - -[[package]] -name = "jupyterlab" -version = "4.2.2" -description = "JupyterLab computational environment" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyterlab-4.2.2-py3-none-any.whl", hash = "sha256:59ee9b839f43308c3dfd55d72d1f1a299ed42a7f91f2d1afe9c12a783f9e525f"}, - {file = "jupyterlab-4.2.2.tar.gz", hash = "sha256:a534b6a25719a92a40d514fb133a9fe8f0d9981b0bbce5d8a5fcaa33344a3038"}, -] - -[package.dependencies] -async-lru = ">=1.0.0" -httpx = ">=0.25.0" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -ipykernel = ">=6.5.0" -jinja2 = ">=3.0.3" -jupyter-core = "*" -jupyter-lsp = ">=2.0.0" -jupyter-server = ">=2.4.0,<3" -jupyterlab-server = ">=2.27.1,<3" -notebook-shim = ">=0.2" -packaging = "*" -setuptools = ">=40.1.0" -tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} -tornado = ">=6.2.0" -traitlets = "*" - -[package.extras] -dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.3.5)"] -docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] -docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] -test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] -upgrade-extension = ["copier (>=8,<10)", "jinja2-time (<0.3)", "pydantic (<2.0)", "pyyaml-include (<2.0)", "tomli-w (<2.0)"] - -[[package]] -name = "jupyterlab-pygments" -version = "0.3.0" -description = "Pygments theme using JupyterLab CSS variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"}, - {file = "jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d"}, -] - -[[package]] -name = "jupyterlab-server" -version = "2.27.2" -description = "A set of server components for JupyterLab and JupyterLab like applications." -optional = false -python-versions = ">=3.8" -files = [ - {file = "jupyterlab_server-2.27.2-py3-none-any.whl", hash = "sha256:54aa2d64fd86383b5438d9f0c032f043c4d8c0264b8af9f60bd061157466ea43"}, - {file = "jupyterlab_server-2.27.2.tar.gz", hash = "sha256:15cbb349dc45e954e09bacf81b9f9bcb10815ff660fb2034ecd7417db3a7ea27"}, -] - -[package.dependencies] -babel = ">=2.10" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.10\""} -jinja2 = ">=3.0.3" -json5 = ">=0.9.0" -jsonschema = ">=4.18.0" -jupyter-server = ">=1.21,<3" -packaging = ">=21.3" -requests = ">=2.31" - -[package.extras] -docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-copybutton", "sphinxcontrib-openapi (>0.8)"] -openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] -test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0,<8)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] - -[[package]] -name = "jupyterlab-widgets" -version = "3.0.11" -description = "Jupyter interactive widgets for JupyterLab" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jupyterlab_widgets-3.0.11-py3-none-any.whl", hash = "sha256:78287fd86d20744ace330a61625024cf5521e1c012a352ddc0a3cdc2348becd0"}, - {file = "jupyterlab_widgets-3.0.11.tar.gz", hash = "sha256:dd5ac679593c969af29c9bed054c24f26842baa51352114736756bc035deee27"}, -] - [[package]] name = "langgraph-cli" -version = "0.1.48" +version = "0.1.52" description = "CLI for interacting with LangGraph API" optional = false python-versions = "^3.9.0,<4.0" @@ -1117,11 +164,11 @@ click = "^8.1.7" [package.source] type = "directory" -url = "../../cli" +url = ".." [[package]] name = "langgraph-sdk" -version = "0.1.23" +version = "0.1.29" description = "SDK for interacting with LangGraph API" optional = false python-versions = "^3.9.0,<4.0" @@ -1137,232 +184,6 @@ orjson = ">=3.10.1" type = "directory" url = "../../sdk-py" -[[package]] -name = "markupsafe" -version = "2.1.5" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, -] - -[[package]] -name = "matplotlib-inline" -version = "0.1.7" -description = "Inline Matplotlib backend for Jupyter" -optional = false -python-versions = ">=3.8" -files = [ - {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, - {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, -] - -[package.dependencies] -traitlets = "*" - -[[package]] -name = "mistune" -version = "3.0.2" -description = "A sane and fast Markdown parser with useful plugins and renderers" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, - {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, -] - -[[package]] -name = "nbclient" -version = "0.10.0" -description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, - {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, -] - -[package.dependencies] -jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -nbformat = ">=5.1" -traitlets = ">=5.4" - -[package.extras] -dev = ["pre-commit"] -docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] -test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] - -[[package]] -name = "nbconvert" -version = "7.16.4" -description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." -optional = false -python-versions = ">=3.8" -files = [ - {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"}, - {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"}, -] - -[package.dependencies] -beautifulsoup4 = "*" -bleach = "!=5.0.0" -defusedxml = "*" -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} -jinja2 = ">=3.0" -jupyter-core = ">=4.7" -jupyterlab-pygments = "*" -markupsafe = ">=2.0" -mistune = ">=2.0.3,<4" -nbclient = ">=0.5.0" -nbformat = ">=5.7" -packaging = "*" -pandocfilters = ">=1.4.1" -pygments = ">=2.4.1" -tinycss2 = "*" -traitlets = ">=5.1" - -[package.extras] -all = ["flaky", "ipykernel", "ipython", "ipywidgets (>=7.5)", "myst-parser", "nbsphinx (>=0.2.12)", "playwright", "pydata-sphinx-theme", "pyqtwebengine (>=5.15)", "pytest (>=7)", "sphinx (==5.0.2)", "sphinxcontrib-spelling", "tornado (>=6.1)"] -docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"] -qtpdf = ["pyqtwebengine (>=5.15)"] -qtpng = ["pyqtwebengine (>=5.15)"] -serve = ["tornado (>=6.1)"] -test = ["flaky", "ipykernel", "ipywidgets (>=7.5)", "pytest (>=7)"] -webpdf = ["playwright"] - -[[package]] -name = "nbformat" -version = "5.10.4" -description = "The Jupyter Notebook format" -optional = false -python-versions = ">=3.8" -files = [ - {file = "nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b"}, - {file = "nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a"}, -] - -[package.dependencies] -fastjsonschema = ">=2.15" -jsonschema = ">=2.6" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -traitlets = ">=5.1" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["pep440", "pre-commit", "pytest", "testpath"] - -[[package]] -name = "nest-asyncio" -version = "1.6.0" -description = "Patch asyncio to allow nested event loops" -optional = false -python-versions = ">=3.5" -files = [ - {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, - {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, -] - -[[package]] -name = "notebook" -version = "7.2.1" -description = "Jupyter Notebook - A web-based notebook environment for interactive computing" -optional = false -python-versions = ">=3.8" -files = [ - {file = "notebook-7.2.1-py3-none-any.whl", hash = "sha256:f45489a3995746f2195a137e0773e2130960b51c9ac3ce257dbc2705aab3a6ca"}, - {file = "notebook-7.2.1.tar.gz", hash = "sha256:4287b6da59740b32173d01d641f763d292f49c30e7a51b89c46ba8473126341e"}, -] - -[package.dependencies] -jupyter-server = ">=2.4.0,<3" -jupyterlab = ">=4.2.0,<4.3" -jupyterlab-server = ">=2.27.1,<3" -notebook-shim = ">=0.2,<0.3" -tornado = ">=6.2.0" - -[package.extras] -dev = ["hatch", "pre-commit"] -docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.27.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] - -[[package]] -name = "notebook-shim" -version = "0.2.4" -description = "A shim layer for notebook traits and config" -optional = false -python-versions = ">=3.7" -files = [ - {file = "notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef"}, - {file = "notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb"}, -] - -[package.dependencies] -jupyter-server = ">=1.8,<3" - -[package.extras] -test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync"] - [[package]] name = "orjson" version = "3.10.5" @@ -1418,667 +239,6 @@ files = [ {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"}, ] -[[package]] -name = "overrides" -version = "7.7.0" -description = "A decorator to automatically detect mismatch when overriding a method." -optional = false -python-versions = ">=3.6" -files = [ - {file = "overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49"}, - {file = "overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a"}, -] - -[[package]] -name = "packaging" -version = "24.1" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "pandocfilters" -version = "1.5.1" -description = "Utilities for writing pandoc filters in python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc"}, - {file = "pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e"}, -] - -[[package]] -name = "parso" -version = "0.8.4" -description = "A Python Parser" -optional = false -python-versions = ">=3.6" -files = [ - {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, - {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, -] - -[package.extras] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["docopt", "pytest"] - -[[package]] -name = "pexpect" -version = "4.9.0" -description = "Pexpect allows easy control of interactive console applications." -optional = false -python-versions = "*" -files = [ - {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, - {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, -] - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "platformdirs" -version = "4.2.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] - -[[package]] -name = "prometheus-client" -version = "0.20.0" -description = "Python client for the Prometheus monitoring system." -optional = false -python-versions = ">=3.8" -files = [ - {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, - {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, -] - -[package.extras] -twisted = ["twisted"] - -[[package]] -name = "prompt-toolkit" -version = "3.0.47" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, - {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "psutil" -version = "6.0.0" -description = "Cross-platform lib for process and system monitoring in Python." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, - {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, - {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, - {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, - {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, - {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, - {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, - {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, - {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, - {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, -] - -[package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -optional = false -python-versions = "*" -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - -[[package]] -name = "pure-eval" -version = "0.2.2" -description = "Safely evaluate AST nodes without side effects" -optional = false -python-versions = "*" -files = [ - {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, - {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pygments" -version = "2.18.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-json-logger" -version = "2.0.7" -description = "A python library adding a json log formatter" -optional = false -python-versions = ">=3.6" -files = [ - {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, - {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, -] - -[[package]] -name = "pywin32" -version = "306" -description = "Python for Window Extensions" -optional = false -python-versions = "*" -files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, - {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, -] - -[[package]] -name = "pywinpty" -version = "2.0.13" -description = "Pseudo terminal support for Windows from Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pywinpty-2.0.13-cp310-none-win_amd64.whl", hash = "sha256:697bff211fb5a6508fee2dc6ff174ce03f34a9a233df9d8b5fe9c8ce4d5eaf56"}, - {file = "pywinpty-2.0.13-cp311-none-win_amd64.whl", hash = "sha256:b96fb14698db1284db84ca38c79f15b4cfdc3172065b5137383910567591fa99"}, - {file = "pywinpty-2.0.13-cp312-none-win_amd64.whl", hash = "sha256:2fd876b82ca750bb1333236ce98488c1be96b08f4f7647cfdf4129dfad83c2d4"}, - {file = "pywinpty-2.0.13-cp38-none-win_amd64.whl", hash = "sha256:61d420c2116c0212808d31625611b51caf621fe67f8a6377e2e8b617ea1c1f7d"}, - {file = "pywinpty-2.0.13-cp39-none-win_amd64.whl", hash = "sha256:71cb613a9ee24174730ac7ae439fd179ca34ccb8c5349e8d7b72ab5dea2c6f4b"}, - {file = "pywinpty-2.0.13.tar.gz", hash = "sha256:c34e32351a3313ddd0d7da23d27f835c860d32fe4ac814d372a3ea9594f41dde"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "pyzmq" -version = "26.0.3" -description = "Python bindings for 0MQ" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:44dd6fc3034f1eaa72ece33588867df9e006a7303725a12d64c3dff92330f625"}, - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acb704195a71ac5ea5ecf2811c9ee19ecdc62b91878528302dd0be1b9451cc90"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbb9c997932473a27afa93954bb77a9f9b786b4ccf718d903f35da3232317de"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bcb34f869d431799c3ee7d516554797f7760cb2198ecaa89c3f176f72d062be"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ece17ec5f20d7d9b442e5174ae9f020365d01ba7c112205a4d59cf19dc38ee"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ba6e5e6588e49139a0979d03a7deb9c734bde647b9a8808f26acf9c547cab1bf"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3bf8b000a4e2967e6dfdd8656cd0757d18c7e5ce3d16339e550bd462f4857e59"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2136f64fbb86451dbbf70223635a468272dd20075f988a102bf8a3f194a411dc"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e8918973fbd34e7814f59143c5f600ecd38b8038161239fd1a3d33d5817a38b8"}, - {file = "pyzmq-26.0.3-cp310-cp310-win32.whl", hash = "sha256:0aaf982e68a7ac284377d051c742610220fd06d330dcd4c4dbb4cdd77c22a537"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f1a9b7d00fdf60b4039f4455afd031fe85ee8305b019334b72dcf73c567edc47"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:80b12f25d805a919d53efc0a5ad7c0c0326f13b4eae981a5d7b7cc343318ebb7"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a72a84570f84c374b4c287183debc776dc319d3e8ce6b6a0041ce2e400de3f32"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ca684ee649b55fd8f378127ac8462fb6c85f251c2fb027eb3c887e8ee347bcd"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e222562dc0f38571c8b1ffdae9d7adb866363134299264a1958d077800b193b7"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17cde1db0754c35a91ac00b22b25c11da6eec5746431d6e5092f0cd31a3fea9"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7c0c0b3244bb2275abe255d4a30c050d541c6cb18b870975553f1fb6f37527"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac97a21de3712afe6a6c071abfad40a6224fd14fa6ff0ff8d0c6e6cd4e2f807a"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88b88282e55fa39dd556d7fc04160bcf39dea015f78e0cecec8ff4f06c1fc2b5"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72b67f966b57dbd18dcc7efbc1c7fc9f5f983e572db1877081f075004614fcdd"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4b6cecbbf3b7380f3b61de3a7b93cb721125dc125c854c14ddc91225ba52f83"}, - {file = "pyzmq-26.0.3-cp311-cp311-win32.whl", hash = "sha256:eed56b6a39216d31ff8cd2f1d048b5bf1700e4b32a01b14379c3b6dde9ce3aa3"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:3191d312c73e3cfd0f0afdf51df8405aafeb0bad71e7ed8f68b24b63c4f36500"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:b6907da3017ef55139cf0e417c5123a84c7332520e73a6902ff1f79046cd3b94"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:068ca17214038ae986d68f4a7021f97e187ed278ab6dccb79f837d765a54d753"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7821d44fe07335bea256b9f1f41474a642ca55fa671dfd9f00af8d68a920c2d4"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb438a26d87c123bb318e5f2b3d86a36060b01f22fbdffd8cf247d52f7c9a2b"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69ea9d6d9baa25a4dc9cef5e2b77b8537827b122214f210dd925132e34ae9b12"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7daa3e1369355766dea11f1d8ef829905c3b9da886ea3152788dc25ee6079e02"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6ca7a9a06b52d0e38ccf6bca1aeff7be178917893f3883f37b75589d42c4ac20"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1b7d0e124948daa4d9686d421ef5087c0516bc6179fdcf8828b8444f8e461a77"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e746524418b70f38550f2190eeee834db8850088c834d4c8406fbb9bc1ae10b2"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b3146f9ae6af82c47a5282ac8803523d381b3b21caeae0327ed2f7ecb718798"}, - {file = "pyzmq-26.0.3-cp312-cp312-win32.whl", hash = "sha256:2b291d1230845871c00c8462c50565a9cd6026fe1228e77ca934470bb7d70ea0"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:926838a535c2c1ea21c903f909a9a54e675c2126728c21381a94ddf37c3cbddf"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:5bf6c237f8c681dfb91b17f8435b2735951f0d1fad10cc5dfd96db110243370b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c0991f5a96a8e620f7691e61178cd8f457b49e17b7d9cfa2067e2a0a89fc1d5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf012d8fcb9f2cf0643b65df3b355fdd74fc0035d70bb5c845e9e30a3a4654b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01fbfbeb8249a68d257f601deb50c70c929dc2dfe683b754659569e502fbd3aa"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8eb19abe87029c18f226d42b8a2c9efdd139d08f8bf6e085dd9075446db450"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5344b896e79800af86ad643408ca9aa303a017f6ebff8cee5a3163c1e9aec987"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:204e0f176fd1d067671157d049466869b3ae1fc51e354708b0dc41cf94e23a3a"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a42db008d58530efa3b881eeee4991146de0b790e095f7ae43ba5cc612decbc5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win32.whl", hash = "sha256:8d7a498671ca87e32b54cb47c82a92b40130a26c5197d392720a1bce1b3c77cf"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3b4032a96410bdc760061b14ed6a33613ffb7f702181ba999df5d16fb96ba16a"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2cc4e280098c1b192c42a849de8de2c8e0f3a84086a76ec5b07bfee29bda7d18"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bde86a2ed3ce587fa2b207424ce15b9a83a9fa14422dcc1c5356a13aed3df9d"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34106f68e20e6ff253c9f596ea50397dbd8699828d55e8fa18bd4323d8d966e6"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ebbbd0e728af5db9b04e56389e2299a57ea8b9dd15c9759153ee2455b32be6ad"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b1d1c631e5940cac5a0b22c5379c86e8df6a4ec277c7a856b714021ab6cfad"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e891ce81edd463b3b4c3b885c5603c00141151dd9c6936d98a680c8c72fe5c67"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9b273ecfbc590a1b98f014ae41e5cf723932f3b53ba9367cfb676f838038b32c"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b32bff85fb02a75ea0b68f21e2412255b5731f3f389ed9aecc13a6752f58ac97"}, - {file = "pyzmq-26.0.3-cp38-cp38-win32.whl", hash = "sha256:f6c21c00478a7bea93caaaef9e7629145d4153b15a8653e8bb4609d4bc70dbfc"}, - {file = "pyzmq-26.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3401613148d93ef0fd9aabdbddb212de3db7a4475367f49f590c837355343972"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"}, - {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"}, - {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"}, -] - -[package.dependencies] -cffi = {version = "*", markers = "implementation_name == \"pypy\""} - -[[package]] -name = "qtconsole" -version = "5.5.2" -description = "Jupyter Qt console" -optional = false -python-versions = ">=3.8" -files = [ - {file = "qtconsole-5.5.2-py3-none-any.whl", hash = "sha256:42d745f3d05d36240244a04e1e1ec2a86d5d9b6edb16dbdef582ccb629e87e0b"}, - {file = "qtconsole-5.5.2.tar.gz", hash = "sha256:6b5fb11274b297463706af84dcbbd5c92273b1f619e6d25d08874b0a88516989"}, -] - -[package.dependencies] -ipykernel = ">=4.1" -jupyter-client = ">=4.1" -jupyter-core = "*" -packaging = "*" -pygments = "*" -pyzmq = ">=17.1" -qtpy = ">=2.4.0" -traitlets = "<5.2.1 || >5.2.1,<5.2.2 || >5.2.2" - -[package.extras] -doc = ["Sphinx (>=1.3)"] -test = ["flaky", "pytest", "pytest-qt"] - -[[package]] -name = "qtpy" -version = "2.4.1" -description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." -optional = false -python-versions = ">=3.7" -files = [ - {file = "QtPy-2.4.1-py3-none-any.whl", hash = "sha256:1c1d8c4fa2c884ae742b069151b0abe15b3f70491f3972698c683b8e38de839b"}, - {file = "QtPy-2.4.1.tar.gz", hash = "sha256:a5a15ffd519550a1361bdc56ffc07fda56a6af7292f17c7b395d4083af632987"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -test = ["pytest (>=6,!=7.0.0,!=7.0.1)", "pytest-cov (>=3.0.0)", "pytest-qt"] - -[[package]] -name = "referencing" -version = "0.35.1" -description = "JSON Referencing + Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, - {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -rpds-py = ">=0.7.0" - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -description = "A pure python RFC3339 validator" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, - {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, -] - -[package.dependencies] -six = "*" - -[[package]] -name = "rfc3986-validator" -version = "0.1.1" -description = "Pure python rfc3986 validator" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"}, - {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, -] - -[[package]] -name = "rpds-py" -version = "0.18.1" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "rpds_py-0.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53"}, - {file = "rpds_py-0.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0"}, - {file = "rpds_py-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da"}, - {file = "rpds_py-0.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1"}, - {file = "rpds_py-0.18.1-cp310-none-win32.whl", hash = "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333"}, - {file = "rpds_py-0.18.1-cp310-none-win_amd64.whl", hash = "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a"}, - {file = "rpds_py-0.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8"}, - {file = "rpds_py-0.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100"}, - {file = "rpds_py-0.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e"}, - {file = "rpds_py-0.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88"}, - {file = "rpds_py-0.18.1-cp311-none-win32.whl", hash = "sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb"}, - {file = "rpds_py-0.18.1-cp311-none-win_amd64.whl", hash = "sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2"}, - {file = "rpds_py-0.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3"}, - {file = "rpds_py-0.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8"}, - {file = "rpds_py-0.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac"}, - {file = "rpds_py-0.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a"}, - {file = "rpds_py-0.18.1-cp312-none-win32.whl", hash = "sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6"}, - {file = "rpds_py-0.18.1-cp312-none-win_amd64.whl", hash = "sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72"}, - {file = "rpds_py-0.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74"}, - {file = "rpds_py-0.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5"}, - {file = "rpds_py-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e"}, - {file = "rpds_py-0.18.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc"}, - {file = "rpds_py-0.18.1-cp38-none-win32.whl", hash = "sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9"}, - {file = "rpds_py-0.18.1-cp38-none-win_amd64.whl", hash = "sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2"}, - {file = "rpds_py-0.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93"}, - {file = "rpds_py-0.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab"}, - {file = "rpds_py-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b"}, - {file = "rpds_py-0.18.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26"}, - {file = "rpds_py-0.18.1-cp39-none-win32.whl", hash = "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360"}, - {file = "rpds_py-0.18.1-cp39-none-win_amd64.whl", hash = "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c"}, - {file = "rpds_py-0.18.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909"}, - {file = "rpds_py-0.18.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49"}, - {file = "rpds_py-0.18.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e"}, - {file = "rpds_py-0.18.1.tar.gz", hash = "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f"}, -] - -[[package]] -name = "send2trash" -version = "1.8.3" -description = "Send file to trash natively under Mac OS X, Windows and Linux" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9"}, - {file = "Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf"}, -] - -[package.extras] -nativelib = ["pyobjc-framework-Cocoa", "pywin32"] -objc = ["pyobjc-framework-Cocoa"] -win32 = ["pywin32"] - -[[package]] -name = "setuptools" -version = "70.1.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-70.1.0-py3-none-any.whl", hash = "sha256:d9b8b771455a97c8a9f3ab3448ebe0b29b5e105f1228bba41028be116985a267"}, - {file = "setuptools-70.1.0.tar.gz", hash = "sha256:01a1e793faa5bd89abc851fa15d0a0db26f160890c7102cd8dce643e886b47f5"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -2090,132 +250,6 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] -[[package]] -name = "soupsieve" -version = "2.5" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, -] - -[[package]] -name = "stack-data" -version = "0.6.3" -description = "Extract data from python stack frames and tracebacks for informative displays" -optional = false -python-versions = "*" -files = [ - {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, - {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, -] - -[package.dependencies] -asttokens = ">=2.1.0" -executing = ">=1.2.0" -pure-eval = "*" - -[package.extras] -tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] - -[[package]] -name = "terminado" -version = "0.18.1" -description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." -optional = false -python-versions = ">=3.8" -files = [ - {file = "terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0"}, - {file = "terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e"}, -] - -[package.dependencies] -ptyprocess = {version = "*", markers = "os_name != \"nt\""} -pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} -tornado = ">=6.1.0" - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["pre-commit", "pytest (>=7.0)", "pytest-timeout"] -typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] - -[[package]] -name = "tinycss2" -version = "1.3.0" -description = "A tiny CSS parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"}, - {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"}, -] - -[package.dependencies] -webencodings = ">=0.4" - -[package.extras] -doc = ["sphinx", "sphinx_rtd_theme"] -test = ["pytest", "ruff"] - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tornado" -version = "6.4.1" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -optional = false -python-versions = ">=3.8" -files = [ - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, - {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, - {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, - {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, -] - -[[package]] -name = "traitlets" -version = "5.14.3" -description = "Traitlets Python configuration system" -optional = false -python-versions = ">=3.8" -files = [ - {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, - {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, -] - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20240316" -description = "Typing stubs for python-dateutil" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, - {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, -] - [[package]] name = "typing-extensions" version = "4.12.2" @@ -2227,117 +261,7 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] -[[package]] -name = "uri-template" -version = "1.3.0" -description = "RFC 6570 URI Template Processor" -optional = false -python-versions = ">=3.7" -files = [ - {file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"}, - {file = "uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363"}, -] - -[package.extras] -dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake8-commas", "flake8-comprehensions", "flake8-continuation", "flake8-datetimez", "flake8-docstrings", "flake8-import-order", "flake8-literal", "flake8-modern-annotations", "flake8-noqa", "flake8-pyproject", "flake8-requirements", "flake8-typechecking-import", "flake8-use-fstring", "mypy", "pep8-naming", "types-PyYAML"] - -[[package]] -name = "urllib3" -version = "2.2.2" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "wcwidth" -version = "0.2.13" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = "*" -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[[package]] -name = "webcolors" -version = "24.6.0" -description = "A library for working with the color formats defined by HTML and CSS." -optional = false -python-versions = ">=3.8" -files = [ - {file = "webcolors-24.6.0-py3-none-any.whl", hash = "sha256:8cf5bc7e28defd1d48b9e83d5fc30741328305a8195c29a8e668fa45586568a1"}, - {file = "webcolors-24.6.0.tar.gz", hash = "sha256:1d160d1de46b3e81e58d0a280d0c78b467dc80f47294b91b1ad8029d2cedb55b"}, -] - -[package.extras] -docs = ["furo", "sphinx", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-notfound-page", "sphinxext-opengraph"] -tests = ["coverage[toml]"] - -[[package]] -name = "webencodings" -version = "0.5.1" -description = "Character encoding aliases for legacy web content" -optional = false -python-versions = "*" -files = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] - -[[package]] -name = "websocket-client" -version = "1.8.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.8" -files = [ - {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, - {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["websockets"] - -[[package]] -name = "widgetsnbextension" -version = "4.0.11" -description = "Jupyter interactive widgets for Jupyter Notebook" -optional = false -python-versions = ">=3.7" -files = [ - {file = "widgetsnbextension-4.0.11-py3-none-any.whl", hash = "sha256:55d4d6949d100e0d08b94948a42efc3ed6dfdc0e9468b2c4b128c9a2ce3a7a36"}, - {file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"}, -] - -[[package]] -name = "zipp" -version = "3.19.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, -] - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "47f805bc137b65d4f0c807763eefcb24af09c1dccda03917a23a05bef00d7341" +content-hash = "ec5109729f30d2033a10a10e8f8d3ed94c7d96d5d31025b4815b0123664bb063" diff --git a/libs/cli/examples/pyproject.toml b/libs/cli/examples/pyproject.toml index 5399e9184..c4d15111c 100644 --- a/libs/cli/examples/pyproject.toml +++ b/libs/cli/examples/pyproject.toml @@ -9,7 +9,6 @@ package-mode = false [tool.poetry.dependencies] python = "^3.9.0,<4.0" -jupyter = "^1.0.0" langgraph-cli = {path = "../../cli", develop = true} langgraph-sdk = {path = "../../sdk-py", develop = true} From 487fc8a8ca2983c134f26698d10933e5f035c96f Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 15:13:56 -0700 Subject: [PATCH 212/287] Detect subgraphs called inside function nodes --- libs/langgraph/langgraph/graph/graph.py | 8 ++++-- libs/langgraph/langgraph/pregel/__init__.py | 30 ++++++++++++++++----- libs/langgraph/tests/test_pregel.py | 5 +++- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index 04c27ebaa..d466a3c96 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -484,6 +484,10 @@ class CompiledGraph(Pregel): START: graph.add_node(self.get_input_schema(config), START) } end_nodes: dict[str, DrawableNode] = {} + if xray: + subgraphs = dict(self.get_subgraphs()) + else: + subgraphs = {} def add_edge( start: str, end: str, label: Optional[str] = None, conditional: bool = False @@ -503,11 +507,11 @@ class CompiledGraph(Pregel): metadata["__interrupt"] = "after" if xray: subgraph = ( - node.get_graph( + subgraphs[key].get_graph( config=config, xray=xray - 1 if isinstance(xray, int) and xray > 0 else xray, ) - if isinstance(node, CompiledGraph) + if key in subgraphs else node.get_graph(config=config) ) subgraph.trim_first_node() diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 8419eb913..01f0ad91d 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -28,6 +28,7 @@ from langchain_core.load.dump import dumpd from langchain_core.pydantic_v1 import BaseModel, Field, root_validator from langchain_core.runnables import ( Runnable, + RunnableLambda, RunnableSequence, RunnableSerializable, ) @@ -43,6 +44,7 @@ from langchain_core.runnables.config import ( from langchain_core.runnables.utils import ( ConfigurableFieldSpec, create_model, + get_function_nonlocals, get_unique_config_specs, ) from langchain_core.tracers._streaming import _StreamingCallbackHandler @@ -102,6 +104,7 @@ from langgraph.pregel.utils import ( from langgraph.pregel.validate import validate_graph, validate_keys from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry from langgraph.store.base import BaseStore +from langgraph.utils import RunnableCallable WriteValue = Union[ Runnable[Input, Output], @@ -356,13 +359,26 @@ class Pregel( for name, node in self.nodes.items(): # find the subgraph, if any graph: Optional[Pregel] = None - if isinstance(node.bound, Pregel): - graph = node.bound - elif isinstance(node.bound, RunnableSequence): - for runnable in node.bound.steps: - if isinstance(runnable, Pregel): - graph = runnable - break + candidates = [node.bound] + for candidate in candidates: + if isinstance(candidate, Pregel): + graph = candidate + break + elif isinstance(candidate, RunnableSequence): + candidates.extend(candidate.steps) + elif isinstance(candidate, RunnableLambda): + candidates.extend(candidate.deps) + elif isinstance(candidate, RunnableCallable): + if candidate.func is not None: + candidates.extend( + nl.__self__ if hasattr(nl, "__self__") else nl + for nl in get_function_nonlocals(candidate.func) + ) + if candidate.afunc is not None: + candidates.extend( + nl.__self__ if hasattr(nl, "__self__") else nl + for nl in get_function_nonlocals(candidate.afunc) + ) # if found, yield recursively if graph: yield name, graph diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 31484cfe3..2f85540f1 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -10209,10 +10209,13 @@ def test_weather_subgraph( else: return "normal_llm_node" + def weather_graph(state: RouterState): + return subgraph.invoke(state) + graph = StateGraph(RouterState) graph.add_node(router_node) graph.add_node(normal_llm_node) - graph.add_node("weather_graph", subgraph) + graph.add_node("weather_graph", weather_graph) graph.add_edge(START, "router_node") graph.add_conditional_edges("router_node", route_after_prediction) graph.add_edge("normal_llm_node", END) From ca4ebe10317f4c099e192fe4b56f4ad11b536892 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 15:51:57 -0700 Subject: [PATCH 213/287] Update tests --- libs/checkpoint/tests/test_jsonplus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint/tests/test_jsonplus.py b/libs/checkpoint/tests/test_jsonplus.py index ce604ed06..5e413b09f 100644 --- a/libs/checkpoint/tests/test_jsonplus.py +++ b/libs/checkpoint/tests/test_jsonplus.py @@ -122,7 +122,7 @@ def test_serde_jsonplus() -> None: assert dumped == ( "json", - b"""{"path": {"lc": 2, "type": "constructor", "id": ["pathlib", "Path"], "method": null, "args": ["foo", "bar"], "kwargs": {}}, "re": {"lc": 2, "type": "constructor", "id": ["re", "compile"], "method": null, "args": ["foo", 48], "kwargs": {}}, "decimal": {"lc": 2, "type": "constructor", "id": ["decimal", "Decimal"], "method": null, "args": ["1.10101"], "kwargs": {}}, "ip4": {"lc": 2, "type": "constructor", "id": ["ipaddress", "IPv4Address"], "method": null, "args": ["192.168.0.1"], "kwargs": {}}, "deque": {"lc": 2, "type": "constructor", "id": ["collections", "deque"], "method": null, "args": [[1, 2, 3]], "kwargs": {}}, "tzn": {"lc": 2, "type": "constructor", "id": ["zoneinfo", "ZoneInfo"], "method": null, "args": ["America/New_York"], "kwargs": {}}, "date": {"lc": 2, "type": "constructor", "id": ["datetime", "date"], "method": null, "args": [2024, 4, 19], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "time"], "method": null, "args": [23, 4, 57, 51022, {"lc": 2, "type": "constructor", "id": ["datetime", "timezone"], "method": null, "args": [{"lc": 2, "type": "constructor", "id": ["datetime", "timedelta"], "method": null, "args": [0, 86340, 0], "kwargs": {}}], "kwargs": {}}], "kwargs": {"fold": 0}}, "uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "timestamp": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": "model_construct", "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": "construct", "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""", + b"""{"path": {"lc": 2, "type": "constructor", "id": ["pathlib", "Path"], "method": null, "args": ["foo", "bar"], "kwargs": {}}, "re": {"lc": 2, "type": "constructor", "id": ["re", "compile"], "method": null, "args": ["foo", 48], "kwargs": {}}, "decimal": {"lc": 2, "type": "constructor", "id": ["decimal", "Decimal"], "method": null, "args": ["1.10101"], "kwargs": {}}, "ip4": {"lc": 2, "type": "constructor", "id": ["ipaddress", "IPv4Address"], "method": null, "args": ["192.168.0.1"], "kwargs": {}}, "deque": {"lc": 2, "type": "constructor", "id": ["collections", "deque"], "method": null, "args": [[1, 2, 3]], "kwargs": {}}, "tzn": {"lc": 2, "type": "constructor", "id": ["zoneinfo", "ZoneInfo"], "method": null, "args": ["America/New_York"], "kwargs": {}}, "date": {"lc": 2, "type": "constructor", "id": ["datetime", "date"], "method": null, "args": [2024, 4, 19], "kwargs": {}}, "time": {"lc": 2, "type": "constructor", "id": ["datetime", "time"], "method": null, "args": [23, 4, 57, 51022, {"lc": 2, "type": "constructor", "id": ["datetime", "timezone"], "method": null, "args": [{"lc": 2, "type": "constructor", "id": ["datetime", "timedelta"], "method": null, "args": [0, 86340, 0], "kwargs": {}}], "kwargs": {}}], "kwargs": {"fold": 0}}, "uid": {"lc": 2, "type": "constructor", "id": ["uuid", "UUID"], "method": null, "args": ["00000000000000000000000000000001"], "kwargs": {}}, "timestamp": {"lc": 2, "type": "constructor", "id": ["datetime", "datetime"], "method": "fromisoformat", "args": ["2024-04-19T23:04:57.051022+23:59"], "kwargs": {}}, "my_slotted_class": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclassWSlots"], "method": null, "args": [], "kwargs": {"foo": "bar", "bar": 2}}, "my_dataclass": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyDataclass"], "method": null, "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_enum": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyEnum"], "method": null, "args": ["foo"], "kwargs": {}}, "my_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyPydantic"], "method": [null, "model_construct"], "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "my_funny_pydantic": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "MyFunnyPydantic"], "method": [null, "construct"], "args": [], "kwargs": {"foo": "foo", "bar": 1}}, "person": {"lc": 2, "type": "constructor", "id": ["tests", "test_jsonplus", "Person"], "method": null, "args": [], "kwargs": {"name": "foo"}}, "a_bool": true, "a_none": null, "a_str": "foo", "a_str_nuc": "foo\\u0000", "a_str_uc": "foo \xe2\x9b\xb0\xef\xb8\x8f", "a_str_ucuc": "foo \xe2\x9b\xb0\xef\xb8\x8f\\u0000", "a_str_ucucuc": "foo \\\\u26f0\\\\ufe0f", "text": ["Hello", "Python", "Surrogate", "Example", "String", "With", "Surrogates", "Embedded", "In", "The", "Text", "\xe6\x94\xb6\xe8\x8a\xb1\xf0\x9f\x99\x84\xc2\xb7\xe5\x88\xb0"], "an_int": 1, "a_float": 1.1, "runnable_map": {"lc": 1, "type": "constructor", "id": ["langchain", "schema", "runnable", "RunnableParallel"], "kwargs": {"steps__": {}}, "name": "RunnableParallel<>", "graph": {"nodes": [{"id": 0, "type": "schema", "data": "Parallel<>Input"}, {"id": 1, "type": "schema", "data": "Parallel<>Output"}], "edges": []}}}""", ) assert serde.loads_typed(dumped) == { From eca419008c6cfd3431a429ea75da0017a3d60463 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 30 Aug 2024 16:10:57 -0700 Subject: [PATCH 214/287] checkpoint 1.0.8 --- libs/checkpoint/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml index b4c21265f..d7351e33c 100644 --- a/libs/checkpoint/pyproject.toml +++ b/libs/checkpoint/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint" -version = "1.0.7" +version = "1.0.8" description = "Library with base interfaces for LangGraph checkpoint savers." authors = [] license = "MIT" From 51f58279baf8d1eb97d4198b6b5daccdf2fe1c98 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:56:22 -0700 Subject: [PATCH 215/287] Update snapshot --- libs/langgraph/langgraph/graph/state.py | 13 +- libs/langgraph/langgraph/utils.py | 89 ++++++++++++-- .../tests/__snapshots__/test_pregel.ambr | 6 +- libs/langgraph/tests/test_state.py | 18 ++- libs/langgraph/tests/test_utils.py | 113 +++++++++++++----- 5 files changed, 182 insertions(+), 57 deletions(-) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 31fdfb4e3..8f4d8fa3d 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -44,7 +44,7 @@ from langgraph.pregel.read import ChannelRead, PregelNode from langgraph.pregel.types import All, RetryPolicy from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry from langgraph.store.base import BaseStore -from langgraph.utils import RunnableCallable, coerce_to_runnable, is_optional_type +from langgraph.utils import RunnableCallable, coerce_to_runnable, field_is_optional logger = logging.getLogger(__name__) @@ -486,10 +486,6 @@ class CompiledStateGraph(CompiledGraph): __root__=(self.channels[keys[0]].UpdateType, None), ) else: - is_total_false = ( - hasattr(self.builder.input, "__total__") - and self.builder.input.__total__ is False - ) return create_model( # type: ignore[call-overload] self.get_name("Input"), **{ @@ -497,8 +493,11 @@ class CompiledStateGraph(CompiledGraph): self.channels[k].UpdateType, ( None - if is_total_false - or is_optional_type(self.channels[k].UpdateType) + if field_is_optional( + k, + self.channels[k].UpdateType, + self.builder.input, + ) else ... ), ) diff --git a/libs/langgraph/langgraph/utils.py b/libs/langgraph/langgraph/utils.py index 9aa1c257c..81ab9b543 100644 --- a/libs/langgraph/langgraph/utils.py +++ b/libs/langgraph/langgraph/utils.py @@ -4,15 +4,7 @@ import inspect import sys from contextvars import copy_context from functools import partial, wraps -from typing import ( - Any, - AsyncIterator, - Awaitable, - Callable, - Optional, - Union, - get_origin, -) +from typing import Any, AsyncIterator, Awaitable, Callable, Optional, Type, Union from langchain_core.runnables.base import ( Runnable, @@ -27,7 +19,14 @@ from langchain_core.runnables.config import ( var_child_runnable_config, ) from langchain_core.runnables.utils import accepts_config -from typing_extensions import TypeGuard +from typing_extensions import ( + Annotated, + NotRequired, + ReadOnly, + Required, + TypeGuard, + get_origin, +) try: from langchain_core.runnables.config import _set_config_context @@ -191,7 +190,7 @@ def coerce_to_runnable(thing: RunnableLike, *, name: str, trace: bool) -> Runnab ) -def is_optional_type(type_: Any) -> bool: +def _is_optional_type(type_: Any) -> bool: """Check if a type is Optional.""" if hasattr(type_, "__origin__") and hasattr(type_, "__args__"): @@ -200,9 +199,73 @@ def is_optional_type(type_: Any) -> bool: return True if origin is Union: return any( - arg is type(None) or is_optional_type(arg) for arg in type_.__args__ + arg is type(None) or _is_optional_type(arg) for arg in type_.__args__ ) return origin is None if hasattr(type_, "__bound__") and type_.__bound__ is not None: - return is_optional_type(type_.__bound__) + return _is_optional_type(type_.__bound__) return type_ is None + + +def _is_required_type(type_: Any) -> Optional[bool]: + """Check if an annotation is marked as Required/NotRequired. + + Returns: + - True if required + - False if not required + - None if not annotated with either + """ + origin = get_origin(type_) + if origin is Annotated or origin: + # See https://typing.readthedocs.io/en/latest/spec/typeddict.html#interaction-with-annotated + return _is_required_type(type_.__args__[0]) + if origin is Required: + return True + if origin is NotRequired: + return False + return None + + +def _is_readonly_type(type_: Any) -> bool: + """Check if an annotation is marked as ReadOnly. + + Returns: + - True if is read only + - False if not read only + """ + + # See: https://typing.readthedocs.io/en/latest/spec/typeddict.html#typing-readonly-type-qualifier + origin = get_origin(type_) + if origin is Annotated: + return _is_readonly_type(type_.__args__[0]) + if origin is ReadOnly: + return True + return False + + +_DEFAULT_KEYS = frozenset() + + +def field_is_optional(name: str, type_: Any, schema: Type[Any]) -> bool: + """Determine if the field is optional for a graph's input. + + This is based on: + If TypedDict: + - Required/NotRequired + - total=False -> everything optional + - Type annotation (Optional/Union[None]) + """ + optional_keys = getattr(schema, "__optional_keys__", _DEFAULT_KEYS) + if name in optional_keys: + # Either total=False or explicit NotRequired. + # No type annotation trumps this. + return True + if _is_required_type(type_): + # Handle Required[] + # (we already handled NotRequired and total=False) + return False + # Note, we ignore ReadOnly attributes, + # as they don't make much sense. (we don't care if you mutate the state in your node) + # and mutating state in your node has no effect on our graph state. + # Base case is the annotation + return _is_optional_type(type_) diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index ba58cda8c..725955315 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -366,7 +366,7 @@ ''' # --- # name: test_conditional_entrypoint_to_multiple_state_graph - '{"title": "LangGraphInput", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}}' + '{"title": "LangGraphInput", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}, "required": ["locations", "results"]}' # --- # name: test_conditional_entrypoint_to_multiple_state_graph.1 '{"title": "LangGraphOutput", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}}' @@ -4855,7 +4855,7 @@ ''' # --- # name: test_prebuilt_tool_chat - '{"title": "LangGraphInput", "type": "object", "properties": {"messages": {"title": "Messages", "type": "array", "items": {"$ref": "#/definitions/BaseMessage"}}}, "definitions": {"BaseMessage": {"title": "BaseMessage", "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "type"]}}}' + '{"title": "LangGraphInput", "type": "object", "properties": {"messages": {"title": "Messages", "type": "array", "items": {"$ref": "#/definitions/BaseMessage"}}}, "required": ["messages"], "definitions": {"BaseMessage": {"title": "BaseMessage", "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "type"]}}}' # --- # name: test_prebuilt_tool_chat.1 '{"title": "LangGraphOutput", "type": "object", "properties": {"messages": {"title": "Messages", "type": "array", "items": {"$ref": "#/definitions/BaseMessage"}}}, "definitions": {"BaseMessage": {"title": "BaseMessage", "description": "Base abstract message class.\\n\\nMessages are the inputs and outputs of ChatModels.", "type": "object", "properties": {"content": {"title": "Content", "anyOf": [{"type": "string"}, {"type": "array", "items": {"anyOf": [{"type": "string"}, {"type": "object"}]}}]}, "additional_kwargs": {"title": "Additional Kwargs", "type": "object"}, "response_metadata": {"title": "Response Metadata", "type": "object"}, "type": {"title": "Type", "type": "string"}, "name": {"title": "Name", "type": "string"}, "id": {"title": "Id", "type": "string"}}, "required": ["content", "type"]}}}' @@ -5094,7 +5094,7 @@ '{"title": "LangGraphConfig", "type": "object", "properties": {"configurable": {"$ref": "#/definitions/Configurable"}}, "definitions": {"Configurable": {"title": "Configurable", "type": "object", "properties": {"tools": {"title": "Tools", "type": "array", "items": {"type": "string"}}}}}}' # --- # name: test_state_graph_w_config_inherited_state_keys.1 - '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' + '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "required": ["input"], "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' # --- # name: test_state_graph_w_config_inherited_state_keys.2 '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' diff --git a/libs/langgraph/tests/test_state.py b/libs/langgraph/tests/test_state.py index a3a0d2806..005f0ffd1 100644 --- a/libs/langgraph/tests/test_state.py +++ b/libs/langgraph/tests/test_state.py @@ -1,5 +1,5 @@ from typing import Annotated as Annotated2 -from typing import Any, Optional +from typing import Any, NotRequired, Optional, Required import pytest from langchain_core.runnables import RunnableConfig @@ -90,11 +90,19 @@ def test_state_schema_with_type_hint(): @pytest.mark.parametrize("total_", [True, False]) def test_state_schema_optional_values(total_: bool): - class InputState(TypedDict, total=total_): # type: ignore + class SomeParentState(TypedDict): + val0a: str + val0b: Optional[str] + + class InputState(SomeParentState, total=total_): # type: ignore val1: str val2: Optional[str] + val3: Required[str] + val4: NotRequired[dict] + val5: Annotated[Required[str], "foo"] + val6: Annotated[NotRequired[str], "bar"] - class State(InputState): + class State(InputState): # this would be ignored val4: dict builder = StateGraph(State, input=InputState) @@ -112,6 +120,10 @@ def test_state_schema_optional_values(total_: bool): expected_optional = {"val2"} + # The others should always have precedence based on the required annotation + expected_required |= {"val0a", "val3", "val5"} + expected_optional |= {"val0b", "val4", "val6"} + assert set(json_schema.get("required", set())) == expected_required assert ( set(json_schema["properties"].keys()) == expected_required | expected_optional diff --git a/libs/langgraph/tests/test_utils.py b/libs/langgraph/tests/test_utils.py index d5bfb53c7..2e5d2b3bb 100644 --- a/libs/langgraph/tests/test_utils.py +++ b/libs/langgraph/tests/test_utils.py @@ -17,10 +17,16 @@ from unittest.mock import patch import langsmith import pytest +from typing_extensions import Annotated, NotRequired, Required from langgraph.graph import END, StateGraph from langgraph.graph.graph import CompiledGraph -from langgraph.utils import is_async_callable, is_async_generator, is_optional_type +from langgraph.utils import ( + _is_optional_type, + field_is_optional, + is_async_callable, + is_async_generator, +) def test_is_async() -> None: @@ -133,48 +139,93 @@ async def test_runnable_callable_tracing_nested_async(rt_graph: CompiledGraph) - def test_is_optional_type(): - assert is_optional_type(None) - assert not is_optional_type(type(None)) - assert is_optional_type(Optional[list]) - assert not is_optional_type(int) - assert is_optional_type(Optional[Literal[1, 2, 3]]) - assert not is_optional_type(Literal[1, 2, 3]) - assert is_optional_type(Optional[List[int]]) - assert is_optional_type(Optional[Dict[str, int]]) - assert not is_optional_type(List[Optional[int]]) - assert is_optional_type(Union[Optional[str], Optional[int]]) - assert is_optional_type( + assert _is_optional_type(None) + assert not _is_optional_type(type(None)) + assert _is_optional_type(Optional[list]) + assert not _is_optional_type(int) + assert _is_optional_type(Optional[Literal[1, 2, 3]]) + assert not _is_optional_type(Literal[1, 2, 3]) + assert _is_optional_type(Optional[List[int]]) + assert _is_optional_type(Optional[Dict[str, int]]) + assert not _is_optional_type(List[Optional[int]]) + assert _is_optional_type(Union[Optional[str], Optional[int]]) + assert _is_optional_type( Union[ Union[Optional[str], Optional[int]], Union[Optional[float], Optional[dict]] ] ) - assert not is_optional_type(Union[Union[str, int], Union[float, dict]]) + assert not _is_optional_type(Union[Union[str, int], Union[float, dict]]) - assert is_optional_type(Union[int, None]) - assert is_optional_type(Union[str, None, int]) - assert is_optional_type(Union[None, str, int]) - assert not is_optional_type(Union[int, str]) + assert _is_optional_type(Union[int, None]) + assert _is_optional_type(Union[str, None, int]) + assert _is_optional_type(Union[None, str, int]) + assert not _is_optional_type(Union[int, str]) - assert not is_optional_type(Any) # Do we actually want this? - assert is_optional_type(Optional[Any]) + assert not _is_optional_type(Any) # Do we actually want this? + assert _is_optional_type(Optional[Any]) class MyClass: pass - assert is_optional_type(Optional[MyClass]) - assert not is_optional_type(MyClass) - assert is_optional_type(Optional[ForwardRef("MyClass")]) - assert not is_optional_type(ForwardRef("MyClass")) + assert _is_optional_type(Optional[MyClass]) + assert not _is_optional_type(MyClass) + assert _is_optional_type(Optional[ForwardRef("MyClass")]) + assert not _is_optional_type(ForwardRef("MyClass")) - assert is_optional_type(Optional[Union[List[int], Dict[str, Optional[int]]]]) - assert not is_optional_type(Union[List[int], Dict[str, Optional[int]]]) + assert _is_optional_type(Optional[Union[List[int], Dict[str, Optional[int]]]]) + assert not _is_optional_type(Union[List[int], Dict[str, Optional[int]]]) - assert is_optional_type(Optional[Callable[[int], str]]) - assert not is_optional_type(Callable[[int], Optional[str]]) + assert _is_optional_type(Optional[Callable[[int], str]]) + assert not _is_optional_type(Callable[[int], Optional[str]]) T = TypeVar("T") - assert is_optional_type(Optional[T]) - assert not is_optional_type(T) + assert _is_optional_type(Optional[T]) + assert not _is_optional_type(T) - U = TypeVar("U", bound=Optional[T]) - assert is_optional_type(U) + U = TypeVar("U", bound=Optional[T]) # type: ignore + assert _is_optional_type(U) + + +def test_is_required(): + class MyBaseTypedDict(TypedDict): + val_1: Required[Optional[str]] + val_2: Required[str] + val_3: NotRequired[str] + val_4: NotRequired[Optional[str]] + val_5: Annotated[NotRequired[int], "foo"] + val_6: NotRequired[Annotated[int, "foo"]] + val_7: Annotated[Required[int], "foo"] + val_8: Required[Annotated[int, "foo"]] + val_9: Optional[str] + val_10: str + + annos = MyBaseTypedDict.__annotations__ + assert not field_is_optional("val_1", annos["val_1"], MyBaseTypedDict) + assert not field_is_optional("val_2", annos["val_2"], MyBaseTypedDict) + assert field_is_optional("val_3", annos["val_3"], MyBaseTypedDict) + assert field_is_optional("val_4", annos["val_4"], MyBaseTypedDict) + # See https://peps.python.org/pep-0655/#interaction-with-annotated + assert field_is_optional("val_5", annos["val_5"], MyBaseTypedDict) + assert field_is_optional("val_6", annos["val_6"], MyBaseTypedDict) + assert not field_is_optional("val_7", annos["val_7"], MyBaseTypedDict) + assert not field_is_optional("val_8", annos["val_8"], MyBaseTypedDict) + assert field_is_optional("val_9", annos["val_9"], MyBaseTypedDict) + assert not field_is_optional("val_10", annos["val_10"], MyBaseTypedDict) + + class MyChildDict(MyBaseTypedDict): + val_11: int + val_11b: Optional[int] + val_11c: Union[int, None, str] + + class MyGrandChildDict(MyChildDict, total=False): + val_12: int + val_13: Required[str] + + cannos = MyChildDict.__annotations__ + gcannos = MyGrandChildDict.__annotations__ + assert not field_is_optional("val_11", cannos["val_11"], MyChildDict) + assert field_is_optional("val_11b", cannos["val_11b"], MyChildDict) + assert field_is_optional("val_11c", cannos["val_11c"], MyChildDict) + assert field_is_optional("val_12", gcannos["val_12"], MyGrandChildDict) + assert field_is_optional("val_9", gcannos["val_9"], MyGrandChildDict) + assert not field_is_optional("val_13", gcannos["val_13"], MyGrandChildDict) From 55263c640f5a737782730cbc4cde852a6a65b262 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Fri, 30 Aug 2024 16:28:01 -0700 Subject: [PATCH 216/287] Return a default value instead --- libs/langgraph/langgraph/graph/state.py | 6 ++--- libs/langgraph/langgraph/utils.py | 12 +++++---- libs/langgraph/tests/test_utils.py | 34 ++++++++++++------------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 8f4d8fa3d..f576a98cd 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -44,7 +44,7 @@ from langgraph.pregel.read import ChannelRead, PregelNode from langgraph.pregel.types import All, RetryPolicy from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry from langgraph.store.base import BaseStore -from langgraph.utils import RunnableCallable, coerce_to_runnable, field_is_optional +from langgraph.utils import RunnableCallable, coerce_to_runnable, get_field_default logger = logging.getLogger(__name__) @@ -492,13 +492,11 @@ class CompiledStateGraph(CompiledGraph): k: ( self.channels[k].UpdateType, ( - None - if field_is_optional( + get_field_default( k, self.channels[k].UpdateType, self.builder.input, ) - else ... ), ) for k in self.builder.schemas[self.builder.input] diff --git a/libs/langgraph/langgraph/utils.py b/libs/langgraph/langgraph/utils.py index 81ab9b543..1fbcc94e3 100644 --- a/libs/langgraph/langgraph/utils.py +++ b/libs/langgraph/langgraph/utils.py @@ -246,8 +246,8 @@ def _is_readonly_type(type_: Any) -> bool: _DEFAULT_KEYS = frozenset() -def field_is_optional(name: str, type_: Any, schema: Type[Any]) -> bool: - """Determine if the field is optional for a graph's input. +def get_field_default(name: str, type_: Any, schema: Type[Any]) -> Any: + """Determine the default value for a field in a state schema. This is based on: If TypedDict: @@ -259,13 +259,15 @@ def field_is_optional(name: str, type_: Any, schema: Type[Any]) -> bool: if name in optional_keys: # Either total=False or explicit NotRequired. # No type annotation trumps this. - return True + return None if _is_required_type(type_): # Handle Required[] # (we already handled NotRequired and total=False) - return False + return ... # Note, we ignore ReadOnly attributes, # as they don't make much sense. (we don't care if you mutate the state in your node) # and mutating state in your node has no effect on our graph state. # Base case is the annotation - return _is_optional_type(type_) + if _is_optional_type(type_): + return None + return ... diff --git a/libs/langgraph/tests/test_utils.py b/libs/langgraph/tests/test_utils.py index 2e5d2b3bb..936fd2847 100644 --- a/libs/langgraph/tests/test_utils.py +++ b/libs/langgraph/tests/test_utils.py @@ -23,7 +23,7 @@ from langgraph.graph import END, StateGraph from langgraph.graph.graph import CompiledGraph from langgraph.utils import ( _is_optional_type, - field_is_optional, + get_field_default, is_async_callable, is_async_generator, ) @@ -200,17 +200,17 @@ def test_is_required(): val_10: str annos = MyBaseTypedDict.__annotations__ - assert not field_is_optional("val_1", annos["val_1"], MyBaseTypedDict) - assert not field_is_optional("val_2", annos["val_2"], MyBaseTypedDict) - assert field_is_optional("val_3", annos["val_3"], MyBaseTypedDict) - assert field_is_optional("val_4", annos["val_4"], MyBaseTypedDict) + assert get_field_default("val_1", annos["val_1"], MyBaseTypedDict) == ... + assert get_field_default("val_2", annos["val_2"], MyBaseTypedDict) == ... + assert get_field_default("val_3", annos["val_3"], MyBaseTypedDict) is None + assert get_field_default("val_4", annos["val_4"], MyBaseTypedDict) is None # See https://peps.python.org/pep-0655/#interaction-with-annotated - assert field_is_optional("val_5", annos["val_5"], MyBaseTypedDict) - assert field_is_optional("val_6", annos["val_6"], MyBaseTypedDict) - assert not field_is_optional("val_7", annos["val_7"], MyBaseTypedDict) - assert not field_is_optional("val_8", annos["val_8"], MyBaseTypedDict) - assert field_is_optional("val_9", annos["val_9"], MyBaseTypedDict) - assert not field_is_optional("val_10", annos["val_10"], MyBaseTypedDict) + assert get_field_default("val_5", annos["val_5"], MyBaseTypedDict) is None + assert get_field_default("val_6", annos["val_6"], MyBaseTypedDict) is None + assert get_field_default("val_7", annos["val_7"], MyBaseTypedDict) == ... + assert get_field_default("val_8", annos["val_8"], MyBaseTypedDict) == ... + assert get_field_default("val_9", annos["val_9"], MyBaseTypedDict) is None + assert get_field_default("val_10", annos["val_10"], MyBaseTypedDict) == ... class MyChildDict(MyBaseTypedDict): val_11: int @@ -223,9 +223,9 @@ def test_is_required(): cannos = MyChildDict.__annotations__ gcannos = MyGrandChildDict.__annotations__ - assert not field_is_optional("val_11", cannos["val_11"], MyChildDict) - assert field_is_optional("val_11b", cannos["val_11b"], MyChildDict) - assert field_is_optional("val_11c", cannos["val_11c"], MyChildDict) - assert field_is_optional("val_12", gcannos["val_12"], MyGrandChildDict) - assert field_is_optional("val_9", gcannos["val_9"], MyGrandChildDict) - assert not field_is_optional("val_13", gcannos["val_13"], MyGrandChildDict) + assert get_field_default("val_11", cannos["val_11"], MyChildDict) == ... + assert get_field_default("val_11b", cannos["val_11b"], MyChildDict) is None + assert get_field_default("val_11c", cannos["val_11c"], MyChildDict) is None + assert get_field_default("val_12", gcannos["val_12"], MyGrandChildDict) is None + assert get_field_default("val_9", gcannos["val_9"], MyGrandChildDict) is None + assert get_field_default("val_13", gcannos["val_13"], MyGrandChildDict) == ... From 01765fbda45c37cb1382fdbf32ce8b2a9cbc56cf Mon Sep 17 00:00:00 2001 From: isaac hershenson Date: Fri, 30 Aug 2024 17:39:45 -0700 Subject: [PATCH 217/287] x --- libs/langgraph/langgraph/pregel/debug.py | 7 +-- libs/langgraph/langgraph/pregel/loop.py | 2 +- libs/langgraph/tests/test_pregel.py | 58 +++++++++++---------- libs/langgraph/tests/test_pregel_async.py | 61 ++++++++++------------- 4 files changed, 56 insertions(+), 72 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index 73f992364..9b20c3c3a 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -104,7 +104,6 @@ def map_debug_task_results( step: int, tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]], stream_keys: Union[str, Sequence[str]], - step_start_time: datetime, ) -> Iterator[DebugOutputTaskResult]: stream_channels_list = ( [stream_keys] if isinstance(stream_keys, str) else stream_keys @@ -123,12 +122,8 @@ def map_debug_task_results( "timestamp": ts.isoformat(), "step": step, "payload": { - "id": str( - uuid5(TASK_NAMESPACE, json.dumps((task.name, step, metadata))) - ), - "task_id": task.task_id, + "id": task.id, "name": task.name, - "node_exec_ms": int((ts - step_start_time).total_seconds() * 1000), "error": next((w[1] for w in writes if w[0] == ERROR), None), "result": [w for w in writes if w[0] in stream_channels_list], "interrupts": [asdict(w[1]) for w in writes if w[0] == INTERRUPT], diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index b2eaac472..7c1d09203 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -241,7 +241,7 @@ class PregelLoop: self.stream.extend( (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) for v in map_debug_task_results( - self.step, [(task, writes)], self.stream_keys + self.step, [(task, writes)], self.stream_keys, ) ) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 7825af3ea..0a69d9385 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -1084,10 +1084,8 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", + "id": AnyStr(), "name": "one", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("inbox", 3)], "error": None, "interrupts": [], @@ -1098,10 +1096,8 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", + "id": AnyStr(), "name": "two", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("output", 13)], "error": None, "interrupts": [], @@ -1123,10 +1119,8 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", + "id": AnyStr(), "name": "two", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("output", 4)], "error": None, "interrupts": [], @@ -6383,9 +6377,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", + "id": AnyStr(), "name": "rewrite_query", - "task_id": AnyStr(), "node_exec_ms": AnyInt(), "result": [("query", "query: what is weather in sf")], "error": None, @@ -6433,9 +6426,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", + "id": AnyStr(), "name": "retriever_two", - "task_id": AnyStr(), "node_exec_ms": AnyInt(), "result": [("docs", ["doc3", "doc4"])], "error": None, @@ -6454,9 +6446,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", + "id": AnyStr(), "name": "retriever_one", - "task_id": AnyStr(), "node_exec_ms": AnyInt(), "result": [("docs", ["doc1", "doc2"])], "error": None, @@ -6496,9 +6487,8 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 3, "payload": { - "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", + "id": AnyStr(), "name": "qa", - "task_id": AnyStr(), "node_exec_ms": AnyInt(), "result": [("answer", "doc1,doc2,doc3,doc4")], "error": None, @@ -6898,16 +6888,19 @@ def test_branch_then( "next": ["prepare"], "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], }, - { + }, + { "type": "task_result", "timestamp": AnyStr(), "step": 1, "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "id": AnyStr(), "name": "prepare", - "task_id": AnyStr(), "node_exec_ms": AnyInt(), "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], + }, }, { "type": "task", @@ -6925,7 +6918,7 @@ def test_branch_then( "timestamp": AnyStr(), "step": 1, "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "id": AnyStr(), "name": "prepare", "result": [("my_key", " prepared")], "error": None, @@ -6960,17 +6953,19 @@ def test_branch_then( }, "next": ["tool_two_slow"], "tasks": [{"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()}], + }, }, { "type": "task_result", "timestamp": AnyStr(), "step": 2, "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "id": AnyStr(), "name": "tool_two_slow", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("my_key", " slow")], + "error": None, + "interrupts": [], + }, }, { "type": "task", @@ -6988,7 +6983,7 @@ def test_branch_then( "timestamp": AnyStr(), "step": 2, "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "id": AnyStr(), "name": "tool_two_slow", "result": [("my_key", " slow")], "error": None, @@ -7024,16 +7019,19 @@ def test_branch_then( "next": ["finish"], "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], }, - { + }, + { "type": "task_result", "timestamp": AnyStr(), "step": 3, "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "id": AnyStr(), "name": "finish", - "task_id": AnyStr(), "node_exec_ms": AnyInt(), - "result": [("my_key", " finished")], + "result": [("my_key", " finished")], + "error": None, + "interrupts": [], + }, }, { "type": "task", @@ -7051,7 +7049,7 @@ def test_branch_then( "timestamp": AnyStr(), "step": 3, "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "id": AnyStr(), "name": "finish", "result": [("my_key", " finished")], "error": None, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 1c63c8565..8208e80f5 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1317,10 +1317,8 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", + "id": AnyStr(), "name": "one", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("inbox", 3)], "error": None, "interrupts": [], @@ -1331,10 +1329,8 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", + "id": AnyStr(), "name": "two", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("output", 13)], "error": None, "interrupts": [], @@ -1356,10 +1352,8 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", + "id": AnyStr(), "name": "two", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("output", 4)], "error": None, "interrupts": [], @@ -5006,10 +5000,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", + "id": AnyStr(), "name": "rewrite_query", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("query", "query: what is weather in sf")], "error": None, "interrupts": [], @@ -5056,10 +5048,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", + "id": AnyStr(), "name": "retriever_two", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("docs", ["doc3", "doc4"])], "error": None, "interrupts": [], @@ -5077,10 +5067,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", + "id": AnyStr(), "name": "retriever_one", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("docs", ["doc1", "doc2"])], "error": None, "interrupts": [], @@ -5119,10 +5107,8 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 3, "payload": { - "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", + "id": AnyStr(), "name": "qa", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("answer", "doc1,doc2,doc3,doc4")], "error": None, "interrupts": [], @@ -5447,17 +5433,19 @@ async def test_branch_then( }, "next": ["prepare"], "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], + }, }, { "type": "task_result", "timestamp": AnyStr(), "step": 1, "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "id": AnyStr(), "name": "prepare", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), - "result": [("my_key", " prepared")], + "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], + }, }, { "type": "task", @@ -5475,7 +5463,7 @@ async def test_branch_then( "timestamp": AnyStr(), "step": 1, "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "id": AnyStr(), "name": "prepare", "result": [("my_key", " prepared")], "error": None, @@ -5510,17 +5498,19 @@ async def test_branch_then( }, "next": ["tool_two_slow"], "tasks": [{"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()}], + }, }, { "type": "task_result", "timestamp": AnyStr(), "step": 2, "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "id": AnyStr(), "name": "tool_two_slow", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("my_key", " slow")], + "error": None, + "interrupts": [], + }, }, { "type": "task", @@ -5538,7 +5528,7 @@ async def test_branch_then( "timestamp": AnyStr(), "step": 2, "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "id": AnyStr(), "name": "tool_two_slow", "result": [("my_key", " slow")], "error": None, @@ -5573,17 +5563,18 @@ async def test_branch_then( }, "next": ["finish"], "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], + }, }, { "type": "task_result", "timestamp": AnyStr(), "step": 3, "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "id": AnyStr(), "name": "finish", - "task_id": AnyStr(), - "node_exec_ms": AnyInt(), "result": [("my_key", " finished")], + "error": None, + "interrupts": [], }, }, { @@ -5602,7 +5593,7 @@ async def test_branch_then( "timestamp": AnyStr(), "step": 3, "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "id": AnyStr(), "name": "finish", "result": [("my_key", " finished")], "error": None, @@ -5730,7 +5721,7 @@ async def test_branch_then( "timestamp": AnyStr(), "step": 1, "payload": { - "id": "1a591be4-f85c-558f-8d00-1ccac0d1877f", + "id": AnyStr(), "name": "prepare", "result": [("my_key", " prepared")], "error": None, From d14477c891271392ffb788b2390306409250c70a Mon Sep 17 00:00:00 2001 From: isaac hershenson Date: Fri, 30 Aug 2024 18:22:22 -0700 Subject: [PATCH 218/287] x --- libs/langgraph/tests/test_pregel.py | 57 +++-------------------- libs/langgraph/tests/test_pregel_async.py | 37 --------------- 2 files changed, 7 insertions(+), 87 deletions(-) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 0a69d9385..74a16b010 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -69,7 +69,6 @@ from langgraph.pregel import ( StateSnapshot, ) from langgraph.pregel.retry import RetryPolicy -from tests.any_int import AnyInt from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore from tests.any_str import AnyDict, AnyStr, AnyVersion, UnsortedSequence @@ -6379,7 +6378,6 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": AnyStr(), "name": "rewrite_query", - "node_exec_ms": AnyInt(), "result": [("query", "query: what is weather in sf")], "error": None, "interrupts": [], @@ -6428,7 +6426,6 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": AnyStr(), "name": "retriever_two", - "node_exec_ms": AnyInt(), "result": [("docs", ["doc3", "doc4"])], "error": None, "interrupts": [], @@ -6448,7 +6445,6 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": AnyStr(), "name": "retriever_one", - "node_exec_ms": AnyInt(), "result": [("docs", ["doc1", "doc2"])], "error": None, "interrupts": [], @@ -6489,7 +6485,6 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "payload": { "id": AnyStr(), "name": "qa", - "node_exec_ms": AnyInt(), "result": [("answer", "doc1,doc2,doc3,doc4")], "error": None, "interrupts": [], @@ -6880,27 +6875,14 @@ def test_branch_then( "market": "DE", }, "metadata": { - "parents": {}, - "source": "loop", - "step": 0, - "writes": None, - }, - "next": ["prepare"], - "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": AnyStr(), - "name": "prepare", - "node_exec_ms": AnyInt(), - "result": [("my_key", " prepared")], - "error": None, - "interrupts": [], + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, }, + "next": ["prepare"], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], + }, }, { "type": "task", @@ -6955,18 +6937,6 @@ def test_branch_then( "tasks": [{"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()}], }, }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": AnyStr(), - "name": "tool_two_slow", - "result": [("my_key", " slow")], - "error": None, - "interrupts": [], - }, - }, { "type": "task", "timestamp": AnyStr(), @@ -7020,19 +6990,6 @@ def test_branch_then( "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], }, }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "id": AnyStr(), - "name": "finish", - "node_exec_ms": AnyInt(), - "result": [("my_key", " finished")], - "error": None, - "interrupts": [], - }, - }, { "type": "task", "timestamp": AnyStr(), diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 8208e80f5..70d90bb62 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -68,7 +68,6 @@ from langgraph.pregel import ( StateSnapshot, ) from langgraph.pregel.retry import RetryPolicy -from tests.any_int import AnyInt from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore from tests.any_str import AnyDict, AnyStr, AnyVersion, UnsortedSequence @@ -5435,18 +5434,6 @@ async def test_branch_then( "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], }, }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": AnyStr(), - "name": "prepare", - "result": [("my_key", " prepared")], - "error": None, - "interrupts": [], - }, - }, { "type": "task", "timestamp": AnyStr(), @@ -5500,18 +5487,6 @@ async def test_branch_then( "tasks": [{"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()}], }, }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": AnyStr(), - "name": "tool_two_slow", - "result": [("my_key", " slow")], - "error": None, - "interrupts": [], - }, - }, { "type": "task", "timestamp": AnyStr(), @@ -5565,18 +5540,6 @@ async def test_branch_then( "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], }, }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "id": AnyStr(), - "name": "finish", - "result": [("my_key", " finished")], - "error": None, - "interrupts": [], - }, - }, { "type": "task", "timestamp": AnyStr(), From ee5801511b7f5a0b099305914e9b4a5a8fca518f Mon Sep 17 00:00:00 2001 From: isaac hershenson Date: Fri, 30 Aug 2024 18:25:02 -0700 Subject: [PATCH 219/287] fmt --- libs/langgraph/langgraph/pregel/__init__.py | 5 ----- libs/langgraph/langgraph/pregel/loop.py | 4 +++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 7cd946430..01f0ad91d 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -4,7 +4,6 @@ import asyncio import concurrent.futures import time from collections import deque -from datetime import datetime, timezone from functools import partial from typing import ( Any, @@ -1232,8 +1231,6 @@ class Pregel( interrupt_after=interrupt_after, manager=run_manager, ): - # start time of superstep - step_start_time = datetime.now(timezone.utc) # debug flag if debug: print_step_checkpoint( @@ -1485,8 +1482,6 @@ class Pregel( interrupt_after=interrupt_after, manager=run_manager, ): - # start time of superstep - step_start_time = datetime.now(timezone.utc) # debug flag if debug: print_step_checkpoint( diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 7c1d09203..3e55d0df8 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -241,7 +241,9 @@ class PregelLoop: self.stream.extend( (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) for v in map_debug_task_results( - self.step, [(task, writes)], self.stream_keys, + self.step, + [(task, writes)], + self.stream_keys, ) ) From 1191dc1b4880624fc5c5efe93e8bac24b91c5e49 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Fri, 30 Aug 2024 23:52:41 -0700 Subject: [PATCH 220/287] Handle py3.9 --- libs/langgraph/langgraph/utils.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/libs/langgraph/langgraph/utils.py b/libs/langgraph/langgraph/utils.py index 5cab1b7f9..cc3fd4760 100644 --- a/libs/langgraph/langgraph/utils.py +++ b/libs/langgraph/langgraph/utils.py @@ -218,13 +218,13 @@ def _is_required_type(type_: Any) -> Optional[bool]: - None if not annotated with either """ origin = get_origin(type_) - if origin is Annotated or origin: - # See https://typing.readthedocs.io/en/latest/spec/typeddict.html#interaction-with-annotated - return _is_required_type(type_.__args__[0]) if origin is Required: return True if origin is NotRequired: return False + if origin is Annotated or getattr(origin, "__args__", None): + # See https://typing.readthedocs.io/en/latest/spec/typeddict.html#interaction-with-annotated + return _is_required_type(type_.__args__[0]) return None @@ -258,14 +258,21 @@ def get_field_default(name: str, type_: Any, schema: Type[Any]) -> Any: - Type annotation (Optional/Union[None]) """ optional_keys = getattr(schema, "__optional_keys__", _DEFAULT_KEYS) + irq = _is_required_type(type_) if name in optional_keys: # Either total=False or explicit NotRequired. # No type annotation trumps this. + if irq: + # Unless it's earlier versions of python & explicit Required + return ... + return None + if irq is not None: + if irq: + # Handle Required[] + # (we already handled NotRequired and total=False) + return ... + # Handle NotRequired[] for earlier versions of python return None - if _is_required_type(type_): - # Handle Required[] - # (we already handled NotRequired and total=False) - return ... # Note, we ignore ReadOnly attributes, # as they don't make much sense. (we don't care if you mutate the state in your node) # and mutating state in your node has no effect on our graph state. From 3a07fe2efb6567acff6d792230f5369d7aa4abe6 Mon Sep 17 00:00:00 2001 From: gbaian10 <34255899+gbaian10@users.noreply.github.com> Date: Sun, 1 Sep 2024 07:35:59 +0800 Subject: [PATCH 221/287] docs: fix agent_supervisor.ipynb (#1562) fix #1561 --- examples/multi_agent/agent_supervisor.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/multi_agent/agent_supervisor.ipynb b/examples/multi_agent/agent_supervisor.ipynb index bd2a7c6ce..b1ecfe31b 100644 --- a/examples/multi_agent/agent_supervisor.ipynb +++ b/examples/multi_agent/agent_supervisor.ipynb @@ -219,7 +219,7 @@ "workflow = StateGraph(AgentState)\n", "workflow.add_node(\"Researcher\", research_node)\n", "workflow.add_node(\"Coder\", code_node)\n", - "workflow.add_node(\"supervisor\", supervisor_chain)" + "workflow.add_node(\"supervisor\", supervisor_agent)" ] }, { From 8c698fc36252bb6a84b09d98c4d0bab2fbf98328 Mon Sep 17 00:00:00 2001 From: Reuben M <120103767+McCReuben@users.noreply.github.com> Date: Sat, 31 Aug 2024 19:41:48 -0400 Subject: [PATCH 222/287] Return replan response in dict. Add Replan example (#1566) --- examples/llm-compiler/LLMCompiler.ipynb | 43 ++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/examples/llm-compiler/LLMCompiler.ipynb b/examples/llm-compiler/LLMCompiler.ipynb index 98f5416d8..3ae67e33b 100644 --- a/examples/llm-compiler/LLMCompiler.ipynb +++ b/examples/llm-compiler/LLMCompiler.ipynb @@ -646,11 +646,12 @@ "def _parse_joiner_output(decision: JoinOutputs) -> List[BaseMessage]:\n", " response = [AIMessage(content=f\"Thought: {decision.thought}\")]\n", " if isinstance(decision.action, Replan):\n", - " return response + [\n", + " return {\"messages\": response + [\n", " SystemMessage(\n", " content=f\"Context from last attempt: {decision.action.feedback}\"\n", " )\n", " ]\n", + " }\n", " else:\n", " return {\"messages\": response + [AIMessage(content=decision.action.response)]}\n", "\n", @@ -933,6 +934,46 @@ "print(step['join']['messages'][-1].content)" ] }, + { + "cell_type": "markdown", + "id": "f9487866", + "metadata": {}, + "source": [ + "#### Complex Replanning Example\n", + "\n", + "This question is likely to prompt the Replan functionality, but it may need to be run multiple times to see this in action." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "391d6931", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'plan_and_schedule': {'messages': [FunctionMessage(content=\"[{'url': 'https://www.timeanddate.com/weather/japan/tokyo', 'content': '88 / 84 °F. 13. 87 / 82 °F. 14. 84 / 80 °F. Detailed forecast for 14 days. Need some help? Current weather in Tokyo and forecast for today, tomorrow, and next 14 days.'}]\", additional_kwargs={'idx': 1, 'args': {'query': 'current temperature in Tokyo'}}, name='tavily_search_results_json', tool_call_id=1), FunctionMessage(content='join', additional_kwargs={'idx': 2, 'args': ()}, name='join', tool_call_id=2)]}}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: The search result provides the current temperature in Tokyo but does not explicitly state which temperature (88 / 84 °F) corresponds to the current condition. It seems to be a range, possibly the day's high and low. Without a clear indication of the exact current temperature, it's challenging to provide a precise flashcard summary.\", id='8ef2a131-69db-4180-a76e-fd9d6f4037c1'), SystemMessage(content='Context from last attempt: The information provided does not explicitly state the current temperature in Tokyo; it provides a temperature range without specifying which is the current temperature. Need to find a source that gives the exact current temperature in Tokyo for a precise flashcard summary.', id='f5bd752c-b068-459a-8d9e-bd1f1b5fa4fe')]}}\n", + "{'plan_and_schedule': {'messages': [FunctionMessage(content='join', additional_kwargs={'idx': 3, 'args': ()}, name='join', tool_call_id=3)]}}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: The search result provides a temperature range for Tokyo but does not specify the current temperature. This makes it challenging to create a precise flashcard without an exact current temperature. The user's request cannot be fully satisfied without this detail.\", id='3cc41891-4f47-4453-8edf-b989926ab25e'), SystemMessage(content='Context from last attempt: The search did not provide an exact current temperature for Tokyo, making it impossible to create a precise flashcard. A source that explicitly states the current temperature is needed for an accurate response.', id='96290b41-a4c4-4ab5-829a-89cc31dfe6c8')]}}\n", + "{'plan_and_schedule': {'messages': [FunctionMessage(content='join', additional_kwargs={'idx': 4, 'args': ()}, name='join', tool_call_id=4)]}}\n", + "{'join': {'messages': [AIMessage(content=\"Thought: The search result provides a temperature range for Tokyo but does not specify the current temperature. This makes it challenging to create a precise flashcard without an exact current temperature. The user's request cannot be fully satisfied without this detail.\", id='4724b242-ddb8-47e6-b235-de25de54fe45'), AIMessage(content='I was unable to find the exact current temperature in Tokyo. However, the temperature range for today in Tokyo is between 88°F and 84°F. For the most accurate and up-to-date temperature, I recommend checking a reliable weather forecasting website or app.', id='40e29a47-a001-4f65-a18f-65c2931d1ae5')]}}\n" + ] + } + ], + "source": [ + "for step in chain.stream({\"messages\":\n", + " [\n", + " HumanMessage(\n", + " content=\"Find the current temperature in Tokyo, then, respond with a flashcard summarizing this information\"\n", + " )\n", + " ]}\n", + "):\n", + " print(step)" + ] + }, { "cell_type": "markdown", "id": "c647d5f3-5e00-4449-9cec-5a9f438c9cff", From a93775413281df9ddf6ba29cc388b2460d94b9af Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Sun, 1 Sep 2024 12:15:20 -0700 Subject: [PATCH 223/287] lib 0.2.16 --- 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 36cdb7fdc..b7064ee22 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.15" +version = "0.2.16" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From d65d0ec665896849bda3f2c370299fc267226fbb Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 Sep 2024 17:24:54 -0700 Subject: [PATCH 224/287] test: Ensure that async checkpointers in tests are setup in same event loop as they are used in - This more closely remembers the environment they're used in, so it's what we should be testing - Remove unnecessary pytest-asyncio dependency, use anyio pytest plugin instead - Convert remaining async tests using only memory checkpointer to use all existing ones --- libs/langgraph/Makefile | 2 +- libs/langgraph/poetry.lock | 35 +- libs/langgraph/pyproject.toml | 4 +- .../__snapshots__/test_pregel_async.ambr | 1100 ++ libs/langgraph/tests/checkpoint/__init__.py | 0 libs/langgraph/tests/conftest.py | 105 +- libs/langgraph/tests/test_channels.py | 2 + libs/langgraph/tests/test_interruption.py | 41 +- libs/langgraph/tests/test_prebuilt.py | 74 +- libs/langgraph/tests/test_pregel.py | 28 +- libs/langgraph/tests/test_pregel_async.py | 8989 +++++++++-------- libs/langgraph/tests/test_state.py | 4 +- libs/langgraph/tests/test_store.py | 3 + libs/langgraph/tests/test_utils.py | 2 + 14 files changed, 5975 insertions(+), 4414 deletions(-) delete mode 100644 libs/langgraph/tests/checkpoint/__init__.py diff --git a/libs/langgraph/Makefile b/libs/langgraph/Makefile index ccb8ef59a..d5c6d7ee3 100644 --- a/libs/langgraph/Makefile +++ b/libs/langgraph/Makefile @@ -15,7 +15,7 @@ coverage: --cov-report term-missing:skip-covered start-postgres: - docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait + docker compose -f tests/compose-postgres.yml up -V --force-recreate --wait --remove-orphans stop-postgres: docker compose -f tests/compose-postgres.yml down -v diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index 241d47b6e..50d6c6f89 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -2829,13 +2829,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.4.4" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] @@ -2843,29 +2843,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.20.3" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, - {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, -] - -[package.dependencies] -pytest = ">=6.1.0" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -3069,7 +3051,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -4310,4 +4291,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9.0,<4.0" -content-hash = "7e0d6fd967987fcf46b038c47b5ca81d86cb8fff89704eac2074f2ce55336a2b" +content-hash = "e0787721e6cb80996a39284c74f1ddee4789c6060d6c2272cb1957f4686bd478" diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index b7064ee22..d6e04486f 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -14,10 +14,9 @@ langgraph-checkpoint = "^1.0.2" [tool.poetry.group.dev.dependencies] -pytest = "^7.3.0" +pytest = "^8.3.2" pytest-cov = "^4.0.0" pytest-dotenv = "^0.5.2" -pytest-asyncio = "^0.20.3" pytest-mock = "^3.10.0" syrupy = "^4.0.2" httpx = "^0.26.0" @@ -73,7 +72,6 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] -asyncio_mode = "auto" # --strict-markers will raise errors on unknown marks. # https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks # diff --git a/libs/langgraph/tests/__snapshots__/test_pregel_async.ambr b/libs/langgraph/tests/__snapshots__/test_pregel_async.ambr index c0d2e782c..3aaccffd1 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel_async.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel_async.ambr @@ -36,6 +36,191 @@ +---------+ ''' # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[memory] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[postgres_aio] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[postgres_aio_pipe] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[postgres_aio_pool] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[sqlite_aio] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2 ''' graph TD; @@ -157,6 +342,611 @@ 'type': 'object', }) # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[memory].2 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio].2 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pipe] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pipe].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pipe].2 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pool] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pool].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[postgres_aio_pool].2 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite_aio] + ''' + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite_aio].1 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2[sqlite_aio].2 + dict({ + '$defs': dict({ + 'InnerObject': dict({ + 'properties': dict({ + 'yo': dict({ + 'title': 'Yo', + 'type': 'integer', + }), + }), + 'required': list([ + 'yo', + ]), + 'title': 'InnerObject', + 'type': 'object', + }), + }), + 'properties': dict({ + 'answer': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'type': 'null', + }), + ]), + 'default': None, + 'title': 'Answer', + }), + 'docs': dict({ + 'items': dict({ + 'type': 'string', + }), + 'title': 'Docs', + 'type': 'array', + }), + 'inner': dict({ + '$ref': '#/$defs/InnerObject', + }), + 'query': dict({ + 'title': 'Query', + 'type': 'string', + }), + }), + 'required': list([ + 'query', + 'inner', + 'docs', + ]), + 'title': 'State', + 'type': 'object', + }) +# --- # name: test_in_one_fan_out_state_graph_waiting_edge_via_branch ''' +-----------+ @@ -194,6 +984,191 @@ +---------+ ''' # --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[memory] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres_aio] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres_aio_pipe] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[postgres_aio_pool] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- +# name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[sqlite_aio] + ''' + +-----------+ + | __start__ | + +-----------+ + * + * + * + +---------------+ + | rewrite_query | + +---------------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . + * . + * . + * . + +---------------+ +---------------+ + | retriever_one | | retriever_two | + +---------------+ +---------------+ + *** *** + * * + ** ** + +----+ + | qa | + +----+ + * + * + * + +---------+ + | __end__ | + +---------+ + ''' +# --- # name: test_nested_graph ''' +-----------+ @@ -219,3 +1194,128 @@ +---------+ ''' # --- +# name: test_weather_subgraph[memory] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres_aio] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres_aio_pipe] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[postgres_aio_pool] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- +# name: test_weather_subgraph[sqlite_aio] + ''' + %%{init: {'flowchart': {'curve': 'linear'}}}%% + graph TD; + __start__([__start__]):::first + router_node(router_node) + normal_llm_node(normal_llm_node) + weather_graph_model_node(model_node) + weather_graph_weather_node(weather_node
__interrupt = before) + __end__([__end__]):::last + subgraph weather_graph + weather_graph_model_node --> weather_graph_weather_node; + end + __start__ --> router_node; + normal_llm_node --> __end__; + weather_graph_weather_node --> __end__; + router_node -.-> normal_llm_node; + router_node -.-> weather_graph_model_node; + router_node -.-> __end__; + classDef default fill:#f2f0ff,line-height:1.2 + classDef first fill-opacity:0 + classDef last fill:#bfb6fc + + ''' +# --- diff --git a/libs/langgraph/tests/checkpoint/__init__.py b/libs/langgraph/tests/checkpoint/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/libs/langgraph/tests/conftest.py b/libs/langgraph/tests/conftest.py index bb6ef9603..ca6351c6b 100644 --- a/libs/langgraph/tests/conftest.py +++ b/libs/langgraph/tests/conftest.py @@ -1,8 +1,6 @@ -import asyncio import sys -from concurrent.futures import ThreadPoolExecutor -from contextlib import asynccontextmanager, contextmanager -from typing import AsyncIterator, Iterator, TypeVar +from contextlib import asynccontextmanager +from typing import AsyncIterator, Optional from uuid import UUID, uuid4 import pytest @@ -10,6 +8,7 @@ from psycopg import AsyncConnection, Connection from psycopg_pool import AsyncConnectionPool, ConnectionPool from pytest_mock import MockerFixture +from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.checkpoint.postgres import PostgresSaver from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver from langgraph.checkpoint.sqlite import SqliteSaver @@ -19,6 +18,11 @@ from tests.memory_assert import MemorySaverAssertImmutable DEFAULT_POSTGRES_URI = "postgres://postgres:postgres@localhost:5442/" +@pytest.fixture +def anyio_backend(): + return "asyncio" + + @pytest.fixture() def deterministic_uuids(mocker: MockerFixture) -> MockerFixture: side_effect = ( @@ -27,35 +31,6 @@ def deterministic_uuids(mocker: MockerFixture) -> MockerFixture: return mocker.patch("uuid.uuid4", side_effect=side_effect) -""" -pytest-asyncio doesn't support calling async fixtures with getfixturevalue -so we need to use ThreadPoolExecutor to run the async fixture in a thread -https://github.com/pytest-dev/pytest-asyncio/issues/112#issuecomment-462062890 -""" -T = TypeVar("T") - - -def close_loop(loop: asyncio.AbstractEventLoop) -> None: - loop.run_until_complete(loop.shutdown_asyncgens()) - loop.run_until_complete(loop.shutdown_default_executor()) - asyncio.set_event_loop(None) - loop.close() - - -@contextmanager -def agen_to_gen(agen: AsyncIterator[T]) -> Iterator[T]: - with ThreadPoolExecutor(1) as bg: - loop = asyncio.new_event_loop() - bg.submit(asyncio.set_event_loop, loop).result() - try: - yield bg.submit(loop.run_until_complete, agen.__aenter__()).result() - finally: - bg.submit( - loop.run_until_complete, agen.__aexit__(None, None, None) - ).result() - bg.submit(close_loop, loop).result() - - # checkpointer fixtures @@ -70,12 +45,6 @@ def checkpointer_sqlite(): yield checkpointer -@pytest.fixture(scope="function") -def checkpointer_sqlite_aio(): - with agen_to_gen(_checkpointer_sqlite_aio()) as checkpointer: - yield checkpointer - - @asynccontextmanager async def _checkpointer_sqlite_aio(): async with AsyncSqliteSaver.from_conn_string(":memory:") as checkpointer: @@ -143,16 +112,10 @@ def checkpointer_postgres_pool(): conn.execute(f"DROP DATABASE {database}") -@pytest.fixture(scope="function") -def checkpointer_postgres_aio(): - if sys.version_info < (3, 10): - pytest.skip("Async Postgres tests require Python 3.10+") - with agen_to_gen(_checkpointer_postgres_aio()) as checkpointer: - yield checkpointer - - @asynccontextmanager async def _checkpointer_postgres_aio(): + if sys.version_info < (3, 10): + pytest.skip("Async Postgres tests require Python 3.10+") database = f"test_{uuid4().hex[:16]}" # create unique db async with await AsyncConnection.connect( @@ -174,16 +137,10 @@ async def _checkpointer_postgres_aio(): await conn.execute(f"DROP DATABASE {database}") -@pytest.fixture(scope="function") -def checkpointer_postgres_aio_pipe(): - if sys.version_info < (3, 10): - pytest.skip("Async Postgres tests require Python 3.10+") - with agen_to_gen(_checkpointer_postgres_aio_pipe()) as checkpointer: - yield checkpointer - - @asynccontextmanager async def _checkpointer_postgres_aio_pipe(): + if sys.version_info < (3, 10): + pytest.skip("Async Postgres tests require Python 3.10+") database = f"test_{uuid4().hex[:16]}" # create unique db async with await AsyncConnection.connect( @@ -208,16 +165,10 @@ async def _checkpointer_postgres_aio_pipe(): await conn.execute(f"DROP DATABASE {database}") -@pytest.fixture(scope="function") -def checkpointer_postgres_aio_pool(): - if sys.version_info < (3, 10): - pytest.skip("Async Postgres tests require Python 3.10+") - with agen_to_gen(_checkpointer_postgres_aio_pool()) as checkpointer: - yield checkpointer - - @asynccontextmanager async def _checkpointer_postgres_aio_pool(): + if sys.version_info < (3, 10): + pytest.skip("Async Postgres tests require Python 3.10+") database = f"test_{uuid4().hex[:16]}" # create unique db async with await AsyncConnection.connect( @@ -240,6 +191,30 @@ async def _checkpointer_postgres_aio_pool(): await conn.execute(f"DROP DATABASE {database}") +@asynccontextmanager +async def awith_checkpointer( + checkpointer_name: Optional[str], +) -> AsyncIterator[BaseCheckpointSaver]: + if checkpointer_name is None: + yield None + elif checkpointer_name == "memory": + yield MemorySaverAssertImmutable() + elif checkpointer_name == "sqlite_aio": + async with _checkpointer_sqlite_aio() as checkpointer: + yield checkpointer + elif checkpointer_name == "postgres_aio": + async with _checkpointer_postgres_aio() as checkpointer: + yield checkpointer + elif checkpointer_name == "postgres_aio_pipe": + async with _checkpointer_postgres_aio_pipe() as checkpointer: + yield checkpointer + elif checkpointer_name == "postgres_aio_pool": + async with _checkpointer_postgres_aio_pool() as checkpointer: + yield checkpointer + else: + raise NotImplementedError(f"Unknown checkpointer: {checkpointer_name}") + + ALL_CHECKPOINTERS_SYNC = [ "memory", "sqlite", @@ -254,3 +229,7 @@ ALL_CHECKPOINTERS_ASYNC = [ "postgres_aio_pipe", "postgres_aio_pool", ] +ALL_CHECKPOINTERS_ASYNC_PLUS_NONE = [ + *ALL_CHECKPOINTERS_ASYNC, + None, +] diff --git a/libs/langgraph/tests/test_channels.py b/libs/langgraph/tests/test_channels.py index ff393577e..69a624969 100644 --- a/libs/langgraph/tests/test_channels.py +++ b/libs/langgraph/tests/test_channels.py @@ -8,6 +8,8 @@ from langgraph.channels.last_value import LastValue from langgraph.channels.topic import Topic from langgraph.errors import EmptyChannelError, InvalidUpdateError +pytestmark = pytest.mark.anyio + def test_last_value() -> None: with LastValue(int).from_checkpoint(None, {}) as channel: diff --git a/libs/langgraph/tests/test_interruption.py b/libs/langgraph/tests/test_interruption.py index e6682e964..d4e618ddf 100644 --- a/libs/langgraph/tests/test_interruption.py +++ b/libs/langgraph/tests/test_interruption.py @@ -4,12 +4,16 @@ import pytest from pytest_mock import MockerFixture from langgraph.graph import END, START, StateGraph - - -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], +from tests.conftest import ( + ALL_CHECKPOINTERS_ASYNC, + ALL_CHECKPOINTERS_SYNC, + awith_checkpointer, ) + +pytestmark = pytest.mark.anyio + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_interruption_without_state_updates( request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture ) -> None: @@ -47,12 +51,9 @@ def test_interruption_without_state_updates( assert graph.get_state(thread).next == () -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_interruption_without_state_updates_async( - request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture + checkpointer_name: str, mocker: MockerFixture ): """Test interruption without state updates. This test confirms that interrupting doesn't require a state key having been updated in the prev step""" @@ -72,17 +73,17 @@ async def test_interruption_without_state_updates_async( builder.add_edge("step_2", "step_3") builder.add_edge("step_3", END) - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") - graph = builder.compile(checkpointer=checkpointer, interrupt_after="*") + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer, interrupt_after="*") - initial_input = {"input": "hello world"} - thread = {"configurable": {"thread_id": "1"}} + initial_input = {"input": "hello world"} + thread = {"configurable": {"thread_id": "1"}} - await graph.ainvoke(initial_input, thread, debug=True) - assert (await graph.aget_state(thread)).next == ("step_2",) + await graph.ainvoke(initial_input, thread, debug=True) + assert (await graph.aget_state(thread)).next == ("step_2",) - await graph.ainvoke(None, thread, debug=True) - assert (await graph.aget_state(thread)).next == ("step_3",) + await graph.ainvoke(None, thread, debug=True) + assert (await graph.aget_state(thread)).next == ("step_3",) - await graph.ainvoke(None, thread, debug=True) - assert (await graph.aget_state(thread)).next == () + await graph.ainvoke(None, thread, debug=True) + assert (await graph.aget_state(thread)).next == () diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index b29074dff..82fd5e613 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -23,8 +23,15 @@ from pydantic import BaseModel as BaseModelV2 from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.prebuilt import ToolNode, ValidationNode, create_react_agent from langgraph.prebuilt.tool_node import InjectedState +from tests.conftest import ( + ALL_CHECKPOINTERS_ASYNC, + ALL_CHECKPOINTERS_SYNC, + awith_checkpointer, +) from tests.messages import _AnyIdHumanMessage +pytestmark = pytest.mark.anyio + class FakeToolCallingModel(BaseChatModel): def _generate( @@ -53,10 +60,7 @@ class FakeToolCallingModel(BaseChatModel): return self -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite", "postgres", "postgres_pipe"], -) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) -> None: checkpointer: BaseCheckpointSaver = request.getfixturevalue( "checkpointer_" + checkpointer_name @@ -89,43 +93,35 @@ def test_no_modifier(request: pytest.FixtureRequest, checkpointer_name: str) -> assert saved.pending_writes == [] -@pytest.mark.parametrize( - "checkpointer_name", - ["memory", "sqlite_aio", "postgres_aio", "postgres_aio_pipe"], -) -async def test_no_modifier_async( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer: BaseCheckpointSaver = request.getfixturevalue( - f"checkpointer_{checkpointer_name}" - ) +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_no_modifier_async(checkpointer_name: str) -> None: + async with awith_checkpointer(checkpointer_name) as checkpointer: + model = FakeToolCallingModel() - model = FakeToolCallingModel() + agent = create_react_agent(model, [], checkpointer=checkpointer) + inputs = [HumanMessage("hi?")] + thread = {"configurable": {"thread_id": "123"}} + response = await agent.ainvoke({"messages": inputs}, thread, debug=True) + expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]} + assert response == expected_response - agent = create_react_agent(model, [], checkpointer=checkpointer) - inputs = [HumanMessage("hi?")] - thread = {"configurable": {"thread_id": "123"}} - response = await agent.ainvoke({"messages": inputs}, thread, debug=True) - expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]} - assert response == expected_response - - if checkpointer: - saved = await checkpointer.aget_tuple(thread) - assert saved is not None - assert saved.checkpoint["channel_values"] == { - "messages": [ - _AnyIdHumanMessage(content="hi?"), - AIMessage(content="hi?", id="0"), - ], - "agent": "agent", - } - assert saved.metadata == { - "parents": {}, - "source": "loop", - "writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}}, - "step": 1, - } - assert saved.pending_writes == [] + if checkpointer: + saved = await checkpointer.aget_tuple(thread) + assert saved is not None + assert saved.checkpoint["channel_values"] == { + "messages": [ + _AnyIdHumanMessage(content="hi?"), + AIMessage(content="hi?", id="0"), + ], + "agent": "agent", + } + assert saved.metadata == { + "parents": {}, + "source": "loop", + "writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}}, + "step": 1, + } + assert saved.pending_writes == [] def test_passing_two_modifiers(): diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 2f85540f1..3ca3906d1 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -74,10 +74,7 @@ from langgraph.store.memory import MemoryStore from tests.any_str import AnyDict, AnyStr, AnyVersion, UnsortedSequence from tests.conftest import ALL_CHECKPOINTERS_SYNC from tests.fake_tracer import FakeTracer -from tests.memory_assert import ( - MemorySaverAssertCheckpointMetadata, - MemorySaverNoPending, -) +from tests.memory_assert import MemorySaverAssertCheckpointMetadata from tests.messages import _AnyIdAIMessage, _AnyIdHumanMessage @@ -1626,29 +1623,6 @@ def test_cond_edge_after_send() -> None: assert graph.invoke(["0"]) == ["0", "1", "2", "2", "3"] -async def test_checkpointer_null_pending_writes() -> None: - class Node: - def __init__(self, name: str): - self.name = name - setattr(self, "__name__", name) - - def __call__(self, state): - return [self.name] - - builder = StateGraph(Annotated[list, operator.add]) - builder.add_node(Node("1")) - builder.add_edge(START, "1") - graph = builder.compile(checkpointer=MemorySaverNoPending()) - assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"] - assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"] * 2 - assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [ - "1" - ] * 3 - assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [ - "1" - ] * 4 - - @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_invoke_checkpoint_three( mocker: MockerFixture, request: pytest.FixtureRequest, checkpointer_name: str diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 2f03a546e..4d004075d 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -17,6 +17,7 @@ from typing import ( Tuple, TypedDict, Union, + cast, ) from uuid import UUID @@ -43,7 +44,6 @@ from langgraph.channels.last_value import LastValue from langgraph.channels.topic import Topic from langgraph.channels.untracked_value import UntrackedValue from langgraph.checkpoint.base import ( - BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointMetadata, @@ -71,14 +71,20 @@ from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.types import PregelTask from langgraph.store.memory import MemoryStore from tests.any_str import AnyDict, AnyStr, AnyVersion, UnsortedSequence -from tests.conftest import ALL_CHECKPOINTERS_ASYNC +from tests.conftest import ( + ALL_CHECKPOINTERS_ASYNC, + ALL_CHECKPOINTERS_ASYNC_PLUS_NONE, + awith_checkpointer, +) from tests.fake_tracer import FakeTracer from tests.memory_assert import ( MemorySaverAssertCheckpointMetadata, - MemorySaverAssertImmutable, + MemorySaverNoPending, ) from tests.messages import _AnyIdAIMessage, _AnyIdHumanMessage +pytestmark = pytest.mark.anyio + async def test_checkpoint_errors() -> None: class FaultyGetCheckpointer(MemorySaver): @@ -217,11 +223,7 @@ async def test_node_cancellation_on_other_node_exception() -> None: @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_dynamic_interrupt( - checkpointer_name: str, snapshot: SnapshotAssertion, request: pytest.FixtureRequest -) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") - +async def test_dynamic_interrupt(checkpointer_name: str) -> None: class State(TypedDict): my_key: Annotated[str, operator.add] market: str @@ -259,59 +261,60 @@ async def test_dynamic_interrupt( "market": "US", } - tool_two = tool_two_graph.compile(checkpointer=checkpointer) + async with awith_checkpointer(checkpointer_name) as checkpointer: + tool_two = tool_two_graph.compile(checkpointer=checkpointer) - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - await tool_two.ainvoke({"my_key": "value", "market": "DE"}) + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value ⛰️", "market": "DE"}, thread1) == { - "my_key": "value ⛰️", - "market": "DE", - } - assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ - { - "parents": {}, - "source": "loop", - "step": 0, - "writes": None, - }, - { - "parents": {}, - "source": "input", - "step": -1, - "writes": {"__start__": {"my_key": "value ⛰️", "market": "DE"}}, - }, - ] - tup = await tool_two.checkpointer.aget_tuple(thread1) - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value ⛰️", "market": "DE"}, - next=("tool_two",), - tasks=( - PregelTask( - AnyStr(), - "tool_two", - interrupts=(Interrupt("Just because..."),), + thread1 = {"configurable": {"thread_id": "1"}} + # stop when about to enter node + assert await tool_two.ainvoke( + {"my_key": "value ⛰️", "market": "DE"}, thread1 + ) == { + "my_key": "value ⛰️", + "market": "DE", + } + assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ + { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + }, + { + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": {"my_key": "value ⛰️", "market": "DE"}}, + }, + ] + tup = await tool_two.checkpointer.aget_tuple(thread1) + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value ⛰️", "market": "DE"}, + next=("tool_two",), + tasks=( + PregelTask( + AnyStr(), + "tool_two", + interrupts=(Interrupt("Just because..."),), + ), ), - ), - config=tup.config, - created_at=tup.checkpoint["ts"], - metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, - parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ - -1 - ].config, - ) - # TODO use aget_state_history + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread1, limit=2) + ][-1].config, + ) + # TODO use aget_state_history @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_node_not_cancelled_on_other_node_interrupted( - checkpointer_name: str, request: pytest.FixtureRequest + checkpointer_name: str, ) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") - class State(TypedDict): hello: str @@ -339,23 +342,24 @@ async def test_node_not_cancelled_on_other_node_interrupted( builder.add_node("bad", iambad) builder.set_conditional_entry_point(lambda _: ["agent", "bad"], then=END) - graph = builder.compile(checkpointer=checkpointer) - thread = {"configurable": {"thread_id": "1"}} + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) + thread = {"configurable": {"thread_id": "1"}} - assert await graph.ainvoke({"hello": "world"}, thread) == {"hello": "world"} + assert await graph.ainvoke({"hello": "world"}, thread) == {"hello": "world"} - assert not inner_task_cancelled - assert awhiles == 1 + assert not inner_task_cancelled + assert awhiles == 1 - assert await graph.ainvoke(None, thread, debug=True) == {"hello": "world"} + assert await graph.ainvoke(None, thread, debug=True) == {"hello": "world"} - assert not inner_task_cancelled - assert awhiles == 1 + assert not inner_task_cancelled + assert awhiles == 1 - assert await graph.ainvoke({"hello": "bye"}, thread) == {"hello": "again"} + assert await graph.ainvoke({"hello": "bye"}, thread) == {"hello": "again"} - assert not inner_task_cancelled - assert awhiles == 2 + assert not inner_task_cancelled + assert awhiles == 2 async def test_step_timeout_on_stream_hang() -> None: @@ -388,12 +392,8 @@ async def test_step_timeout_on_stream_hang() -> None: assert inner_task_cancelled -@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_cancel_graph_astream( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") - +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC_PLUS_NONE) +async def test_cancel_graph_astream(checkpointer_name: str) -> None: class State(TypedDict): value: Annotated[int, operator.add] @@ -426,53 +426,47 @@ async def test_cancel_graph_astream( builder.add_edge(START, "alittlewhile") builder.add_edge(START, "aparallelwhile") builder.add_edge("alittlewhile", "awhile") - graph = builder.compile(checkpointer=checkpointer) - # test interrupting astream - got_event = False - thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} - async with aclosing(graph.astream({"value": 1}, thread1)) as stream: - async for chunk in stream: - assert chunk == {"alittlewhile": {"value": 2}} - got_event = True - break + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) - assert got_event + # test interrupting astream + got_event = False + thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} + async with aclosing(graph.astream({"value": 1}, thread1)) as stream: + async for chunk in stream: + assert chunk == {"alittlewhile": {"value": 2}} + got_event = True + break - # node aparallelwhile should start, but be cancelled - assert aparallelwhile.started is True - assert aparallelwhile.cancelled is True + assert got_event - # node "awhile" should never start - assert awhile.started is False + # node aparallelwhile should start, but be cancelled + assert aparallelwhile.started is True + assert aparallelwhile.cancelled is True - # checkpoint with output of "alittlewhile" should not be saved - if checkpointer is not None: - state = await graph.aget_state(thread1) - assert state is not None - assert state.values == {"value": 1} - assert state.next == ( - "aparallelwhile", - "alittlewhile", - ) - assert state.metadata == { - "parents": {}, - "source": "loop", - "step": 0, - "writes": None, - } + # node "awhile" should never start + assert awhile.started is False + + # checkpoint with output of "alittlewhile" should not be saved + if checkpointer is not None: + state = await graph.aget_state(thread1) + assert state is not None + assert state.values == {"value": 1} + assert state.next == ( + "aparallelwhile", + "alittlewhile", + ) + assert state.metadata == { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + } -@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_cancel_graph_astream_events_v2( - request: pytest.FixtureRequest, checkpointer_name: Optional[str] -) -> None: - checkpointer = ( - request.getfixturevalue(f"checkpointer_{checkpointer_name}") - if checkpointer_name - else None - ) - +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC_PLUS_NONE) +async def test_cancel_graph_astream_events_v2(checkpointer_name: Optional[str]) -> None: class State(TypedDict): value: int @@ -505,43 +499,45 @@ async def test_cancel_graph_astream_events_v2( builder.add_edge(START, "alittlewhile") builder.add_edge("alittlewhile", "awhile") builder.add_edge("awhile", "anotherwhile") - graph = builder.compile(checkpointer=checkpointer) - # test interrupting astream_events v2 - got_event = False - thread2: RunnableConfig = {"configurable": {"thread_id": "2"}} - async with aclosing( - graph.astream_events({"value": 1}, thread2, version="v2") - ) as stream: - async for chunk in stream: - if chunk["event"] == "on_chain_stream" and not chunk["parent_ids"]: - got_event = True - assert chunk["data"]["chunk"] == {"alittlewhile": {"value": 2}} - break + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) - # did break - assert got_event + # test interrupting astream_events v2 + got_event = False + thread2: RunnableConfig = {"configurable": {"thread_id": "2"}} + async with aclosing( + graph.astream_events({"value": 1}, thread2, version="v2") + ) as stream: + async for chunk in stream: + if chunk["event"] == "on_chain_stream" and not chunk["parent_ids"]: + got_event = True + assert chunk["data"]["chunk"] == {"alittlewhile": {"value": 2}} + break - # node "awhile" maybe starts (impl detail of astream_events) - # if it does start, it must be cancelled - if awhile.started: - assert awhile.cancelled is True + # did break + assert got_event - # node "anotherwhile" should never start - assert anotherwhile.started is False + # node "awhile" maybe starts (impl detail of astream_events) + # if it does start, it must be cancelled + if awhile.started: + assert awhile.cancelled is True - # checkpoint with output of "alittlewhile" should not be saved - if checkpointer is not None: - state = await graph.aget_state(thread2) - assert state is not None - assert state.values == {"value": 2} - assert state.next == ("awhile",) - assert state.metadata == { - "parents": {}, - "source": "loop", - "step": 1, - "writes": {"alittlewhile": {"value": 2}}, - } + # node "anotherwhile" should never start + assert anotherwhile.started is False + + # checkpoint with output of "alittlewhile" should not be saved + if checkpointer is not None: + state = await graph.aget_state(thread2) + assert state is not None + assert state.values == {"value": 2} + assert state.next == ("awhile",) + assert state.metadata == { + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"alittlewhile": {"value": 2}}, + } async def test_node_schemas_custom_output() -> None: @@ -819,424 +815,438 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_invoke_two_processes_in_out_interrupt( - request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture + checkpointer_name: str, mocker: MockerFixture ) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") add_one = mocker.Mock(side_effect=lambda x: x + 1) one = Channel.subscribe_to("input") | add_one | Channel.write_to("inbox") two = Channel.subscribe_to("inbox") | add_one | Channel.write_to("output") - app = Pregel( - nodes={"one": one, "two": two}, - channels={ - "inbox": LastValue(int), - "output": LastValue(int), - "input": LastValue(int), - }, - input_channels="input", - output_channels="output", - checkpointer=checkpointer, - interrupt_after_nodes=["one"], - ) - thread1 = {"configurable": {"thread_id": "1"}} - thread2 = {"configurable": {"thread_id": "2"}} + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = Pregel( + nodes={"one": one, "two": two}, + channels={ + "inbox": LastValue(int), + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", + checkpointer=checkpointer, + interrupt_after_nodes=["one"], + ) + thread1 = {"configurable": {"thread_id": "1"}} + thread2 = {"configurable": {"thread_id": "2"}} - # start execution, stop at inbox - assert await app.ainvoke(2, thread1) is None + # start execution, stop at inbox + assert await app.ainvoke(2, thread1) is None - # inbox == 3 - checkpoint = await checkpointer.aget(thread1) - assert checkpoint is not None - assert checkpoint["channel_values"]["inbox"] == 3 + # inbox == 3 + checkpoint = await checkpointer.aget(thread1) + assert checkpoint is not None + assert checkpoint["channel_values"]["inbox"] == 3 - # resume execution, finish - assert await app.ainvoke(None, thread1) == 4 + # resume execution, finish + assert await app.ainvoke(None, thread1) == 4 - # start execution again, stop at inbox - assert await app.ainvoke(20, thread1) is None + # start execution again, stop at inbox + assert await app.ainvoke(20, thread1) is None - # inbox == 21 - checkpoint = await checkpointer.aget(thread1) - assert checkpoint is not None - assert checkpoint["channel_values"]["inbox"] == 21 + # inbox == 21 + checkpoint = await checkpointer.aget(thread1) + assert checkpoint is not None + assert checkpoint["channel_values"]["inbox"] == 21 - # send a new value in, interrupting the previous execution - assert await app.ainvoke(3, thread1) is None - assert await app.ainvoke(None, thread1) == 5 + # send a new value in, interrupting the previous execution + assert await app.ainvoke(3, thread1) is None + assert await app.ainvoke(None, thread1) == 5 - # start execution again, stopping at inbox - assert await app.ainvoke(20, thread2) is None + # start execution again, stopping at inbox + assert await app.ainvoke(20, thread2) is None - # inbox == 21 - snapshot = await app.aget_state(thread2) - assert snapshot.values["inbox"] == 21 - assert snapshot.next == ("two",) + # inbox == 21 + snapshot = await app.aget_state(thread2) + assert snapshot.values["inbox"] == 21 + assert snapshot.next == ("two",) - # update the state, resume - await app.aupdate_state(thread2, 25, as_node="one") - assert await app.ainvoke(None, thread2) == 26 + # update the state, resume + await app.aupdate_state(thread2, 25, as_node="one") + assert await app.ainvoke(None, thread2) == 26 - # no pending tasks - snapshot = await app.aget_state(thread2) - assert snapshot.next == () + # no pending tasks + snapshot = await app.aget_state(thread2) + assert snapshot.next == () - # list history - history = [c async for c in app.aget_state_history(thread1)] - assert history == [ - StateSnapshot( - values={"inbox": 4, "output": 5, "input": 3}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "step": 6, "writes": {"two": 5}}, - created_at=AnyStr(), - parent_config=history[1].config, - ), - StateSnapshot( - values={"inbox": 4, "output": 4, "input": 3}, - tasks=(PregelTask(AnyStr(), "two"),), - next=("two",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "step": 5, - "writes": {"one": None}, - }, - created_at=AnyStr(), - parent_config=history[2].config, - ), - StateSnapshot( - values={"inbox": 21, "output": 4, "input": 3}, - tasks=(PregelTask(AnyStr(), "one"),), - next=("one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "step": 4, - "writes": {"input": 3}, - }, - created_at=AnyStr(), - parent_config=history[3].config, - ), - StateSnapshot( - values={"inbox": 21, "output": 4, "input": 20}, - tasks=(PregelTask(AnyStr(), "two"),), - next=("two",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "step": 3, - "writes": {"one": None}, - }, - created_at=AnyStr(), - parent_config=history[4].config, - ), - StateSnapshot( - values={"inbox": 3, "output": 4, "input": 20}, - tasks=(PregelTask(AnyStr(), "one"),), - next=("one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "step": 2, - "writes": {"input": 20}, - }, - created_at=AnyStr(), - parent_config=history[5].config, - ), - StateSnapshot( - values={"inbox": 3, "output": 4, "input": 2}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "step": 1, "writes": {"two": 4}}, - created_at=AnyStr(), - parent_config=history[6].config, - ), - StateSnapshot( - values={"inbox": 3, "input": 2}, - tasks=(PregelTask(AnyStr(), "two"),), - next=("two",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "step": 0, - "writes": {"one": None}, - }, - created_at=AnyStr(), - parent_config=history[7].config, - ), - StateSnapshot( - values={"input": 2}, - tasks=(PregelTask(AnyStr(), "one"),), - next=("one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "step": -1, - "writes": {"input": 2}, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] + # list history + history = [c async for c in app.aget_state_history(thread1)] + assert history == [ + StateSnapshot( + values={"inbox": 4, "output": 5, "input": 3}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 6, + "writes": {"two": 5}, + }, + created_at=AnyStr(), + parent_config=history[1].config, + ), + StateSnapshot( + values={"inbox": 4, "output": 4, "input": 3}, + tasks=(PregelTask(AnyStr(), "two"),), + next=("two",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 5, + "writes": {"one": None}, + }, + created_at=AnyStr(), + parent_config=history[2].config, + ), + StateSnapshot( + values={"inbox": 21, "output": 4, "input": 3}, + tasks=(PregelTask(AnyStr(), "one"),), + next=("one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "step": 4, + "writes": {"input": 3}, + }, + created_at=AnyStr(), + parent_config=history[3].config, + ), + StateSnapshot( + values={"inbox": 21, "output": 4, "input": 20}, + tasks=(PregelTask(AnyStr(), "two"),), + next=("two",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"one": None}, + }, + created_at=AnyStr(), + parent_config=history[4].config, + ), + StateSnapshot( + values={"inbox": 3, "output": 4, "input": 20}, + tasks=(PregelTask(AnyStr(), "one"),), + next=("one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "step": 2, + "writes": {"input": 20}, + }, + created_at=AnyStr(), + parent_config=history[5].config, + ), + StateSnapshot( + values={"inbox": 3, "output": 4, "input": 2}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"two": 4}, + }, + created_at=AnyStr(), + parent_config=history[6].config, + ), + StateSnapshot( + values={"inbox": 3, "input": 2}, + tasks=(PregelTask(AnyStr(), "two"),), + next=("two",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 0, + "writes": {"one": None}, + }, + created_at=AnyStr(), + parent_config=history[7].config, + ), + StateSnapshot( + values={"input": 2}, + tasks=(PregelTask(AnyStr(), "one"),), + next=("one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "step": -1, + "writes": {"input": 2}, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] - # forking from any previous checkpoint should re-run nodes - assert [ - c async for c in app.astream(None, history[0].config, stream_mode="updates") - ] == [] - assert [ - c async for c in app.astream(None, history[1].config, stream_mode="updates") - ] == [ - {"two": {"output": 5}}, - ] - assert [ - c async for c in app.astream(None, history[2].config, stream_mode="updates") - ] == [ - {"one": {"inbox": 4}}, - ] + # forking from any previous checkpoint should re-run nodes + assert [ + c async for c in app.astream(None, history[0].config, stream_mode="updates") + ] == [] + assert [ + c async for c in app.astream(None, history[1].config, stream_mode="updates") + ] == [ + {"two": {"output": 5}}, + ] + assert [ + c async for c in app.astream(None, history[2].config, stream_mode="updates") + ] == [ + {"one": {"inbox": 4}}, + ] @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_fork_always_re_runs_nodes( - request: pytest.FixtureRequest, checkpointer_name: str, mocker: MockerFixture + checkpointer_name: str, mocker: MockerFixture ) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") add_one = mocker.Mock(side_effect=lambda _: 1) builder = StateGraph(Annotated[int, operator.add]) builder.add_node("add_one", add_one) builder.add_edge(START, "add_one") builder.add_conditional_edges("add_one", lambda cnt: "add_one" if cnt < 6 else END) - graph = builder.compile(checkpointer=checkpointer) + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) - thread1 = {"configurable": {"thread_id": "1"}} + thread1 = {"configurable": {"thread_id": "1"}} - # start execution, stop at inbox - assert [ - c async for c in graph.astream(1, thread1, stream_mode=["values", "updates"]) - ] == [ - ("values", 1), - ("updates", {"add_one": 1}), - ("values", 2), - ("updates", {"add_one": 1}), - ("values", 3), - ("updates", {"add_one": 1}), - ("values", 4), - ("updates", {"add_one": 1}), - ("values", 5), - ("updates", {"add_one": 1}), - ("values", 6), - ] + # start execution, stop at inbox + assert [ + c + async for c in graph.astream(1, thread1, stream_mode=["values", "updates"]) + ] == [ + ("values", 1), + ("updates", {"add_one": 1}), + ("values", 2), + ("updates", {"add_one": 1}), + ("values", 3), + ("updates", {"add_one": 1}), + ("values", 4), + ("updates", {"add_one": 1}), + ("values", 5), + ("updates", {"add_one": 1}), + ("values", 6), + ] - # list history - history = [c async for c in graph.aget_state_history(thread1)] - assert history == [ - StateSnapshot( - values=6, - next=(), - tasks=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "step": 5, - "writes": {"add_one": 1}, - }, - created_at=AnyStr(), - parent_config=history[1].config, - ), - StateSnapshot( - values=5, - tasks=(PregelTask(AnyStr(), "add_one"),), - next=("add_one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "step": 4, - "writes": {"add_one": 1}, - }, - created_at=AnyStr(), - parent_config=history[2].config, - ), - StateSnapshot( - values=4, - tasks=(PregelTask(AnyStr(), "add_one"),), - next=("add_one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "step": 3, - "writes": {"add_one": 1}, - }, - created_at=AnyStr(), - parent_config=history[3].config, - ), - StateSnapshot( - values=3, - tasks=(PregelTask(AnyStr(), "add_one"),), - next=("add_one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "step": 2, - "writes": {"add_one": 1}, - }, - created_at=AnyStr(), - parent_config=history[4].config, - ), - StateSnapshot( - values=2, - tasks=(PregelTask(AnyStr(), "add_one"),), - next=("add_one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": {"add_one": 1}, - }, - created_at=AnyStr(), - parent_config=history[5].config, - ), - StateSnapshot( - values=1, - tasks=(PregelTask(AnyStr(), "add_one"),), - next=("add_one",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, - created_at=AnyStr(), - parent_config=history[6].config, - ), - StateSnapshot( - values=0, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "step": -1, - "writes": {"__start__": 1}, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] + # list history + history = [c async for c in graph.aget_state_history(thread1)] + assert history == [ + StateSnapshot( + values=6, + next=(), + tasks=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 5, + "writes": {"add_one": 1}, + }, + created_at=AnyStr(), + parent_config=history[1].config, + ), + StateSnapshot( + values=5, + tasks=(PregelTask(AnyStr(), "add_one"),), + next=("add_one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 4, + "writes": {"add_one": 1}, + }, + created_at=AnyStr(), + parent_config=history[2].config, + ), + StateSnapshot( + values=4, + tasks=(PregelTask(AnyStr(), "add_one"),), + next=("add_one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"add_one": 1}, + }, + created_at=AnyStr(), + parent_config=history[3].config, + ), + StateSnapshot( + values=3, + tasks=(PregelTask(AnyStr(), "add_one"),), + next=("add_one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 2, + "writes": {"add_one": 1}, + }, + created_at=AnyStr(), + parent_config=history[4].config, + ), + StateSnapshot( + values=2, + tasks=(PregelTask(AnyStr(), "add_one"),), + next=("add_one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"add_one": 1}, + }, + created_at=AnyStr(), + parent_config=history[5].config, + ), + StateSnapshot( + values=1, + tasks=(PregelTask(AnyStr(), "add_one"),), + next=("add_one",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, + created_at=AnyStr(), + parent_config=history[6].config, + ), + StateSnapshot( + values=0, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": 1}, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] - # forking from any previous checkpoint should re-run nodes - assert [ - c async for c in graph.astream(None, history[0].config, stream_mode="updates") - ] == [] - assert [ - c async for c in graph.astream(None, history[1].config, stream_mode="updates") - ] == [ - {"add_one": 1}, - ] - assert [ - c async for c in graph.astream(None, history[2].config, stream_mode="updates") - ] == [ - {"add_one": 1}, - {"add_one": 1}, - ] + # forking from any previous checkpoint should re-run nodes + assert [ + c + async for c in graph.astream(None, history[0].config, stream_mode="updates") + ] == [] + assert [ + c + async for c in graph.astream(None, history[1].config, stream_mode="updates") + ] == [ + {"add_one": 1}, + ] + assert [ + c + async for c in graph.astream(None, history[2].config, stream_mode="updates") + ] == [ + {"add_one": 1}, + {"add_one": 1}, + ] async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: @@ -1508,7 +1518,8 @@ async def test_invoke_two_processes_two_in_two_out_valid(mocker: MockerFixture) assert await app.ainvoke(2) == [3, 3] -async def test_invoke_checkpoint(mocker: MockerFixture) -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_invoke_checkpoint(mocker: MockerFixture, checkpointer_name: str) -> None: add_one = mocker.Mock(side_effect=lambda x: x["total"] + x["input"]) errored_once = False @@ -1531,57 +1542,52 @@ async def test_invoke_checkpoint(mocker: MockerFixture) -> None: | raise_if_above_10 ) - memory = MemorySaverAssertImmutable() + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = Pregel( + nodes={"one": one}, + channels={ + "total": BinaryOperatorAggregate(int, operator.add), + "input": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", + checkpointer=checkpointer, + retry_policy=RetryPolicy(), + ) - app = Pregel( - nodes={"one": one}, - channels={ - "total": BinaryOperatorAggregate(int, operator.add), - "input": LastValue(int), - "output": LastValue(int), - }, - input_channels="input", - output_channels="output", - checkpointer=memory, - retry_policy=RetryPolicy(), - ) - - # total starts out as 0, so output is 0+2=2 - assert await app.ainvoke(2, {"configurable": {"thread_id": "1"}}) == 2 - checkpoint = await memory.aget({"configurable": {"thread_id": "1"}}) - assert checkpoint is not None - assert checkpoint["channel_values"].get("total") == 2 - # total is now 2, so output is 2+3=5 - assert await app.ainvoke(3, {"configurable": {"thread_id": "1"}}) == 5 - assert errored_once, "errored and retried" - checkpoint = await memory.aget({"configurable": {"thread_id": "1"}}) - assert checkpoint is not None - assert checkpoint["channel_values"].get("total") == 7 - # total is now 2+5=7, so output would be 7+4=11, but raises ValueError - with pytest.raises(ValueError): - await app.ainvoke(4, {"configurable": {"thread_id": "1"}}) - # checkpoint is not updated - checkpoint = await memory.aget({"configurable": {"thread_id": "1"}}) - assert checkpoint is not None - assert checkpoint["channel_values"].get("total") == 7 - # on a new thread, total starts out as 0, so output is 0+5=5 - assert await app.ainvoke(5, {"configurable": {"thread_id": "2"}}) == 5 - checkpoint = await memory.aget({"configurable": {"thread_id": "1"}}) - assert checkpoint is not None - assert checkpoint["channel_values"].get("total") == 7 - checkpoint = await memory.aget({"configurable": {"thread_id": "2"}}) - assert checkpoint is not None - assert checkpoint["channel_values"].get("total") == 5 + # total starts out as 0, so output is 0+2=2 + assert await app.ainvoke(2, {"configurable": {"thread_id": "1"}}) == 2 + checkpoint = await checkpointer.aget({"configurable": {"thread_id": "1"}}) + assert checkpoint is not None + assert checkpoint["channel_values"].get("total") == 2 + # total is now 2, so output is 2+3=5 + assert await app.ainvoke(3, {"configurable": {"thread_id": "1"}}) == 5 + assert errored_once, "errored and retried" + checkpoint = await checkpointer.aget({"configurable": {"thread_id": "1"}}) + assert checkpoint is not None + assert checkpoint["channel_values"].get("total") == 7 + # total is now 2+5=7, so output would be 7+4=11, but raises ValueError + with pytest.raises(ValueError): + await app.ainvoke(4, {"configurable": {"thread_id": "1"}}) + # checkpoint is not updated + checkpoint = await checkpointer.aget({"configurable": {"thread_id": "1"}}) + assert checkpoint is not None + assert checkpoint["channel_values"].get("total") == 7 + # on a new thread, total starts out as 0, so output is 0+5=5 + assert await app.ainvoke(5, {"configurable": {"thread_id": "2"}}) == 5 + checkpoint = await checkpointer.aget({"configurable": {"thread_id": "1"}}) + assert checkpoint is not None + assert checkpoint["channel_values"].get("total") == 7 + checkpoint = await checkpointer.aget({"configurable": {"thread_id": "2"}}) + assert checkpoint is not None + assert checkpoint["channel_values"].get("total") == 5 @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_pending_writes_resume( request: pytest.FixtureRequest, checkpointer_name: str ) -> None: - checkpointer: BaseCheckpointSaver = request.getfixturevalue( - f"checkpointer_{checkpointer_name}" - ) - class State(TypedDict): value: Annotated[int, operator.add] @@ -1609,214 +1615,219 @@ async def test_pending_writes_resume( builder.add_node("two", two) builder.add_edge(START, "one") builder.add_edge(START, "two") - graph = builder.compile(checkpointer=checkpointer) + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) - thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} - with pytest.raises(ValueError, match="I'm not good"): - await graph.ainvoke({"value": 1}, thread1) + thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} + with pytest.raises(ValueError, match="I'm not good"): + await graph.ainvoke({"value": 1}, thread1) - # both nodes should have been called once - assert one.calls == 1 - assert two.calls == 1 + # both nodes should have been called once + assert one.calls == 1 + assert two.calls == 1 - # latest checkpoint should be before nodes "one", "two" - state = await graph.aget_state(thread1) - assert state is not None - assert state.values == {"value": 1} - assert state.next == ("one", "two") - assert state.tasks == ( - PregelTask(AnyStr(), "one"), - PregelTask(AnyStr(), "two", 'ValueError("I\'m not good")'), - ) - assert state.metadata == { - "parents": {}, - "source": "loop", - "step": 0, - "writes": None, - } - # should contain pending write of "one" - checkpoint = await checkpointer.aget_tuple(thread1) - assert checkpoint is not None - # should contain error from "two" - expected_writes = [ - (AnyStr(), "one", "one"), - (AnyStr(), "value", 2), - (AnyStr(), ERROR, 'ValueError("I\'m not good")'), - ] - assert len(checkpoint.pending_writes) == 3 - assert all(w in expected_writes for w in checkpoint.pending_writes) - # both non-error pending writes come from same task - non_error_writes = [w for w in checkpoint.pending_writes if w[1] != ERROR] - assert non_error_writes[0][0] == non_error_writes[1][0] - # error write is from the other task - error_write = next(w for w in checkpoint.pending_writes if w[1] == ERROR) - assert error_write[0] != non_error_writes[0][0] + # latest checkpoint should be before nodes "one", "two" + state = await graph.aget_state(thread1) + assert state is not None + assert state.values == {"value": 1} + assert state.next == ("one", "two") + assert state.tasks == ( + PregelTask(AnyStr(), "one"), + PregelTask(AnyStr(), "two", 'ValueError("I\'m not good")'), + ) + assert state.metadata == { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + } + # should contain pending write of "one" + checkpoint = await checkpointer.aget_tuple(thread1) + assert checkpoint is not None + # should contain error from "two" + expected_writes = [ + (AnyStr(), "one", "one"), + (AnyStr(), "value", 2), + (AnyStr(), ERROR, 'ValueError("I\'m not good")'), + ] + assert len(checkpoint.pending_writes) == 3 + assert all(w in expected_writes for w in checkpoint.pending_writes) + # both non-error pending writes come from same task + non_error_writes = [w for w in checkpoint.pending_writes if w[1] != ERROR] + assert non_error_writes[0][0] == non_error_writes[1][0] + # error write is from the other task + error_write = next(w for w in checkpoint.pending_writes if w[1] == ERROR) + assert error_write[0] != non_error_writes[0][0] - # TODO arguably this shouldn't even run the failed task again, - # and should require empty update_state (ie new checkpoint_id) - # in order to try again + # TODO arguably this shouldn't even run the failed task again, + # and should require empty update_state (ie new checkpoint_id) + # in order to try again - # resume execution - with pytest.raises(ValueError, match="I'm not good"): - await graph.ainvoke(None, thread1) + # resume execution + with pytest.raises(ValueError, match="I'm not good"): + await graph.ainvoke(None, thread1) - # node "one" succeeded previously, so shouldn't be called again - assert one.calls == 1 - # node "two" should have been called once again - assert two.calls == 2 + # node "one" succeeded previously, so shouldn't be called again + assert one.calls == 1 + # node "two" should have been called once again + assert two.calls == 2 - # confirm no new checkpoints saved - state_two = await graph.aget_state(thread1) - assert state_two.metadata == state.metadata + # confirm no new checkpoints saved + state_two = await graph.aget_state(thread1) + assert state_two.metadata == state.metadata - # 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} + # 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} - # check all final checkpoints - checkpoints = [c async for c in checkpointer.alist(thread1)] - # we should have 3 - assert len(checkpoints) == 3 - # the last one not too interesting for this test - assert checkpoints[0] == CheckpointTuple( - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - checkpoint={ - "v": 1, - "id": AnyStr(), - "ts": AnyStr(), - "pending_sends": [], - "versions_seen": { - "one": { - "start:one": AnyVersion(), + # check all final checkpoints + checkpoints = [c async for c in checkpointer.alist(thread1)] + # we should have 3 + assert len(checkpoints) == 3 + # the last one not too interesting for this test + assert checkpoints[0] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "pending_sends": [], + "versions_seen": { + "one": { + "start:one": AnyVersion(), + }, + "two": { + "start:two": AnyVersion(), + }, + "__input__": {}, + "__start__": { + "__start__": AnyVersion(), + }, + "__interrupt__": { + "value": AnyVersion(), + "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, }, - "two": { - "start:two": AnyVersion(), - }, - "__input__": {}, - "__start__": { - "__start__": AnyVersion(), - }, - "__interrupt__": { + "channel_versions": { + "one": AnyVersion(), + "two": AnyVersion(), "value": AnyVersion(), "__start__": AnyVersion(), "start:one": AnyVersion(), "start:two": AnyVersion(), }, + "channel_values": {"one": "one", "two": "two", "value": 6}, }, - "channel_versions": { - "one": AnyVersion(), - "two": AnyVersion(), - "value": AnyVersion(), - "__start__": AnyVersion(), - "start:one": AnyVersion(), - "start:two": AnyVersion(), + metadata={ + "parents": {}, + "step": 1, + "source": "loop", + "writes": {"one": {"value": 2}, "two": {"value": 3}}, }, - "channel_values": {"one": "one", "two": "two", "value": 6}, - }, - metadata={ - "parents": {}, - "step": 1, - "source": "loop", - "writes": {"one": {"value": 2}, "two": {"value": 3}}, - }, - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": checkpoints[1].config["configurable"]["checkpoint_id"], - } - }, - pending_writes=[], - ) - # the previous one we assert that pending writes contains both - # - original error - # - successful writes from resuming after preventing error - assert checkpoints[1] == CheckpointTuple( - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - checkpoint={ - "v": 1, - "id": AnyStr(), - "ts": AnyStr(), - "pending_sends": [], - "versions_seen": { - "__input__": {}, - "__start__": { + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": checkpoints[1].config["configurable"][ + "checkpoint_id" + ], + } + }, + pending_writes=[], + ) + # the previous one we assert that pending writes contains both + # - original error + # - successful writes from resuming after preventing error + assert checkpoints[1] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "pending_sends": [], + "versions_seen": { + "__input__": {}, + "__start__": { + "__start__": AnyVersion(), + }, + }, + "channel_versions": { + "value": AnyVersion(), "__start__": AnyVersion(), + "start:one": AnyVersion(), + "start:two": AnyVersion(), + }, + "channel_values": { + "value": 1, + "start:one": "__start__", + "start:two": "__start__", }, }, - "channel_versions": { - "value": AnyVersion(), - "__start__": AnyVersion(), - "start:one": AnyVersion(), - "start:two": AnyVersion(), + metadata={"parents": {}, "step": 0, "source": "loop", "writes": None}, + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": checkpoints[2].config["configurable"][ + "checkpoint_id" + ], + } }, - "channel_values": { - "value": 1, - "start:one": "__start__", - "start:two": "__start__", + pending_writes=UnsortedSequence( + (AnyStr(), "one", "one"), + (AnyStr(), "value", 2), + (AnyStr(), "__error__", 'ValueError("I\'m not good")'), + (AnyStr(), "two", "two"), + (AnyStr(), "value", 3), + ), + ) + assert checkpoints[2] == CheckpointTuple( + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } }, - }, - metadata={"parents": {}, "step": 0, "source": "loop", "writes": None}, - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": checkpoints[2].config["configurable"]["checkpoint_id"], - } - }, - pending_writes=UnsortedSequence( - (AnyStr(), "one", "one"), - (AnyStr(), "value", 2), - (AnyStr(), "__error__", 'ValueError("I\'m not good")'), - (AnyStr(), "two", "two"), - (AnyStr(), "value", 3), - ), - ) - assert checkpoints[2] == CheckpointTuple( - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - checkpoint={ - "v": 1, - "id": AnyStr(), - "ts": AnyStr(), - "pending_sends": [], - "versions_seen": {"__input__": {}}, - "channel_versions": { - "__start__": AnyVersion(), + checkpoint={ + "v": 1, + "id": AnyStr(), + "ts": AnyStr(), + "pending_sends": [], + "versions_seen": {"__input__": {}}, + "channel_versions": { + "__start__": AnyVersion(), + }, + "channel_values": {"__start__": {"value": 1}}, }, - "channel_values": {"__start__": {"value": 1}}, - }, - metadata={ - "parents": {}, - "step": -1, - "source": "input", - "writes": {"__start__": {"value": 1}}, - }, - parent_config=None, - pending_writes=UnsortedSequence( - (AnyStr(), "value", 1), - (AnyStr(), "start:one", "__start__"), - (AnyStr(), "start:two", "__start__"), - ), - ) + metadata={ + "parents": {}, + "step": -1, + "source": "input", + "writes": {"__start__": {"value": 1}}, + }, + parent_config=None, + pending_writes=UnsortedSequence( + (AnyStr(), "value", 1), + (AnyStr(), "start:one", "__start__"), + (AnyStr(), "start:two", "__start__"), + ), + ) async def test_cond_edge_after_send() -> None: @@ -1848,9 +1859,8 @@ async def test_cond_edge_after_send() -> None: @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_invoke_checkpoint_three( - mocker: MockerFixture, request: pytest.FixtureRequest, checkpointer_name: str + mocker: MockerFixture, checkpointer_name: str ) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) add_one = mocker.Mock(side_effect=lambda x: x["total"] + x["input"]) def raise_if_above_10(input: int) -> int: @@ -1865,118 +1875,121 @@ async def test_invoke_checkpoint_three( | raise_if_above_10 ) - app = Pregel( - nodes={"one": one}, - channels={ - "total": BinaryOperatorAggregate(int, operator.add), - "input": LastValue(int), - "output": LastValue(int), - }, - input_channels="input", - output_channels="output", - checkpointer=checkpointer, - debug=True, - ) - - thread_1 = {"configurable": {"thread_id": "1"}} - # total starts out as 0, so output is 0+2=2 - assert await app.ainvoke(2, thread_1) == 2 - state = await app.aget_state(thread_1) - assert state is not None - assert state.values.get("total") == 2 - assert ( - state.config["configurable"]["checkpoint_id"] - == (await checkpointer.aget(thread_1))["id"] - ) - # total is now 2, so output is 2+3=5 - assert await app.ainvoke(3, thread_1) == 5 - state = await app.aget_state(thread_1) - assert state is not None - assert state.values.get("total") == 7 - assert ( - state.config["configurable"]["checkpoint_id"] - == (await checkpointer.aget(thread_1))["id"] - ) - # total is now 2+5=7, so output would be 7+4=11, but raises ValueError - with pytest.raises(ValueError): - await app.ainvoke(4, thread_1) - # checkpoint is not updated - state = await app.aget_state(thread_1) - assert state is not None - assert state.values.get("total") == 7 - assert state.next == ("one",) - """we checkpoint inputs and it failed on "one", so the next node is one""" - # we can recover from error by sending new inputs - assert await app.ainvoke(2, thread_1) == 9 - state = await app.aget_state(thread_1) - assert state is not None - assert state.values.get("total") == 16, "total is now 7+9=16" - assert state.next == () - - thread_2 = {"configurable": {"thread_id": "2"}} - # on a new thread, total starts out as 0, so output is 0+5=5 - assert await app.ainvoke(5, thread_2) == 5 - state = await app.aget_state({"configurable": {"thread_id": "1"}}) - assert state is not None - assert state.values.get("total") == 16 - assert state.next == () - state = await app.aget_state(thread_2) - assert state is not None - assert state.values.get("total") == 5 - assert state.next == () - - assert len([c async for c in app.aget_state_history(thread_1, limit=1)]) == 1 - # list all checkpoints for thread 1 - thread_1_history = [c async for c in app.aget_state_history(thread_1)] - # there are 7 checkpoints - assert len(thread_1_history) == 7 - assert Counter(c.metadata["source"] for c in thread_1_history) == { - "input": 4, - "loop": 3, - } - # sorted descending - assert ( - thread_1_history[0].config["configurable"]["checkpoint_id"] - > thread_1_history[1].config["configurable"]["checkpoint_id"] - ) - # cursor pagination - cursored = [ - c - async for c in app.aget_state_history( - thread_1, limit=1, before=thread_1_history[0].config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = Pregel( + nodes={"one": one}, + channels={ + "total": BinaryOperatorAggregate(int, operator.add), + "input": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", + checkpointer=checkpointer, + debug=True, ) - ] - assert len(cursored) == 1 - assert cursored[0].config == thread_1_history[1].config - # the last checkpoint - assert thread_1_history[0].values["total"] == 16 - # the first "loop" checkpoint - assert thread_1_history[-2].values["total"] == 2 - # can get each checkpoint using aget with config - assert (await checkpointer.aget(thread_1_history[0].config))[ - "id" - ] == thread_1_history[0].config["configurable"]["checkpoint_id"] - assert (await checkpointer.aget(thread_1_history[1].config))[ - "id" - ] == thread_1_history[1].config["configurable"]["checkpoint_id"] - thread_1_next_config = await app.aupdate_state(thread_1_history[1].config, 10) - # update creates a new checkpoint - assert ( - thread_1_next_config["configurable"]["checkpoint_id"] - > thread_1_history[0].config["configurable"]["checkpoint_id"] - ) - # 1 more checkpoint in history - assert len([c async for c in app.aget_state_history(thread_1)]) == 8 - assert Counter( - [c.metadata["source"] async for c in app.aget_state_history(thread_1)] - ) == { - "update": 1, - "input": 4, - "loop": 3, - } - # the latest checkpoint is the updated one - assert await app.aget_state(thread_1) == await app.aget_state(thread_1_next_config) + thread_1 = {"configurable": {"thread_id": "1"}} + # total starts out as 0, so output is 0+2=2 + assert await app.ainvoke(2, thread_1) == 2 + state = await app.aget_state(thread_1) + assert state is not None + assert state.values.get("total") == 2 + assert ( + state.config["configurable"]["checkpoint_id"] + == (await checkpointer.aget(thread_1))["id"] + ) + # total is now 2, so output is 2+3=5 + assert await app.ainvoke(3, thread_1) == 5 + state = await app.aget_state(thread_1) + assert state is not None + assert state.values.get("total") == 7 + assert ( + state.config["configurable"]["checkpoint_id"] + == (await checkpointer.aget(thread_1))["id"] + ) + # total is now 2+5=7, so output would be 7+4=11, but raises ValueError + with pytest.raises(ValueError): + await app.ainvoke(4, thread_1) + # checkpoint is not updated + state = await app.aget_state(thread_1) + assert state is not None + assert state.values.get("total") == 7 + assert state.next == ("one",) + """we checkpoint inputs and it failed on "one", so the next node is one""" + # we can recover from error by sending new inputs + assert await app.ainvoke(2, thread_1) == 9 + state = await app.aget_state(thread_1) + assert state is not None + assert state.values.get("total") == 16, "total is now 7+9=16" + assert state.next == () + + thread_2 = {"configurable": {"thread_id": "2"}} + # on a new thread, total starts out as 0, so output is 0+5=5 + assert await app.ainvoke(5, thread_2) == 5 + state = await app.aget_state({"configurable": {"thread_id": "1"}}) + assert state is not None + assert state.values.get("total") == 16 + assert state.next == () + state = await app.aget_state(thread_2) + assert state is not None + assert state.values.get("total") == 5 + assert state.next == () + + assert len([c async for c in app.aget_state_history(thread_1, limit=1)]) == 1 + # list all checkpoints for thread 1 + thread_1_history = [c async for c in app.aget_state_history(thread_1)] + # there are 7 checkpoints + assert len(thread_1_history) == 7 + assert Counter(c.metadata["source"] for c in thread_1_history) == { + "input": 4, + "loop": 3, + } + # sorted descending + assert ( + thread_1_history[0].config["configurable"]["checkpoint_id"] + > thread_1_history[1].config["configurable"]["checkpoint_id"] + ) + # cursor pagination + cursored = [ + c + async for c in app.aget_state_history( + thread_1, limit=1, before=thread_1_history[0].config + ) + ] + assert len(cursored) == 1 + assert cursored[0].config == thread_1_history[1].config + # the last checkpoint + assert thread_1_history[0].values["total"] == 16 + # the first "loop" checkpoint + assert thread_1_history[-2].values["total"] == 2 + # can get each checkpoint using aget with config + assert (await checkpointer.aget(thread_1_history[0].config))[ + "id" + ] == thread_1_history[0].config["configurable"]["checkpoint_id"] + assert (await checkpointer.aget(thread_1_history[1].config))[ + "id" + ] == thread_1_history[1].config["configurable"]["checkpoint_id"] + + thread_1_next_config = await app.aupdate_state(thread_1_history[1].config, 10) + # update creates a new checkpoint + assert ( + thread_1_next_config["configurable"]["checkpoint_id"] + > thread_1_history[0].config["configurable"]["checkpoint_id"] + ) + # 1 more checkpoint in history + assert len([c async for c in app.aget_state_history(thread_1)]) == 8 + assert Counter( + [c.metadata["source"] async for c in app.aget_state_history(thread_1)] + ) == { + "update": 1, + "input": 4, + "loop": 3, + } + # the latest checkpoint is the updated one + assert await app.aget_state(thread_1) == await app.aget_state( + thread_1_next_config + ) async def test_invoke_two_processes_two_in_join_two_out(mocker: MockerFixture) -> None: @@ -2189,9 +2202,7 @@ async def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_conditional_graph( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: +async def test_conditional_graph(checkpointer_name: str) -> None: from copy import deepcopy from langchain_core.agents import AgentAction, AgentFinish @@ -2200,8 +2211,6 @@ async def test_conditional_graph( from langchain_core.runnables import RunnablePassthrough from langchain_core.tools import tool - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - # Assemble the tools @tool() def search_api(query: str) -> str: @@ -2458,82 +2467,75 @@ async def test_conditional_graph( }, ] - # test state get/update methods with interrupt_after + async with awith_checkpointer(checkpointer_name) as checkpointer: + # test state get/update methods with interrupt_after - app_w_interrupt = workflow.compile( - checkpointer=checkpointer, - interrupt_after=["agent"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"input": "what is weather in sf"}, config + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["agent"], ) - ] == [ - { - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - } - } - ] + config = {"configurable": {"thread_id": "1"}} - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - }, - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "loop", - "step": 0, - "writes": { + assert [ + c + async for c in app_w_interrupt.astream( + {"input": "what is weather in sf"}, config + ) + ] == [ + { "agent": { - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - } + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), } + } + ] + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 0, + "writes": { + "agent": { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - await app_w_interrupt.aupdate_state( - config, - { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "input": "what is weather in sf", - }, - ) - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { + await app_w_interrupt.aupdate_state( + config, + { "agent_outcome": AgentAction( tool="search_api", tool_input="query", @@ -2541,18 +2543,10 @@ async def test_conditional_graph( ), "input": "what is weather in sf", }, - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "update", - "step": 1, - "writes": { + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ "agent": { "agent_outcome": AgentAction( tool="search_api", @@ -2560,86 +2554,85 @@ async def test_conditional_graph( log="tool:search_api:a different query", ), "input": "what is weather in sf", - } + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "input": "what is weather in sf", - }, - }, - { - "tools": { - "input": "what is weather in sf", - "intermediate_steps": [ - [ - AgentAction( + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 1, + "writes": { + "agent": { + "agent_outcome": AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), - "result for query", - ] - ], - } - }, - { - "agent": { - "input": "what is weather in sf", - "intermediate_steps": [ - [ - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "result for query", - ] - ], - "agent_outcome": AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - } - }, - ] + "input": "what is weather in sf", + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - await app_w_interrupt.aupdate_state( - config, - { - "input": "what is weather in sf", - "intermediate_steps": [ - [ - AgentAction( + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), - "result for query", - ] - ], - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ), - }, - ) + "input": "what is weather in sf", + }, + }, + { + "tools": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + } + }, + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + ] - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { + await app_w_interrupt.aupdate_state( + config, + { "input": "what is weather in sf", "intermediate_steps": [ [ @@ -2656,18 +2649,10 @@ async def test_conditional_graph( log="finish:a really nice answer", ), }, - }, - tasks=(), - next=(), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "update", - "step": 4, - "writes": { + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ "agent": { "input": "what is weather in sf", "intermediate_steps": [ @@ -2684,91 +2669,112 @@ async def test_conditional_graph( return_values={"answer": "a really nice answer"}, log="finish:a really nice answer", ), - } + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - # test state get/update methods with interrupt_before - - app_w_interrupt = workflow.compile( - checkpointer=checkpointer, - interrupt_before=["tools"], - ) - config = {"configurable": {"thread_id": "2"}} - llm.i = 0 - - assert [ - c - async for c in app_w_interrupt.astream( - {"input": "what is weather in sf"}, config - ) - ] == [ - { - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - } - } - ] - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - }, - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "loop", - "step": 0, - "writes": { - "agent": { + tasks=(), + next=(), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 4, + "writes": { "agent": { "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", ), } - } + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - await app_w_interrupt.aupdate_state( - config, - { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "input": "what is weather in sf", - }, - ) + # test state get/update methods with interrupt_before - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_before=["tools"], + ) + config = {"configurable": {"thread_id": "2"}} + llm.i = 0 + + assert [ + c + async for c in app_w_interrupt.astream( + {"input": "what is weather in sf"}, config + ) + ] == [ + { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + } + ] + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + }, + }, + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 0, + "writes": { + "agent": { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + await app_w_interrupt.aupdate_state( + config, + { "agent_outcome": AgentAction( tool="search_api", tool_input="query", @@ -2776,18 +2782,10 @@ async def test_conditional_graph( ), "input": "what is weather in sf", }, - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "update", - "step": 1, - "writes": { + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ "agent": { "agent_outcome": AgentAction( tool="search_api", @@ -2795,86 +2793,85 @@ async def test_conditional_graph( log="tool:search_api:a different query", ), "input": "what is weather in sf", - } + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "input": "what is weather in sf", - }, - }, - { - "tools": { - "input": "what is weather in sf", - "intermediate_steps": [ - [ - AgentAction( + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 1, + "writes": { + "agent": { + "agent_outcome": AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), - "result for query", - ] - ], - } - }, - { - "agent": { - "input": "what is weather in sf", - "intermediate_steps": [ - [ - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "result for query", - ] - ], - "agent_outcome": AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - } - }, - ] + "input": "what is weather in sf", + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - await app_w_interrupt.aupdate_state( - config, - { - "input": "what is weather in sf", - "intermediate_steps": [ - [ - AgentAction( + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( tool="search_api", tool_input="query", log="tool:search_api:a different query", ), - "result for query", - ] - ], - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ), - }, - ) + "input": "what is weather in sf", + }, + }, + { + "tools": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + } + }, + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + ] - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { + await app_w_interrupt.aupdate_state( + config, + { "input": "what is weather in sf", "intermediate_steps": [ [ @@ -2891,18 +2888,10 @@ async def test_conditional_graph( log="finish:a really nice answer", ), }, - }, - tasks=(), - next=(), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "update", - "step": 4, - "writes": { + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ "agent": { "input": "what is weather in sf", "intermediate_steps": [ @@ -2919,196 +2908,234 @@ async def test_conditional_graph( return_values={"answer": "a really nice answer"}, log="finish:a really nice answer", ), - } + }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - # test re-invoke to continue with interrupt_before - - app_w_interrupt = workflow.compile( - checkpointer=checkpointer, - interrupt_before=["tools"], - ) - config = {"configurable": {"thread_id": "3"}} - llm.i = 0 # reset the llm - - assert [ - c - async for c in app_w_interrupt.astream( - {"input": "what is weather in sf"}, config - ) - ] == [ - { - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - } - } - ] - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - }, - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "loop", - "step": 0, - "writes": { - "agent": { + tasks=(), + next=(), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 4, + "writes": { "agent": { "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", ), } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + # test re-invoke to continue with interrupt_before + + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_before=["tools"], + ) + config = {"configurable": {"thread_id": "3"}} + llm.i = 0 # reset the llm + + assert [ + c + async for c in app_w_interrupt.astream( + {"input": "what is weather in sf"}, config + ) + ] == [ + { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + } + ] + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + }, + }, + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 0, + "writes": { + "agent": { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + }, + }, + { + "tools": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ] + ], } }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "agent": { - "input": "what is weather in sf", - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ] + ], + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } }, - }, - { - "tools": { - "input": "what is weather in sf", - "intermediate_steps": [ - [ - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - "result for query", - ] - ], - } - }, - { - "agent": { - "input": "what is weather in sf", - "intermediate_steps": [ - [ - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - "result for query", - ] - ], - "agent_outcome": AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - } - }, - ] + ] - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "agent": { - "input": "what is weather in sf", - "intermediate_steps": [ - [ - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - "result for query", - ] - ], - "agent_outcome": AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - } - }, - { - "tools": { - "input": "what is weather in sf", - "intermediate_steps": [ - [ - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - "result for query", + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ] ], - [ - AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - "result for another", + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + { + "tools": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ], + [ + AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + "result for another", + ], ], - ], - } - }, - { - "agent": { - "input": "what is weather in sf", - "intermediate_steps": [ - [ - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - "result for query", + } + }, + { + "agent": { + "input": "what is weather in sf", + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ], + [ + AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + "result for another", + ], ], - [ - AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - "result for another", - ], - ], - "agent_outcome": AgentFinish( - return_values={"answer": "answer"}, log="finish:answer" - ), - } - }, - ] + "agent_outcome": AgentFinish( + return_values={"answer": "answer"}, log="finish:answer" + ), + } + }, + ] -async def test_conditional_graph_state(mocker: MockerFixture) -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_conditional_graph_state( + mocker: MockerFixture, checkpointer_name: str +) -> None: from langchain_core.agents import AgentAction, AgentFinish from langchain_core.language_models.fake import FakeStreamingListLLM from langchain_core.prompts import PromptTemplate @@ -3345,15 +3372,201 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: }, ] - # test state get/update methods with interrupt_after + async with awith_checkpointer(checkpointer_name) as checkpointer: + # test state get/update methods with interrupt_after - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["agent"], - ) - config = {"configurable": {"thread_id": "1"}} + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["agent"], + ) + config = {"configurable": {"thread_id": "1"}} + + async with assert_ctx_once(): + assert [ + c + async for c in app_w_interrupt.astream( + {"input": "what is weather in sf"}, config + ) + ] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + }, + ] + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "intermediate_steps": [], + }, + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + async with assert_ctx_once(): + await app_w_interrupt.aupdate_state( + config, + { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ) + }, + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "intermediate_steps": [], + }, + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 2, + "writes": { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + async with assert_ctx_once(): + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "tools": { + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + } + }, + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + ] + + async with assert_ctx_once(): + await app_w_interrupt.aupdate_state( + config, + { + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ) + }, + ) + + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ), + "intermediate_steps": [ + [ + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "result for query", + ] + ], + }, + tasks=(), + next=(), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 5, + "writes": { + "agent": { + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + # test state get/update methods with interrupt_before + + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_before=["tools"], + ) + config = {"configurable": {"thread_id": "2"}} + llm.i = 0 # reset the llm - async with assert_ctx_once(): assert [ c async for c in app_w_interrupt.astream( @@ -3371,41 +3584,38 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: }, ] - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - "intermediate_steps": [], - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - } + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + "intermediate_steps": [], }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - async with assert_ctx_once(): await app_w_interrupt.aupdate_state( config, { @@ -3417,41 +3627,40 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: }, ) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "intermediate_steps": [], - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "update", - "step": 2, - "writes": { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ) - } + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ), + "intermediate_steps": [], }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 2, + "writes": { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:a different query", + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - async with assert_ctx_once(): assert [c async for c in app_w_interrupt.astream(None, config)] == [ { "tools": { @@ -3478,7 +3687,6 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: }, ] - async with assert_ctx_once(): await app_w_interrupt.aupdate_state( config, { @@ -3489,151 +3697,12 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: }, ) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ), - "intermediate_steps": [ - [ - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "result for query", - ] - ], - }, - tasks=(), - next=(), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "update", - "step": 5, - "writes": { - "agent": { - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ) - } - }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - # test state get/update methods with interrupt_before - - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_before=["tools"], - ) - config = {"configurable": {"thread_id": "2"}} - llm.i = 0 # reset the llm - - assert [ - c - async for c in app_w_interrupt.astream( - {"input": "what is weather in sf"}, config - ) - ] == [ - { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", ), - } - }, - ] - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentAction( - tool="search_api", tool_input="query", log="tool:search_api:query" - ), - "intermediate_steps": [], - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:query", - ), - } - }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - await app_w_interrupt.aupdate_state( - config, - { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ) - }, - ) - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "intermediate_steps": [], - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "update", - "step": 2, - "writes": { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ) - } - }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "tools": { "intermediate_steps": [ [ AgentAction( @@ -3644,69 +3713,30 @@ async def test_conditional_graph_state(mocker: MockerFixture) -> None: "result for query", ] ], - } - }, - { - "agent": { - "agent_outcome": AgentAction( - tool="search_api", - tool_input="another", - log="tool:search_api:another", - ), - } - }, - ] - - await app_w_interrupt.aupdate_state( - config, - { - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ) - }, - ) - - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ), - "intermediate_steps": [ - [ - AgentAction( - tool="search_api", - tool_input="query", - log="tool:search_api:a different query", - ), - "result for query", - ] - ], - }, - tasks=(), - next=(), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "update", - "step": 5, - "writes": { - "agent": { - "agent_outcome": AgentFinish( - return_values={"answer": "a really nice answer"}, - log="finish:a really nice answer", - ) - } }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + tasks=(), + next=(), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 5, + "writes": { + "agent": { + "agent_outcome": AgentFinish( + return_values={"answer": "a really nice answer"}, + log="finish:a really nice answer", + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) async def test_conditional_entrypoint_graph() -> None: @@ -3983,9 +4013,7 @@ class ToolInput(BaseModel, arbitrary_types_allowed=True): @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_state_graph_packets( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: +async def test_state_graph_packets(checkpointer_name: str) -> None: from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, ) @@ -3997,8 +4025,6 @@ async def test_state_graph_packets( ) from langchain_core.tools import tool - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - class AgentState(TypedDict): messages: Annotated[list[BaseMessage], add_messages] session: Annotated[httpx.AsyncClient, Context(httpx.AsyncClient)] @@ -4220,63 +4246,20 @@ async def test_state_graph_packets( {"agent": {"messages": AIMessage(content="answer", id="ai3")}}, ] - app_w_interrupt = workflow.compile( - checkpointer=checkpointer, - interrupt_after=["agent"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"messages": HumanMessage(content="what is weather in sf")}, config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["agent"], ) - ] == [ - { - "agent": { - "messages": AIMessage( - id="ai1", - content="", - tool_calls=[ - { - "id": "tool_call123", - "name": "search_api", - "args": {"query": "query"}, - }, - ], - ) - } - }, - ] + config = {"configurable": {"thread_id": "1"}} - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "messages": [ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id="ai1", - content="", - tool_calls=[ - { - "id": "tool_call123", - "name": "search_api", - "args": {"query": "query"}, - }, - ], - ), - ] - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, - created_at=(await app_w_interrupt.checkpointer.aget_tuple(config)).checkpoint[ - "ts" - ], - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": { + assert [ + c + async for c in app_w_interrupt.astream( + {"messages": HumanMessage(content="what is weather in sf")}, config + ) + ] == [ + { "agent": { "messages": AIMessage( id="ai1", @@ -4291,47 +4274,68 @@ async def test_state_graph_packets( ) } }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + ] - # modify ai message - last_message = (await app_w_interrupt.aget_state(config)).values["messages"][-1] - last_message.tool_calls[0]["args"]["query"] = "a different query" - await app_w_interrupt.aupdate_state(config, {"messages": last_message}) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + id="ai1", + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "query"}, + }, + ], + ), + ] + }, + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=(await app_w_interrupt.checkpointer.aget_tuple(config)).config, + created_at=( + await app_w_interrupt.checkpointer.aget_tuple(config) + ).checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": { + "agent": { + "messages": AIMessage( + id="ai1", + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "query"}, + }, + ], + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - # message was replaced instead of appended - tup = await app_w_interrupt.checkpointer.aget_tuple(config) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "messages": [ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id="ai1", - content="", - tool_calls=[ - { - "id": "tool_call123", - "name": "search_api", - "args": {"query": "a different query"}, - }, - ], - ), - ] - }, - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=tup.config, - created_at=tup.checkpoint["ts"], - metadata={ - "parents": {}, - "source": "update", - "step": 2, - "writes": { - "agent": { - "messages": AIMessage( + # modify ai message + last_message = (await app_w_interrupt.aget_state(config)).values["messages"][-1] + last_message.tool_calls[0]["args"]["query"] = "a different query" + await app_w_interrupt.aupdate_state(config, {"messages": last_message}) + + # message was replaced instead of appended + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( id="ai1", content="", tool_calls=[ @@ -4341,97 +4345,50 @@ async def test_state_graph_packets( "args": {"query": "a different query"}, }, ], + ), + ] + }, + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 2, + "writes": { + "agent": { + "messages": AIMessage( + id="ai1", + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "a different query"}, + }, + ], + ) + } + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "tools": { + "messages": ToolMessage( + content="result for a different query", + name="search_api", + id=AnyStr(), + tool_call_id="tool_call123", ) } }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "tools": { - "messages": ToolMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - tool_call_id="tool_call123", - ) - } - }, - { - "agent": { - "messages": AIMessage( - id="ai2", - content="", - tool_calls=[ - { - "id": "tool_call234", - "name": "search_api", - "args": {"query": "another", "idx": 0}, - }, - { - "id": "tool_call567", - "name": "search_api", - "args": {"query": "a third one", "idx": 1}, - }, - ], - ) - }, - }, - ] - - tup = await app_w_interrupt.checkpointer.aget_tuple(config) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "messages": [ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id="ai1", - content="", - tool_calls=[ - { - "id": "tool_call123", - "name": "search_api", - "args": {"query": "a different query"}, - }, - ], - ), - ToolMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - tool_call_id="tool_call123", - ), - AIMessage( - id="ai2", - content="", - tool_calls=[ - { - "id": "tool_call234", - "name": "search_api", - "args": {"query": "another", "idx": 0}, - }, - { - "id": "tool_call567", - "name": "search_api", - "args": {"query": "a third one", "idx": 1}, - }, - ], - ), - ] - }, - tasks=(PregelTask(AnyStr(), "tools"), PregelTask(AnyStr(), "tools")), - next=("tools", "tools"), - config=tup.config, - created_at=tup.checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 4, - "writes": { + { "agent": { "messages": AIMessage( id="ai2", @@ -4451,63 +4408,133 @@ async def test_state_graph_packets( ) }, }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + ] - await app_w_interrupt.aupdate_state( - config, - {"messages": AIMessage(content="answer", id="ai2")}, - ) + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + id="ai1", + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "a different query"}, + }, + ], + ), + ToolMessage( + content="result for a different query", + name="search_api", + id=AnyStr(), + tool_call_id="tool_call123", + ), + AIMessage( + id="ai2", + content="", + tool_calls=[ + { + "id": "tool_call234", + "name": "search_api", + "args": {"query": "another", "idx": 0}, + }, + { + "id": "tool_call567", + "name": "search_api", + "args": {"query": "a third one", "idx": 1}, + }, + ], + ), + ] + }, + tasks=(PregelTask(AnyStr(), "tools"), PregelTask(AnyStr(), "tools")), + next=("tools", "tools"), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 4, + "writes": { + "agent": { + "messages": AIMessage( + id="ai2", + content="", + tool_calls=[ + { + "id": "tool_call234", + "name": "search_api", + "args": {"query": "another", "idx": 0}, + }, + { + "id": "tool_call567", + "name": "search_api", + "args": {"query": "a third one", "idx": 1}, + }, + ], + ) + }, + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) - # replaces message even if object identity is different, as long as id is the same - tup = await app_w_interrupt.checkpointer.aget_tuple(config) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "messages": [ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - id="ai1", - content="", - tool_calls=[ - { - "id": "tool_call123", - "name": "search_api", - "args": {"query": "a different query"}, - }, - ], - ), - ToolMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - tool_call_id="tool_call123", - ), - AIMessage(content="answer", id="ai2"), - ] - }, - tasks=(), - next=(), - config=tup.config, - created_at=tup.checkpoint["ts"], - metadata={ - "parents": {}, - "source": "update", - "step": 5, - "writes": {"agent": {"messages": AIMessage(content="answer", id="ai2")}}, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + await app_w_interrupt.aupdate_state( + config, + {"messages": AIMessage(content="answer", id="ai2")}, + ) + + # replaces message even if object identity is different, as long as id is the same + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "messages": [ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + id="ai1", + content="", + tool_calls=[ + { + "id": "tool_call123", + "name": "search_api", + "args": {"query": "a different query"}, + }, + ], + ), + ToolMessage( + content="result for a different query", + name="search_api", + id=AnyStr(), + tool_call_id="tool_call123", + ), + AIMessage(content="answer", id="ai2"), + ] + }, + tasks=(), + next=(), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 5, + "writes": { + "agent": {"messages": AIMessage(content="answer", id="ai2")} + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_message_graph( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: +async def test_message_graph(checkpointer_name: str) -> None: from langchain_core.agents import AgentAction from langchain_core.language_models.fake_chat_models import ( FakeMessagesListChatModel, @@ -4515,8 +4542,6 @@ async def test_message_graph( from langchain_core.messages import AIMessage, FunctionMessage, HumanMessage from langchain_core.tools import tool - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - class FakeFuntionChatModel(FakeMessagesListChatModel): def bind_functions(self, functions: list): return self @@ -4679,50 +4704,20 @@ async def test_message_graph( {"agent": AIMessage(content="answer", id="ai3")}, ] - app_w_interrupt = workflow.compile( - checkpointer=checkpointer, - interrupt_after=["agent"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - HumanMessage(content="what is weather in sf"), config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["agent"], ) - ] == [ - { - "agent": AIMessage( - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} - }, - id="ai1", - ) - }, - ] + config = {"configurable": {"thread_id": "1"}} - tup = await app_w_interrupt.checkpointer.aget_tuple(config) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values=[ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"query"'} - }, - id="ai1", - ), - ], - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=tup.config, - created_at=tup.checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": { + assert [ + c + async for c in app_w_interrupt.astream( + HumanMessage(content="what is weather in sf"), config + ) + ] == [ + { "agent": AIMessage( content="", additional_kwargs={ @@ -4731,43 +4726,59 @@ async def test_message_graph( id="ai1", ) }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + ] - # modify ai message - last_message = (await app_w_interrupt.aget_state(config)).values[-1] - last_message.additional_kwargs["function_call"]["arguments"] = '"a different query"' - await app_w_interrupt.aupdate_state(config, last_message) - - # message was replaced instead of appended - tup = await app_w_interrupt.checkpointer.aget_tuple(config) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values=[ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": '"a different query"', - } + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values=[ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": {"name": "search_api", "arguments": '"query"'} + }, + id="ai1", + ), + ], + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": { + "agent": AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"query"', + } + }, + id="ai1", + ) }, - id="ai1", - ), - ], - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=tup.config, - created_at=tup.checkpoint["ts"], - metadata={ - "parents": {}, - "source": "update", - "step": 2, - "writes": { - "agent": AIMessage( + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + # modify ai message + last_message = (await app_w_interrupt.aget_state(config)).values[-1] + last_message.additional_kwargs["function_call"]["arguments"] = ( + '"a different query"' + ) + await app_w_interrupt.aupdate_state(config, last_message) + + # message was replaced instead of appended + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values=[ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( content="", additional_kwargs={ "function_call": { @@ -4776,69 +4787,43 @@ async def test_message_graph( } }, id="ai1", + ), + ], + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 2, + "writes": { + "agent": AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"a different query"', + } + }, + id="ai1", + ) + }, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + { + "tools": FunctionMessage( + content="result for a different query", + name="search_api", + id=AnyStr(), ) }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) - - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - { - "tools": FunctionMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - ) - }, - { - "agent": AIMessage( - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"another"'} - }, - id="ai2", - ) - }, - ] - - tup = await app_w_interrupt.checkpointer.aget_tuple(config) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values=[ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": '"a different query"', - } - }, - id="ai1", - ), - FunctionMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - ), - AIMessage( - content="", - additional_kwargs={ - "function_call": {"name": "search_api", "arguments": '"another"'} - }, - id="ai2", - ), - ], - tasks=(PregelTask(AnyStr(), "tools"),), - next=("tools",), - config=tup.config, - created_at=tup.checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 4, - "writes": { + { "agent": AIMessage( content="", additional_kwargs={ @@ -4850,53 +4835,105 @@ async def test_message_graph( id="ai2", ) }, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + ] - await app_w_interrupt.aupdate_state( - config, - AIMessage(content="answer", id="ai2"), - ) - - # replaces message even if object identity is different, as long as id is the same - tup = await app_w_interrupt.checkpointer.aget_tuple(config) - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values=[ - _AnyIdHumanMessage(content="what is weather in sf"), - AIMessage( - content="", - additional_kwargs={ - "function_call": { - "name": "search_api", - "arguments": '"a different query"', - } + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values=[ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"a different query"', + } + }, + id="ai1", + ), + FunctionMessage( + content="result for a different query", + name="search_api", + id=AnyStr(), + ), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"another"', + } + }, + id="ai2", + ), + ], + tasks=(PregelTask(AnyStr(), "tools"),), + next=("tools",), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "loop", + "step": 4, + "writes": { + "agent": AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"another"', + } + }, + id="ai2", + ) }, - id="ai1", - ), - FunctionMessage( - content="result for a different query", - name="search_api", - id=AnyStr(), - ), + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) + + await app_w_interrupt.aupdate_state( + config, AIMessage(content="answer", id="ai2"), - ], - tasks=(), - next=(), - config=tup.config, - created_at=tup.checkpoint["ts"], - metadata={ - "parents": {}, - "source": "update", - "step": 5, - "writes": {"agent": AIMessage(content="answer", id="ai2")}, - }, - parent_config=[ - c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) - ][-1].config, - ) + ) + + # replaces message even if object identity is different, as long as id is the same + tup = await app_w_interrupt.checkpointer.aget_tuple(config) + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values=[ + _AnyIdHumanMessage(content="what is weather in sf"), + AIMessage( + content="", + additional_kwargs={ + "function_call": { + "name": "search_api", + "arguments": '"a different query"', + } + }, + id="ai1", + ), + FunctionMessage( + content="result for a different query", + name="search_api", + id=AnyStr(), + ), + AIMessage(content="answer", id="ai2"), + ], + tasks=(), + next=(), + config=tup.config, + created_at=tup.checkpoint["ts"], + metadata={ + "parents": {}, + "source": "update", + "step": 5, + "writes": {"agent": AIMessage(content="answer", id="ai2")}, + }, + parent_config=[ + c async for c in app_w_interrupt.checkpointer.alist(config, limit=2) + ][-1].config, + ) async def test_in_one_fan_out_out_one_graph_state() -> None: @@ -5126,11 +5163,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_start_branch_then( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - +async def test_start_branch_then(checkpointer_name: str) -> None: class State(TypedDict): my_key: Annotated[str, operator.add] market: str @@ -5175,172 +5208,183 @@ async def test_start_branch_then( "market": "US", } - tool_two = tool_two_graph.compile( - store=MemoryStore(), - checkpointer=checkpointer, - interrupt_before=["tool_two_fast", "tool_two_slow"], - ) + async with awith_checkpointer(checkpointer_name) as checkpointer: + tool_two = tool_two_graph.compile( + store=MemoryStore(), + checkpointer=checkpointer, + interrupt_before=["tool_two_fast", "tool_two_slow"], + ) - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - await tool_two.ainvoke({"my_key": "value", "market": "DE"}) + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - thread1 = {"configurable": {"thread_id": "1", "assistant_id": "a"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { - "my_key": "value", - "market": "DE", - } - assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ - { - "parents": {}, - "source": "loop", - "step": 0, - "writes": None, - }, - { - "parents": {}, - "source": "input", - "step": -1, - "writes": {"__start__": {"my_key": "value", "market": "DE"}}, - }, - ] - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], - metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, - parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ - -1 - ].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread1, debug=1) == { - "my_key": "value slow", - "market": "DE", - } - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value slow", "market": "DE"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": {"tool_two_slow": {"my_key": " slow"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ - -1 - ].config, - ) + thread1 = {"configurable": {"thread_id": "1", "assistant_id": "a"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { + "my_key": "value", + "market": "DE", + } + assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ + { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + }, + { + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": {"my_key": "value", "market": "DE"}}, + }, + ] + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ + "ts" + ], + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread1, limit=2) + ][-1].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread1, debug=1) == { + "my_key": "value slow", + "market": "DE", + } + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value slow", "market": "DE"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"tool_two_slow": {"my_key": " slow"}}, + }, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread1, limit=2) + ][-1].config, + ) - thread2 = {"configurable": {"thread_id": "2", "assistant_id": "a"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], - metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, - parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ - -1 - ].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread2, debug=1) == { - "my_key": "value fast", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value fast", "market": "US"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": {"tool_two_fast": {"my_key": " fast"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ - -1 - ].config, - ) + thread2 = {"configurable": {"thread_id": "2", "assistant_id": "a"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ + "ts" + ], + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread2, limit=2) + ][-1].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread2, debug=1) == { + "my_key": "value fast", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value fast", "market": "US"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"tool_two_fast": {"my_key": " fast"}}, + }, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread2, limit=2) + ][-1].config, + ) - thread3 = {"configurable": {"thread_id": "3", "assistant_id": "b"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread3) == { - "my_key": "value", - "market": "US", - } - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "value", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=(await tool_two.checkpointer.aget_tuple(thread3)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], - metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, - parent_config=[c async for c in tool_two.checkpointer.alist(thread3, limit=2)][ - -1 - ].config, - ) - # update state - await tool_two.aupdate_state(thread3, {"my_key": "key"}) # appends to my_key - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "valuekey", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=(await tool_two.checkpointer.aget_tuple(thread3)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "update", - "step": 1, - "writes": {START: {"my_key": "key"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread3, limit=2)][ - -1 - ].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread3, debug=1) == { - "my_key": "valuekey fast", - "market": "US", - } - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "valuekey fast", "market": "US"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread3)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 2, - "writes": {"tool_two_fast": {"my_key": " fast"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread3, limit=2)][ - -1 - ].config, - ) + thread3 = {"configurable": {"thread_id": "3", "assistant_id": "b"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread3) == { + "my_key": "value", + "market": "US", + } + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "value", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=(await tool_two.checkpointer.aget_tuple(thread3)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ + "ts" + ], + metadata={"parents": {}, "source": "loop", "step": 0, "writes": None}, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread3, limit=2) + ][-1].config, + ) + # update state + await tool_two.aupdate_state(thread3, {"my_key": "key"}) # appends to my_key + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "valuekey", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=(await tool_two.checkpointer.aget_tuple(thread3)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "update", + "step": 1, + "writes": {START: {"my_key": "key"}}, + }, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread3, limit=2) + ][-1].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread3, debug=1) == { + "my_key": "valuekey fast", + "market": "US", + } + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "valuekey fast", "market": "US"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread3)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 2, + "writes": {"tool_two_fast": {"my_key": " fast"}}, + }, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread3, limit=2) + ][-1].config, + ) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_branch_then( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - +async def test_branch_then(checkpointer_name: str) -> None: class State(TypedDict): my_key: Annotated[str, operator.add] market: str @@ -5368,600 +5412,629 @@ async def test_branch_then( "market": "US", } - # test stream_mode=debug - tool_two = tool_two_graph.compile(checkpointer=checkpointer) - thread10 = {"configurable": {"thread_id": "10"}} - assert [ - c - async for c in tool_two.astream( - {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" + async with awith_checkpointer(checkpointer_name) as checkpointer: + # test stream_mode=debug + tool_two = tool_two_graph.compile(checkpointer=checkpointer) + thread10 = {"configurable": {"thread_id": "10"}} + assert [ + c + async for c in tool_two.astream( + {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" + ) + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": -1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": {"my_key": ""}, + "metadata": { + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": {"my_key": "value", "market": "DE"}}, + }, + "next": ["__start__"], + "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + }, + "next": ["prepare"], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "name": "prepare", + "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + "next": ["tool_two_slow"], + "tasks": [ + {"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()} + ], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "name": "tool_two_slow", + "input": {"my_key": "value prepared", "market": "DE"}, + "triggers": ["branch:prepare:condition:tool_two_slow"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "name": "tool_two_slow", + "result": [("my_key", " slow")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared slow", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 2, + "writes": {"tool_two_slow": {"my_key": " slow"}}, + }, + "next": ["finish"], + "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "name": "finish", + "input": {"my_key": "value prepared slow", "market": "DE"}, + "triggers": ["branch:prepare:condition::then"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "name": "finish", + "result": [("my_key", " finished")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "10"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "10", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared slow finished", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + "next": [], + "tasks": [], + }, + }, + ] + + tool_two = tool_two_graph.compile( + checkpointer=checkpointer, + interrupt_before=["tool_two_fast", "tool_two_slow"], ) - ] == [ - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": -1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": {"my_key": ""}, - "metadata": { - "parents": {}, - "source": "input", - "step": -1, - "writes": {"__start__": {"my_key": "value", "market": "DE"}}, - }, - "next": ["__start__"], - "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 0, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value", - "market": "DE", - }, - "metadata": { - "parents": {}, - "source": "loop", - "step": 0, - "writes": None, - }, - "next": ["prepare"], - "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], - }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", - "name": "prepare", - "input": {"my_key": "value", "market": "DE"}, - "triggers": ["start:prepare"], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", - "name": "prepare", - "result": [("my_key", " prepared")], - "error": None, - "interrupts": [], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value prepared", - "market": "DE", - }, - "metadata": { - "parents": {}, - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - "next": ["tool_two_slow"], - "tasks": [{"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()}], - }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", - "name": "tool_two_slow", - "input": {"my_key": "value prepared", "market": "DE"}, - "triggers": ["branch:prepare:condition:tool_two_slow"], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", - "name": "tool_two_slow", - "result": [("my_key", " slow")], - "error": None, - "interrupts": [], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value prepared slow", - "market": "DE", - }, - "metadata": { - "parents": {}, - "source": "loop", - "step": 2, - "writes": {"tool_two_slow": {"my_key": " slow"}}, - }, - "next": ["finish"], - "tasks": [{"id": AnyStr(), "name": "finish", "interrupts": ()}], - }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", - "name": "finish", - "input": {"my_key": "value prepared slow", "market": "DE"}, - "triggers": ["branch:prepare:condition::then"], - }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", - "name": "finish", - "result": [("my_key", " finished")], - "error": None, - "interrupts": [], - }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "10"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "10", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value prepared slow finished", - "market": "DE", - }, - "metadata": { - "parents": {}, - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - "next": [], - "tasks": [], - }, - }, - ] - tool_two = tool_two_graph.compile( - checkpointer=checkpointer, interrupt_before=["tool_two_fast", "tool_two_slow"] - ) + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - - thread1 = {"configurable": {"thread_id": "11"}} - # stop when about to enter node - assert [ - c - async for c in tool_two.astream( - {"my_key": "value", "market": "DE"}, thread1, stream_mode="debug" + thread1 = {"configurable": {"thread_id": "11"}} + # stop when about to enter node + assert [ + c + async for c in tool_two.astream( + {"my_key": "value", "market": "DE"}, thread1, stream_mode="debug" + ) + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": -1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "11"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "11", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": {"my_key": ""}, + "metadata": { + "parents": {}, + "source": "input", + "step": -1, + "writes": {"__start__": {"my_key": "value", "market": "DE"}}, + }, + "next": ["__start__"], + "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "11"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "11", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 0, + "writes": None, + }, + "next": ["prepare"], + "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "1a591be4-f85c-558f-8d00-1ccac0d1877f", + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "1a591be4-f85c-558f-8d00-1ccac0d1877f", + "name": "prepare", + "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": { + "tags": [], + "metadata": {"thread_id": "11"}, + "callbacks": None, + "recursion_limit": 25, + "configurable": { + "thread_id": "11", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + }, + }, + "values": { + "my_key": "value prepared", + "market": "DE", + }, + "metadata": { + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + "next": ["tool_two_slow"], + "tasks": [ + {"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()} + ], + }, + }, + ] + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread1, limit=2) + ][-1].config, ) - ] == [ - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": -1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "11"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "11", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": {"my_key": ""}, - "metadata": { - "parents": {}, - "source": "input", - "step": -1, - "writes": {"__start__": {"my_key": "value", "market": "DE"}}, - }, - "next": ["__start__"], - "tasks": [{"id": AnyStr(), "name": "__start__", "interrupts": ()}], + # resume, for same result as above + assert await tool_two.ainvoke(None, thread1, debug=1) == { + "my_key": "value prepared slow finished", + "market": "DE", + } + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 0, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "11"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "11", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value", - "market": "DE", - }, - "metadata": { - "parents": {}, - "source": "loop", - "step": 0, - "writes": None, - }, - "next": ["prepare"], - "tasks": [{"id": AnyStr(), "name": "prepare", "interrupts": ()}], + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread1, limit=2) + ][-1].config, + ) + + thread2 = {"configurable": {"thread_id": "12"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value prepared", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value prepared", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, }, - }, - { - "type": "task", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "1a591be4-f85c-558f-8d00-1ccac0d1877f", - "name": "prepare", - "input": {"my_key": "value", "market": "DE"}, - "triggers": ["start:prepare"], + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread2, limit=2) + ][-1].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread2, debug=1) == { + "my_key": "value prepared fast finished", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value prepared fast finished", "market": "US"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": "1a591be4-f85c-558f-8d00-1ccac0d1877f", - "name": "prepare", - "result": [("my_key", " prepared")], - "error": None, - "interrupts": [], + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread2, limit=2) + ][-1].config, + ) + + tool_two = tool_two_graph.compile( + checkpointer=checkpointer, interrupt_after=["prepare"] + ) + + # missing thread_id + with pytest.raises(ValueError, match="thread_id"): + await tool_two.ainvoke({"my_key": "value", "market": "DE"}) + + thread1 = {"configurable": {"thread_id": "21"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { + "my_key": "value prepared", + "market": "DE", + } + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, }, - }, - { - "type": "checkpoint", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "config": { - "tags": [], - "metadata": {"thread_id": "11"}, - "callbacks": None, - "recursion_limit": 25, - "configurable": { - "thread_id": "11", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - }, - }, - "values": { - "my_key": "value prepared", - "market": "DE", - }, - "metadata": { - "parents": {}, - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - "next": ["tool_two_slow"], - "tasks": [{"id": AnyStr(), "name": "tool_two_slow", "interrupts": ()}], + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread1, limit=2) + ][-1].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread1, debug=1) == { + "my_key": "value prepared slow finished", + "market": "DE", + } + assert await tool_two.aget_state(thread1) == StateSnapshot( + values={"my_key": "value prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread1)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, }, - }, - ] - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ - -1 - ].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread1, debug=1) == { - "my_key": "value prepared slow finished", - "market": "DE", - } - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ - -1 - ].config, - ) + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread1, limit=2) + ][-1].config, + ) - thread2 = {"configurable": {"thread_id": "12"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value prepared", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value prepared", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ - -1 - ].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread2, debug=1) == { - "my_key": "value prepared fast finished", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value prepared fast finished", "market": "US"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ - -1 - ].config, - ) + thread2 = {"configurable": {"thread_id": "22"}} + # stop when about to enter node + assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { + "my_key": "value prepared", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value prepared", "market": "US"}, + tasks=(PregelTask(AnyStr(), "tool_two_fast"),), + next=("tool_two_fast",), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread2, limit=2) + ][-1].config, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread2, debug=1) == { + "my_key": "value prepared fast finished", + "market": "US", + } + assert await tool_two.aget_state(thread2) == StateSnapshot( + values={"my_key": "value prepared fast finished", "market": "US"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread2)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread2, limit=2) + ][-1].config, + ) - tool_two = tool_two_graph.compile( - checkpointer=checkpointer, interrupt_after=["prepare"] - ) - - # missing thread_id - with pytest.raises(ValueError, match="thread_id"): - await tool_two.ainvoke({"my_key": "value", "market": "DE"}) - - thread1 = {"configurable": {"thread_id": "21"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "DE"}, thread1) == { - "my_key": "value prepared", - "market": "DE", - } - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ - -1 - ].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread1, debug=1) == { - "my_key": "value prepared slow finished", - "market": "DE", - } - assert await tool_two.aget_state(thread1) == StateSnapshot( - values={"my_key": "value prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread1)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread1)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread1, limit=2)][ - -1 - ].config, - ) - - thread2 = {"configurable": {"thread_id": "22"}} - # stop when about to enter node - assert await tool_two.ainvoke({"my_key": "value", "market": "US"}, thread2) == { - "my_key": "value prepared", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value prepared", "market": "US"}, - tasks=(PregelTask(AnyStr(), "tool_two_fast"),), - next=("tool_two_fast",), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ - -1 - ].config, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread2, debug=1) == { - "my_key": "value prepared fast finished", - "market": "US", - } - assert await tool_two.aget_state(thread2) == StateSnapshot( - values={"my_key": "value prepared fast finished", "market": "US"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread2)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread2)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread2, limit=2)][ - -1 - ].config, - ) - - thread3 = {"configurable": {"thread_id": "23"}} - # update an empty thread before first run - uconfig = await tool_two.aupdate_state(thread3, {"my_key": "key", "market": "DE"}) - # check current state - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "key", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "prepare"),), - next=("prepare",), - config=uconfig, - created_at=AnyStr(), - metadata={ - "parents": {}, - "source": "update", - "step": 0, - "writes": {START: {"my_key": "key", "market": "DE"}}, - }, - parent_config=None, - ) - # run from this point - assert await tool_two.ainvoke(None, thread3) == { - "my_key": "key prepared", - "market": "DE", - } - # get state after first node - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "key prepared", "market": "DE"}, - tasks=(PregelTask(AnyStr(), "tool_two_slow"),), - next=("tool_two_slow",), - config=(await tool_two.checkpointer.aget_tuple(thread3)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 1, - "writes": {"prepare": {"my_key": " prepared"}}, - }, - parent_config=uconfig, - ) - # resume, for same result as above - assert await tool_two.ainvoke(None, thread3, debug=1) == { - "my_key": "key prepared slow finished", - "market": "DE", - } - assert await tool_two.aget_state(thread3) == StateSnapshot( - values={"my_key": "key prepared slow finished", "market": "DE"}, - tasks=(), - next=(), - config=(await tool_two.checkpointer.aget_tuple(thread3)).config, - created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint["ts"], - metadata={ - "parents": {}, - "source": "loop", - "step": 3, - "writes": {"finish": {"my_key": " finished"}}, - }, - parent_config=[c async for c in tool_two.checkpointer.alist(thread3, limit=2)][ - -1 - ].config, - ) + thread3 = {"configurable": {"thread_id": "23"}} + # update an empty thread before first run + uconfig = await tool_two.aupdate_state( + thread3, {"my_key": "key", "market": "DE"} + ) + # check current state + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "key", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "prepare"),), + next=("prepare",), + config=uconfig, + created_at=AnyStr(), + metadata={ + "parents": {}, + "source": "update", + "step": 0, + "writes": {START: {"my_key": "key", "market": "DE"}}, + }, + parent_config=None, + ) + # run from this point + assert await tool_two.ainvoke(None, thread3) == { + "my_key": "key prepared", + "market": "DE", + } + # get state after first node + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "key prepared", "market": "DE"}, + tasks=(PregelTask(AnyStr(), "tool_two_slow"),), + next=("tool_two_slow",), + config=(await tool_two.checkpointer.aget_tuple(thread3)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 1, + "writes": {"prepare": {"my_key": " prepared"}}, + }, + parent_config=uconfig, + ) + # resume, for same result as above + assert await tool_two.ainvoke(None, thread3, debug=1) == { + "my_key": "key prepared slow finished", + "market": "DE", + } + assert await tool_two.aget_state(thread3) == StateSnapshot( + values={"my_key": "key prepared slow finished", "market": "DE"}, + tasks=(), + next=(), + config=(await tool_two.checkpointer.aget_tuple(thread3)).config, + created_at=(await tool_two.checkpointer.aget_tuple(thread3)).checkpoint[ + "ts" + ], + metadata={ + "parents": {}, + "source": "loop", + "step": 3, + "writes": {"finish": {"my_key": " finished"}}, + }, + parent_config=[ + c async for c in tool_two.checkpointer.alist(thread3, limit=2) + ][-1].config, + ) -async def test_in_one_fan_out_state_graph_waiting_edge() -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_in_one_fan_out_state_graph_waiting_edge(checkpointer_name: str) -> None: def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] ) -> list[str]: @@ -6023,31 +6096,33 @@ async def test_in_one_fan_out_state_graph_waiting_edge() -> None: {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["retriever_one"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"query": "what is weather in sf"}, config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["retriever_one"], ) - ] == [ - {"rewrite_query": {"query": "query: what is weather in sf"}}, - {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, - {"retriever_two": {"docs": ["doc3", "doc4"]}}, - {"retriever_one": {"docs": ["doc1", "doc2"]}}, - ] + config = {"configurable": {"thread_id": "1"}} - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - ] + assert [ + c + async for c in app_w_interrupt.astream( + {"query": "what is weather in sf"}, config + ) + ] == [ + {"rewrite_query": {"query": "query: what is weather in sf"}}, + {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, + {"retriever_two": {"docs": ["doc3", "doc4"]}}, + {"retriever_one": {"docs": ["doc1", "doc2"]}}, + ] + + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + ] +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_in_one_fan_out_state_graph_waiting_edge_via_branch( - snapshot: SnapshotAssertion, + snapshot: SnapshotAssertion, checkpointer_name: str ) -> None: def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] @@ -6114,31 +6189,33 @@ async def test_in_one_fan_out_state_graph_waiting_edge_via_branch( {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["retriever_one"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"query": "what is weather in sf"}, config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["retriever_one"], ) - ] == [ - {"rewrite_query": {"query": "query: what is weather in sf"}}, - {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, - {"retriever_two": {"docs": ["doc3", "doc4"]}}, - {"retriever_one": {"docs": ["doc1", "doc2"]}}, - ] + config = {"configurable": {"thread_id": "1"}} - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - ] + assert [ + c + async for c in app_w_interrupt.astream( + {"query": "what is weather in sf"}, config + ) + ] == [ + {"rewrite_query": {"query": "query: what is weather in sf"}}, + {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, + {"retriever_two": {"docs": ["doc3", "doc4"]}}, + {"retriever_one": {"docs": ["doc1", "doc2"]}}, + ] + + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + ] +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class( - snapshot: SnapshotAssertion, mocker: MockerFixture + snapshot: SnapshotAssertion, mocker: MockerFixture, checkpointer_name: str ) -> None: from langchain_core.pydantic_v1 import BaseModel, ValidationError @@ -6256,75 +6333,77 @@ async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class( {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["retriever_one"], - ) - config = {"configurable": {"thread_id": "1"}} + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["retriever_one"], + ) + config = {"configurable": {"thread_id": "1"}} - async with assert_ctx_once(): - assert [ - c - async for c in app_w_interrupt.astream( - {"query": "what is weather in sf"}, config - ) - ] == [ - {"rewrite_query": {"query": "query: what is weather in sf"}}, - {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, - {"retriever_two": {"docs": ["doc3", "doc4"]}}, - {"retriever_one": {"docs": ["doc1", "doc2"]}}, - ] + async with assert_ctx_once(): + assert [ + c + async for c in app_w_interrupt.astream( + {"query": "what is weather in sf"}, config + ) + ] == [ + {"rewrite_query": {"query": "query: what is weather in sf"}}, + {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, + {"retriever_two": {"docs": ["doc3", "doc4"]}}, + {"retriever_one": {"docs": ["doc1", "doc2"]}}, + ] - async with assert_ctx_once(): - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - ] + async with assert_ctx_once(): + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + ] - assert await app_w_interrupt.aget_state(config) == StateSnapshot( - values={ - "query": "analyzed: query: what is weather in sf", - "answer": "doc1,doc2,doc3,doc4", - "docs": ["doc1", "doc2", "doc3", "doc4"], - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), + assert await app_w_interrupt.aget_state(config) == StateSnapshot( + values={ + "query": "analyzed: query: what is weather in sf", + "answer": "doc1,doc2,doc3,doc4", + "docs": ["doc1", "doc2", "doc3", "doc4"], + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + "step": 4, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + + async with assert_ctx_once(): + assert await app_w_interrupt.aupdate_state( + config, {"docs": ["doc5"]}, as_node="rewrite_query" + ) == { + "configurable": { + "thread_id": "1", + "checkpoint_id": AnyStr(), + "checkpoint_ns": "", + } } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - "step": 4, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - - async with assert_ctx_once(): - assert await app_w_interrupt.aupdate_state( - config, {"docs": ["doc5"]}, as_node="rewrite_query" - ) == { - "configurable": { - "thread_id": "1", - "checkpoint_id": AnyStr(), - "checkpoint_ns": "", - } - } +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydantic2( - snapshot: SnapshotAssertion, + snapshot: SnapshotAssertion, checkpointer_name: str ) -> None: from pydantic import BaseModel, ValidationError @@ -6419,40 +6498,44 @@ async def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class_pydant {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["retriever_one"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"query": "what is weather in sf", "inner": {"yo": 1}}, config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["retriever_one"], ) - ] == [ - {"rewrite_query": {"query": "query: what is weather in sf"}}, - {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, - {"retriever_two": {"docs": ["doc3", "doc4"]}}, - {"retriever_one": {"docs": ["doc1", "doc2"]}}, - ] + config = {"configurable": {"thread_id": "1"}} - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - ] + assert [ + c + async for c in app_w_interrupt.astream( + {"query": "what is weather in sf", "inner": {"yo": 1}}, config + ) + ] == [ + {"rewrite_query": {"query": "query: what is weather in sf"}}, + {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, + {"retriever_two": {"docs": ["doc3", "doc4"]}}, + {"retriever_one": {"docs": ["doc1", "doc2"]}}, + ] - assert await app_w_interrupt.aupdate_state( - config, {"docs": ["doc5"]}, as_node="rewrite_query" - ) == { - "configurable": { - "thread_id": "1", - "checkpoint_id": AnyStr(), - "checkpoint_ns": "", + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + ] + + assert await app_w_interrupt.aupdate_state( + config, {"docs": ["doc5"]}, as_node="rewrite_query" + ) == { + "configurable": { + "thread_id": "1", + "checkpoint_id": AnyStr(), + "checkpoint_ns": "", + } } - } -async def test_in_one_fan_out_state_graph_waiting_edge_plus_regular() -> None: +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_in_one_fan_out_state_graph_waiting_edge_plus_regular( + checkpointer_name: str, +) -> None: def sorted_add( x: list[str], y: Union[list[str], list[tuple[str, str]]] ) -> list[str]: @@ -6520,28 +6603,29 @@ async def test_in_one_fan_out_state_graph_waiting_edge_plus_regular() -> None: {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] - app_w_interrupt = workflow.compile( - checkpointer=MemorySaverAssertImmutable(), - interrupt_after=["retriever_one"], - ) - config = {"configurable": {"thread_id": "1"}} - - assert [ - c - async for c in app_w_interrupt.astream( - {"query": "what is weather in sf"}, config + async with awith_checkpointer(checkpointer_name) as checkpointer: + app_w_interrupt = workflow.compile( + checkpointer=checkpointer, + interrupt_after=["retriever_one"], ) - ] == [ - {"rewrite_query": {"query": "query: what is weather in sf"}}, - {"qa": {"answer": ""}}, - {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, - {"retriever_two": {"docs": ["doc3", "doc4"]}}, - {"retriever_one": {"docs": ["doc1", "doc2"]}}, - ] + config = {"configurable": {"thread_id": "1"}} - assert [c async for c in app_w_interrupt.astream(None, config)] == [ - {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, - ] + assert [ + c + async for c in app_w_interrupt.astream( + {"query": "what is weather in sf"}, config + ) + ] == [ + {"rewrite_query": {"query": "query: what is weather in sf"}}, + {"qa": {"answer": ""}}, + {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, + {"retriever_two": {"docs": ["doc3", "doc4"]}}, + {"retriever_one": {"docs": ["doc1", "doc2"]}}, + ] + + assert [c async for c in app_w_interrupt.astream(None, config)] == [ + {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, + ] async def test_in_one_fan_out_state_graph_waiting_edge_multiple() -> None: @@ -6839,11 +6923,7 @@ async def test_nested_graph(snapshot: SnapshotAssertion) -> None: @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_nested_graph_interrupts_parallel( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") - +async def test_nested_graph_interrupts_parallel(checkpointer_name: str) -> None: class InnerState(TypedDict): my_key: Annotated[str, operator.add] my_other_key: str @@ -6887,89 +6967,91 @@ async def test_nested_graph_interrupts_parallel( graph.add_edge(["inner", "outer_1"], "outer_2") graph.set_finish_point("outer_2") - app = graph.compile(checkpointer=checkpointer) + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = graph.compile(checkpointer=checkpointer) - # test invoke w/ nested interrupt - config = {"configurable": {"thread_id": "1"}} - assert await app.ainvoke({"my_key": ""}, config, debug=True) == { - "my_key": "", - } + # test invoke w/ nested interrupt + config = {"configurable": {"thread_id": "1"}} + assert await app.ainvoke({"my_key": ""}, config, debug=True) == { + "my_key": "", + } - assert await app.ainvoke(None, config, debug=True) == { - "my_key": "got here and there and parallel and back again", - } + assert await app.ainvoke(None, config, debug=True) == { + "my_key": "got here and there and parallel and back again", + } - # below combo of assertions is asserting two things - # - outer_1 finishes before inner interrupts (because we see its output in stream, which only happens after node finishes) - # - 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 [c async for c in app.astream({"my_key": ""}, config, subgraphs=True)] == [ - # we got to parallel node first - ((), {"outer_1": {"my_key": " and parallel"}}), - ((AnyStr("inner:"),), {"inner_1": {"my_key": "got here", "my_other_key": ""}}), - ] - assert [c async for c in app.astream(None, config)] == [ - {"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}}, - {"inner": {"my_key": "got here and there"}}, - {"outer_2": {"my_key": " and back again"}}, - ] + # below combo of assertions is asserting two things + # - outer_1 finishes before inner interrupts (because we see its output in stream, which only happens after node finishes) + # - 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 [ + c async for c in app.astream({"my_key": ""}, config, subgraphs=True) + ] == [ + # we got to parallel node first + ((), {"outer_1": {"my_key": " and parallel"}}), + ( + (AnyStr("inner:"),), + {"inner_1": {"my_key": "got here", "my_other_key": ""}}, + ), + ] + assert [c async for c in app.astream(None, config)] == [ + {"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}}, + {"inner": {"my_key": "got here and there"}}, + {"outer_2": {"my_key": " and back again"}}, + ] - # 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") - ] == [ - {"my_key": ""}, - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - {"my_key": ""}, - {"my_key": "got here and there and parallel"}, - {"my_key": "got here and there and parallel and back again"}, - ] + # 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") + ] == [ + {"my_key": ""}, + ] + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": ""}, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, + ] - # # test interrupts BEFORE the parallel node - 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") - ] == [ - {"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")] == [ - {"my_key": ""}, - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - {"my_key": ""}, - {"my_key": "got here and there and parallel"}, - {"my_key": "got here and there and parallel and back again"}, - ] + # # test interrupts BEFORE the parallel node + 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") + ] == [ + {"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")] == [ + {"my_key": ""}, + ] + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": ""}, + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, + ] - # test interrupts AFTER the parallel node - 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") - ] == [ - {"my_key": ""}, - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - {"my_key": ""}, - {"my_key": "got here and there and parallel"}, - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - {"my_key": "got here and there and parallel"}, - {"my_key": "got here and there and parallel and back again"}, - ] + # test interrupts AFTER the parallel node + 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") + ] == [ + {"my_key": ""}, + ] + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": ""}, + {"my_key": "got here and there and parallel"}, + ] + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": "got here and there and parallel"}, + {"my_key": "got here and there and parallel and back again"}, + ] @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_doubly_nested_graph_interrupts( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue(f"checkpointer_{checkpointer_name}") - +async def test_doubly_nested_graph_interrupts(checkpointer_name: str) -> None: class State(TypedDict): my_key: str @@ -7017,50 +7099,49 @@ async def test_doubly_nested_graph_interrupts( graph.add_edge("child", "parent_2") graph.set_finish_point("parent_2") - app = graph.compile(checkpointer=checkpointer) + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = graph.compile(checkpointer=checkpointer) - # test invoke w/ nested interrupt - config = {"configurable": {"thread_id": "1"}} - assert await app.ainvoke({"my_key": "my value"}, config, debug=True) == { - "my_key": "hi my value", - } + # test invoke w/ nested interrupt + config = {"configurable": {"thread_id": "1"}} + assert await app.ainvoke({"my_key": "my value"}, config, debug=True) == { + "my_key": "hi my value", + } - assert await app.ainvoke(None, config, debug=True) == { - "my_key": "hi my value here and there and back again", - } + assert await app.ainvoke(None, config, debug=True) == { + "my_key": "hi my value here and there and back again", + } - # test stream updates w/ nested interrupt - config = {"configurable": {"thread_id": "2"}} - assert [c async for c in app.astream({"my_key": "my value"}, config)] == [ - {"parent_1": {"my_key": "hi my value"}}, - ] - assert [c async for c in app.astream(None, config)] == [ - {"child": {"my_key": "hi my value here and there"}}, - {"parent_2": {"my_key": "hi my value here and there and back again"}}, - ] + # test stream updates w/ nested interrupt + config = {"configurable": {"thread_id": "2"}} + assert [c async for c in app.astream({"my_key": "my value"}, config)] == [ + {"parent_1": {"my_key": "hi my value"}}, + ] + assert [c async for c in app.astream(None, config)] == [ + {"child": {"my_key": "hi my value here and there"}}, + {"parent_2": {"my_key": "hi my value here and there and back again"}}, + ] - # test stream values w/ nested interrupt - config = {"configurable": {"thread_id": "3"}} - assert [ - c - async for c in app.astream({"my_key": "my value"}, config, stream_mode="values") - ] == [ - {"my_key": "my value"}, - {"my_key": "hi my value"}, - ] - assert [c async for c in app.astream(None, config, stream_mode="values")] == [ - {"my_key": "hi my value"}, - {"my_key": "hi my value here and there"}, - {"my_key": "hi my value here and there and back again"}, - ] + # test stream values w/ nested interrupt + config = {"configurable": {"thread_id": "3"}} + assert [ + c + async for c in app.astream( + {"my_key": "my value"}, config, stream_mode="values" + ) + ] == [ + {"my_key": "my value"}, + {"my_key": "hi my value"}, + ] + assert [c async for c in app.astream(None, config, stream_mode="values")] == [ + {"my_key": "hi my value"}, + {"my_key": "hi my value here and there"}, + {"my_key": "hi my value here and there and back again"}, + ] @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_nested_graph_state( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - +async def test_nested_graph_state(checkpointer_name: str) -> None: class InnerState(TypedDict): my_key: str my_other_key: str @@ -7106,388 +7187,14 @@ async def test_nested_graph_state( graph.add_edge("inner", "outer_2") graph.set_finish_point("outer_2") - app = graph.compile(checkpointer=checkpointer) + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = graph.compile(checkpointer=checkpointer) - config = {"configurable": {"thread_id": "1"}} - await app.ainvoke({"my_key": "my value"}, config, debug=True) - # test state w/ nested subgraph state (right after interrupt) - # first get_state without subgraph state - assert await app.aget_state(config) == StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state={"configurable": {"thread_id": "1", "checkpoint_ns": AnyStr()}}, - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - # now, get_state with subgraphs state - assert await app.aget_state(config, subgraphs=True) == StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state=StateSnapshot( - values={ - "my_key": "hi my value here", - "my_other_key": "hi my value", - }, - tasks=( - PregelTask( - AnyStr(), - name="inner_2", - error=None, - ), - ), - next=("inner_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("inner:"), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - {"": AnyStr(), AnyStr("child:"): AnyStr()} - ), - } - }, - metadata={ - "parents": { - "": AnyStr(), - }, - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("inner:"), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - # get_state_history returns outer graph checkpoints - history = [c async for c in app.aget_state_history(config)] - assert history == [ - StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "inner", - state={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("inner:"), - } - }, - ), - ), - next=("inner",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"outer_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": None, - "step": 0, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "input", - "writes": {"__start__": {"my_key": "my value"}}, - "step": -1, - }, - created_at=AnyStr(), - parent_config=None, - ), - ] - # get_state_history for a subgraph returns its checkpoints - child_history = [c async for c in app.aget_state_history(history[0].tasks[0].state)] - assert child_history == [ - StateSnapshot( - values={"my_key": "hi my value here", "my_other_key": "hi my value"}, - next=("inner_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("inner:"), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - {"": AnyStr(), AnyStr("inner:"): AnyStr()} - ), - } - }, - metadata={ - "source": "loop", - "writes": { - "inner_1": { - "my_key": "hi my value here", - "my_other_key": "hi my value", - } - }, - "step": 1, - "parents": {"": AnyStr()}, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("inner:"), - "checkpoint_id": AnyStr(), - } - }, - tasks=(PregelTask(id=AnyStr(), name="inner_2"),), - ), - StateSnapshot( - values={"my_key": "hi my value"}, - next=("inner_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("inner:"), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - {"": AnyStr(), AnyStr("inner:"): AnyStr()} - ), - } - }, - metadata={ - "source": "loop", - "writes": None, - "step": 0, - "parents": {"": AnyStr()}, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("inner:"), - "checkpoint_id": AnyStr(), - } - }, - tasks=(PregelTask(id=AnyStr(), name="inner_1"),), - ), - StateSnapshot( - values={}, - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("inner:"), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - {"": AnyStr(), AnyStr("inner:"): AnyStr()} - ), - } - }, - metadata={ - "source": "input", - "writes": {"__start__": {"my_key": "hi my value"}}, - "step": -1, - "parents": {"": AnyStr()}, - }, - created_at=AnyStr(), - parent_config=None, - tasks=(PregelTask(id=AnyStr(), name="__start__"),), - ), - ] - - # resume - await app.ainvoke(None, config, debug=True) - # test state w/ nested subgraph state (after resuming from interrupt) - assert await app.aget_state(config) == StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - # test full history at the end - actual_history = [c async for c in app.aget_state_history(config)] - expected_history = [ - StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "outer_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "hi my value here and there"}, - tasks=(PregelTask(AnyStr(), "outer_2"),), - next=("outer_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"inner": {"my_key": "hi my value here and there"}}, - "step": 2, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( + config = {"configurable": {"thread_id": "1"}} + await app.ainvoke({"my_key": "my value"}, config, debug=True) + # test state w/ nested subgraph state (right after interrupt) + # first get_state without subgraph state + assert await app.aget_state(config) == StateSnapshot( values={"my_key": "hi my value"}, tasks=( PregelTask( @@ -7520,11 +7227,62 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - ), - StateSnapshot( - values={"my_key": "my value"}, - tasks=(PregelTask(AnyStr(), "outer_1"),), - next=("outer_1",), + ) + # now, get_state with subgraphs state + assert await app.aget_state(config, subgraphs=True) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state=StateSnapshot( + values={ + "my_key": "hi my value here", + "my_other_key": "hi my value", + }, + tasks=( + PregelTask( + AnyStr(), + name="inner_2", + error=None, + ), + ), + next=("inner_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "parents": { + "": AnyStr(), + }, + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), + next=("inner",), config={ "configurable": { "thread_id": "1", @@ -7535,8 +7293,8 @@ async def test_nested_graph_state( metadata={ "parents": {}, "source": "loop", - "writes": None, - "step": 0, + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, }, created_at=AnyStr(), parent_config={ @@ -7546,11 +7304,194 @@ async def test_nested_graph_state( "checkpoint_id": AnyStr(), } }, - ), - StateSnapshot( - values={}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), + ) + # get_state_history returns outer graph checkpoints + history = [c async for c in app.aget_state_history(config)] + assert history == [ + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + } + }, + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + tasks=(PregelTask(AnyStr(), "outer_1"),), + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={}, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"__start__": {"my_key": "my value"}}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] + # get_state_history for a subgraph returns its checkpoints + child_history = [ + c async for c in app.aget_state_history(history[0].tasks[0].state) + ] + assert child_history == [ + StateSnapshot( + values={"my_key": "hi my value here", "my_other_key": "hi my value"}, + next=("inner_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("inner:"): AnyStr()} + ), + } + }, + metadata={ + "source": "loop", + "writes": { + "inner_1": { + "my_key": "hi my value here", + "my_other_key": "hi my value", + } + }, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="inner_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("inner_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("inner:"): AnyStr()} + ), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="inner_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("inner:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("inner:"): AnyStr()} + ), + } + }, + metadata={ + "source": "input", + "writes": {"__start__": {"my_key": "hi my value"}}, + "step": -1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] + + # resume + await app.ainvoke(None, config, debug=True) + # test state w/ nested subgraph state (after resuming from interrupt) + assert await app.aget_state(config) == StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), config={ "configurable": { "thread_id": "1", @@ -7560,26 +7501,172 @@ async def test_nested_graph_state( }, metadata={ "parents": {}, - "source": "input", - "writes": {"__start__": {"my_key": "my value"}}, - "step": -1, + "source": "loop", + "writes": { + "outer_2": {"my_key": "hi my value here and there and back again"} + }, + "step": 3, }, created_at=AnyStr(), - parent_config=None, - ), - ] - assert actual_history == expected_history - # test looking up parent state by checkpoint ID - for actual_snapshot, expected_snapshot in zip(actual_history, expected_history): - assert await app.aget_state(actual_snapshot.config) == expected_snapshot + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # test full history at the end + actual_history = [c async for c in app.aget_state_history(config)] + expected_history = [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "outer_2": { + "my_key": "hi my value here and there and back again" + } + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + tasks=(PregelTask(AnyStr(), "outer_2"),), + next=("outer_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"inner": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "inner", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + } + }, + ), + ), + next=("inner",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"outer_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + tasks=(PregelTask(AnyStr(), "outer_1"),), + next=("outer_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={}, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"__start__": {"my_key": "my value"}}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] + assert actual_history == expected_history + # test looking up parent state by checkpoint ID + for actual_snapshot, expected_snapshot in zip(actual_history, expected_history): + assert await app.aget_state(actual_snapshot.config) == expected_snapshot @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_doubly_nested_graph_state( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - +async def test_doubly_nested_graph_state(checkpointer_name: str) -> None: class State(TypedDict): my_key: str @@ -7627,277 +7714,37 @@ async def test_doubly_nested_graph_state( graph.add_edge("child", "parent_2") graph.set_finish_point("parent_2") - app = graph.compile(checkpointer=checkpointer) + async with awith_checkpointer(checkpointer_name) as checkpointer: + app = graph.compile(checkpointer=checkpointer) - # test invoke w/ nested interrupt - config = {"configurable": {"thread_id": "1"}} - assert [ - c async for c in app.astream({"my_key": "my value"}, config, subgraphs=True) - ] == [ - ((), {"parent_1": {"my_key": "hi my value"}}), - ( - (AnyStr("child:"), AnyStr("child_1:")), - {"grandchild_1": {"my_key": "hi my value here"}}, - ), - ] - # get state without subgraphs - outer_state = await app.aget_state(config) - assert outer_state == StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "child", - state={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child"), - } - }, + # test invoke w/ nested interrupt + config = {"configurable": {"thread_id": "1"}} + assert [ + c async for c in app.astream({"my_key": "my value"}, config, subgraphs=True) + ] == [ + ((), {"parent_1": {"my_key": "hi my value"}}), + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_1": {"my_key": "hi my value here"}}, ), - ), - next=("child",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"parent_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - child_state = await app.aget_state(outer_state.tasks[0].state) - assert ( - child_state.tasks[0] - == StateSnapshot( + ] + # get state without subgraphs + outer_state = await app.aget_state(config) + assert outer_state == StateSnapshot( values={"my_key": "hi my value"}, tasks=( PregelTask( AnyStr(), - "child_1", + "child", state={ "configurable": { "thread_id": "1", - "checkpoint_ns": AnyStr(), + "checkpoint_ns": AnyStr("child"), } }, ), ), - next=("child_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child:"), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - {"": AnyStr(), AnyStr("child:"): AnyStr()} - ), - } - }, - metadata={ - "parents": {"": AnyStr()}, - "source": "loop", - "writes": None, - "step": 0, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child:"), - "checkpoint_id": AnyStr(), - } - }, - ).tasks[0] - ) - grandchild_state = await app.aget_state(child_state.tasks[0].state) - assert grandchild_state == StateSnapshot( - values={"my_key": "hi my value here"}, - tasks=( - PregelTask( - AnyStr(), - "grandchild_2", - ), - ), - next=("grandchild_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - { - "": AnyStr(), - AnyStr("child:"): AnyStr(), - AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), - } - ), - } - }, - metadata={ - "parents": AnyDict( - { - "": AnyStr(), - AnyStr("child:"): AnyStr(), - } - ), - "source": "loop", - "writes": {"grandchild_1": {"my_key": "hi my value here"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ) - # get state with subgraphs - assert await app.aget_state(config, subgraphs=True) == StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "child", - state=StateSnapshot( - values={"my_key": "hi my value"}, - tasks=( - PregelTask( - AnyStr(), - "child_1", - state=StateSnapshot( - values={"my_key": "hi my value here"}, - tasks=( - PregelTask( - AnyStr(), - "grandchild_2", - ), - ), - next=("grandchild_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - { - "": AnyStr(), - AnyStr("child:"): AnyStr(), - AnyStr( - re.compile(r"child:.+|child1:") - ): AnyStr(), - } - ), - } - }, - metadata={ - "parents": AnyDict( - { - "": AnyStr(), - AnyStr("child:"): AnyStr(), - } - ), - "source": "loop", - "writes": { - "grandchild_1": {"my_key": "hi my value here"} - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("child_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child:"), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - {"": AnyStr(), AnyStr("child:"): AnyStr()} - ), - } - }, - metadata={ - "parents": {"": AnyStr()}, - "source": "loop", - "writes": None, - "step": 0, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child:"), - "checkpoint_id": AnyStr(), - } - }, - ), - ), - ), - next=("child",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"parent_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - # resume - assert [c async for c in app.astream(None, config, subgraphs=True)] == [ - ( - (AnyStr("child:"), AnyStr("child_1:")), - {"grandchild_2": {"my_key": "hi my value here and there"}}, - ), - ((AnyStr("child:"),), {"child_1": {"my_key": "hi my value here and there"}}), - ((), {"child": {"my_key": "hi my value here and there"}}), - ((), {"parent_2": {"my_key": "hi my value here and there and back again"}}), - ] - # get state with and without subgraphs - assert ( - await app.aget_state(config) - == await app.aget_state(config, subgraphs=True) - == StateSnapshot( - values={"my_key": "hi my value here and there and back again"}, - tasks=(), - next=(), + next=("child",), config={ "configurable": { "thread_id": "1", @@ -7908,10 +7755,8 @@ async def test_doubly_nested_graph_state( metadata={ "parents": {}, "source": "loop", - "writes": { - "parent_2": {"my_key": "hi my value here and there and back again"} - }, - "step": 3, + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, }, created_at=AnyStr(), parent_config={ @@ -7922,13 +7767,227 @@ async def test_doubly_nested_graph_state( } }, ) - ) - # get outer graph history - outer_history = [c async for c in app.aget_state_history(config)] - assert ( - outer_history[0] - == [ - StateSnapshot( + child_state = await app.aget_state(outer_state.tasks[0].state) + assert ( + child_state.tasks[0] + == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child_1", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + } + }, + ), + ), + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "parents": {"": AnyStr()}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + ).tasks[0] + ) + grandchild_state = await app.aget_state(child_state.tasks[0].state) + assert grandchild_state == StateSnapshot( + values={"my_key": "hi my value here"}, + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + ), + ), + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ) + # get state with subgraphs + assert await app.aget_state(config, subgraphs=True) == StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state=StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child_1", + state=StateSnapshot( + values={"my_key": "hi my value here"}, + tasks=( + PregelTask( + AnyStr(), + "grandchild_2", + ), + ), + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr( + re.compile(r"child:.+|child1:") + ): AnyStr(), + } + ), + } + }, + metadata={ + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + "source": "loop", + "writes": { + "grandchild_1": { + "my_key": "hi my value here" + } + }, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "parents": {"": AnyStr()}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, + ), + ), + ), + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) + # resume + assert [c async for c in app.astream(None, config, subgraphs=True)] == [ + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_2": {"my_key": "hi my value here and there"}}, + ), + ( + (AnyStr("child:"),), + {"child_1": {"my_key": "hi my value here and there"}}, + ), + ((), {"child": {"my_key": "hi my value here and there"}}), + ((), {"parent_2": {"my_key": "hi my value here and there and back again"}}), + ] + # get state with and without subgraphs + assert ( + await app.aget_state(config) + == await app.aget_state(config, subgraphs=True) + == StateSnapshot( values={"my_key": "hi my value here and there and back again"}, tasks=(), next=(), @@ -7957,95 +8016,227 @@ async def test_doubly_nested_graph_state( "checkpoint_id": AnyStr(), } }, - ), + ) + ) + # get outer graph history + outer_history = [c async for c in app.aget_state_history(config)] + assert ( + outer_history[0] + == [ + StateSnapshot( + values={"my_key": "hi my value here and there and back again"}, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "parent_2": { + "my_key": "hi my value here and there and back again" + } + }, + "step": 3, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=("parent_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"child": {"my_key": "hi my value here and there"}}, + "step": 2, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + tasks=( + PregelTask( + AnyStr(), + "child", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child"), + } + }, + ), + ), + next=("child",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": {"parent_1": {"my_key": "hi my value"}}, + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"my_key": "my value"}, + next=("parent_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": None, + "step": 0, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="parent_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"my_key": "my value"}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ][0] + ) + # get child graph history + child_history = [ + c async for c in app.aget_state_history(outer_history[2].tasks[0].state) + ] + assert child_history == [ StateSnapshot( values={"my_key": "hi my value here and there"}, - next=("parent_2",), + next=(), config={ "configurable": { "thread_id": "1", - "checkpoint_ns": "", + "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ - "parents": {}, "source": "loop", - "writes": {"child": {"my_key": "hi my value here and there"}}, - "step": 2, + "writes": {"child_1": {"my_key": "hi my value here and there"}}, + "step": 1, + "parents": {"": AnyStr()}, }, created_at=AnyStr(), parent_config={ "configurable": { "thread_id": "1", - "checkpoint_ns": "", + "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), } }, - tasks=(PregelTask(id=AnyStr(), name="parent_2"),), + tasks=(), ), StateSnapshot( values={"my_key": "hi my value"}, + next=("child_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("child:"), + "checkpoint_id": AnyStr(), + } + }, tasks=( PregelTask( - AnyStr(), - "child", + id=AnyStr(), + name="child_1", state={ "configurable": { "thread_id": "1", - "checkpoint_ns": AnyStr("child"), + "checkpoint_ns": AnyStr("child:"), } }, ), ), - next=("child",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": {"parent_1": {"my_key": "hi my value"}}, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"my_key": "my value"}, - next=("parent_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": None, - "step": 0, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - tasks=(PregelTask(id=AnyStr(), name="parent_1"),), ), StateSnapshot( values={}, @@ -8053,290 +8244,195 @@ async def test_doubly_nested_graph_state( config={ "configurable": { "thread_id": "1", - "checkpoint_ns": "", + "checkpoint_ns": AnyStr("child:"), "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + {"": AnyStr(), AnyStr("child:"): AnyStr()} + ), } }, metadata={ - "parents": {}, "source": "input", - "writes": {"my_key": "my value"}, + "writes": {"__start__": {"my_key": "hi my value"}}, "step": -1, + "parents": {"": AnyStr()}, }, created_at=AnyStr(), parent_config=None, tasks=(PregelTask(id=AnyStr(), name="__start__"),), ), - ][0] - ) - # get child graph history - child_history = [ - c async for c in app.aget_state_history(outer_history[2].tasks[0].state) - ] - assert child_history == [ - StateSnapshot( - values={"my_key": "hi my value here and there"}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child:"), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - {"": AnyStr(), AnyStr("child:"): AnyStr()} - ), - } - }, - metadata={ - "source": "loop", - "writes": {"child_1": {"my_key": "hi my value here and there"}}, - "step": 1, - "parents": {"": AnyStr()}, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child:"), - "checkpoint_id": AnyStr(), - } - }, - tasks=(), - ), - StateSnapshot( - values={"my_key": "hi my value"}, - next=("child_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child:"), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - {"": AnyStr(), AnyStr("child:"): AnyStr()} - ), - } - }, - metadata={ - "source": "loop", - "writes": None, - "step": 0, - "parents": {"": AnyStr()}, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child:"), - "checkpoint_id": AnyStr(), - } - }, - tasks=( - PregelTask( - id=AnyStr(), - name="child_1", - state={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child:"), - } + ] + # get grandchild graph history + grandchild_history = [ + c async for c in app.aget_state_history(child_history[1].tasks[0].state) + ] + assert grandchild_history == [ + StateSnapshot( + values={"my_key": "hi my value here and there"}, + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": { + "grandchild_2": {"my_key": "hi my value here and there"} }, - ), + "step": 2, + "parents": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + } + ), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), ), - ), - StateSnapshot( - values={}, - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("child:"), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - {"": AnyStr(), AnyStr("child:"): AnyStr()} - ), - } - }, - metadata={ - "source": "input", - "writes": {"__start__": {"my_key": "hi my value"}}, - "step": -1, - "parents": {"": AnyStr()}, - }, - created_at=AnyStr(), - parent_config=None, - tasks=(PregelTask(id=AnyStr(), name="__start__"),), - ), - ] - # get grandchild graph history - grandchild_history = [ - c async for c in app.aget_state_history(child_history[1].tasks[0].state) - ] - assert grandchild_history == [ - StateSnapshot( - values={"my_key": "hi my value here and there"}, - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( + StateSnapshot( + values={"my_key": "hi my value here"}, + next=("grandchild_2",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": {"grandchild_1": {"my_key": "hi my value here"}}, + "step": 1, + "parents": AnyDict( { "": AnyStr(), AnyStr("child:"): AnyStr(), - AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), } ), - } - }, - metadata={ - "source": "loop", - "writes": {"grandchild_2": {"my_key": "hi my value here and there"}}, - "step": 2, - "parents": AnyDict( - { - "": AnyStr(), - AnyStr("child:"): AnyStr(), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), } - ), - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - tasks=(), - ), - StateSnapshot( - values={"my_key": "hi my value here"}, - next=("grandchild_2",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( + }, + tasks=(PregelTask(id=AnyStr(), name="grandchild_2"),), + ), + StateSnapshot( + values={"my_key": "hi my value"}, + next=("grandchild_1",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": None, + "step": 0, + "parents": AnyDict( { "": AnyStr(), AnyStr("child:"): AnyStr(), - AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), } ), - } - }, - metadata={ - "source": "loop", - "writes": {"grandchild_1": {"my_key": "hi my value here"}}, - "step": 1, - "parents": AnyDict( - { - "": AnyStr(), - AnyStr("child:"): AnyStr(), + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), } - ), - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - tasks=(PregelTask(id=AnyStr(), name="grandchild_2"),), - ), - StateSnapshot( - values={"my_key": "hi my value"}, - next=("grandchild_1",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( + }, + tasks=(PregelTask(id=AnyStr(), name="grandchild_1"),), + ), + StateSnapshot( + values={}, + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr(), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("child:"): AnyStr(), + AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), + } + ), + } + }, + metadata={ + "source": "input", + "writes": {"__start__": {"my_key": "hi my value"}}, + "step": -1, + "parents": AnyDict( { "": AnyStr(), AnyStr("child:"): AnyStr(), - AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), } ), - } - }, - metadata={ - "source": "loop", - "writes": None, - "step": 0, - "parents": AnyDict( - { - "": AnyStr(), - AnyStr("child:"): AnyStr(), - } - ), - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - } - }, - tasks=(PregelTask(id=AnyStr(), name="grandchild_1"),), - ), - StateSnapshot( - values={}, - next=("__start__",), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr(), - "checkpoint_id": AnyStr(), - "checkpoint_map": AnyDict( - { - "": AnyStr(), - AnyStr("child:"): AnyStr(), - AnyStr(re.compile(r"child:.+|child1:")): AnyStr(), - } - ), - } - }, - metadata={ - "source": "input", - "writes": {"__start__": {"my_key": "hi my value"}}, - "step": -1, - "parents": AnyDict( - { - "": AnyStr(), - AnyStr("child:"): AnyStr(), - } - ), - }, - created_at=AnyStr(), - parent_config=None, - tasks=(PregelTask(id=AnyStr(), name="__start__"),), - ), - ] + }, + created_at=AnyStr(), + parent_config=None, + tasks=(PregelTask(id=AnyStr(), name="__start__"),), + ), + ] - # replay grandchild checkpoint - assert [ - c async for c in app.astream(None, grandchild_history[2].config, subgraphs=True) - ] == [ - ( - (AnyStr("child:"), AnyStr("child_1:")), - {"grandchild_1": {"my_key": "hi my value here"}}, - ) - ] + # replay grandchild checkpoint + assert [ + c + async for c in app.astream( + None, grandchild_history[2].config, subgraphs=True + ) + ] == [ + ( + (AnyStr("child:"), AnyStr("child_1:")), + {"grandchild_1": {"my_key": "hi my value here"}}, + ) + ] @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_send_to_nested_graphs( - request: pytest.FixtureRequest, checkpointer_name: str -) -> None: - checkpointer = request.getfixturevalue("checkpointer_" + checkpointer_name) - +async def test_send_to_nested_graphs(checkpointer_name: str) -> None: class OverallState(TypedDict): subjects: list[str] jokes: Annotated[list[str], operator.add] @@ -8370,112 +8466,74 @@ async def test_send_to_nested_graphs( builder.add_conditional_edges(START, continue_to_jokes) builder.add_edge("generate_joke", END) - graph = builder.compile(checkpointer=checkpointer) - config = {"configurable": {"thread_id": "1"}} + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) + config = {"configurable": {"thread_id": "1"}} - # invoke and pause at nested interrupt - assert await graph.ainvoke({"subjects": ["cats", "dogs"]}, config=config) == { - "subjects": ["cats", "dogs"], - "jokes": [], - } - # check state - outer_state = await graph.aget_state(config) + # invoke and pause at nested interrupt + assert await graph.ainvoke({"subjects": ["cats", "dogs"]}, config=config) == { + "subjects": ["cats", "dogs"], + "jokes": [], + } + # check state + outer_state = await graph.aget_state(config) - assert outer_state == StateSnapshot( - values={"subjects": ["cats", "dogs"], "jokes": []}, - tasks=( - PregelTask( - AnyStr(), - "generate_joke", - state={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("generate_joke:"), - } - }, + assert outer_state == StateSnapshot( + values={"subjects": ["cats", "dogs"], "jokes": []}, + tasks=( + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), ), - PregelTask( - AnyStr(), - "generate_joke", - state={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("generate_joke:"), - } - }, - ), - ), - next=("generate_joke", "generate_joke"), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) + next=("generate_joke", "generate_joke"), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ) - # update state of dogs joke graph - await graph.aupdate_state( - outer_state.tasks[1].state, {"subject": "turtles - hohoho"} - ) + # update state of dogs joke graph + await graph.aupdate_state( + outer_state.tasks[1].state, {"subject": "turtles - hohoho"} + ) - # continue past interrupt - assert await graph.ainvoke(None, config=config) == { - "subjects": ["cats", "dogs"], - "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], - } - - actual_snapshot = await graph.aget_state(config) - expected_snapshot = StateSnapshot( - values={ + # continue past interrupt + assert await graph.ainvoke(None, config=config) == { "subjects": ["cats", "dogs"], "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], - }, - tasks=(), - next=(), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - metadata={ - "parents": {}, - "source": "loop", - "writes": { - "generate_joke": [ - {"jokes": ["Joke about cats - hohoho"]}, - {"jokes": ["Joke about turtles - hohoho"]}, - ] - }, - "step": 1, - }, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ) - assert actual_snapshot == expected_snapshot + } - # test full history - actual_history = [c async for c in graph.aget_state_history(config)] - expected_history = [ - StateSnapshot( + actual_snapshot = await graph.aget_state(config) + expected_snapshot = StateSnapshot( values={ "subjects": ["cats", "dogs"], "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], @@ -8508,53 +8566,247 @@ async def test_send_to_nested_graphs( "checkpoint_id": AnyStr(), } }, - ), - StateSnapshot( - values={"subjects": ["cats", "dogs"], "jokes": []}, - next=("generate_joke", "generate_joke"), - tasks=( - PregelTask( - AnyStr(), - "generate_joke", - state={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("generate_joke:"), - } + ) + assert actual_snapshot == expected_snapshot + + # test full history + actual_history = [c async for c in graph.aget_state_history(config)] + expected_history = [ + StateSnapshot( + values={ + "subjects": ["cats", "dogs"], + "jokes": [ + "Joke about cats - hohoho", + "Joke about turtles - hohoho", + ], + }, + tasks=(), + next=(), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "loop", + "writes": { + "generate_joke": [ + {"jokes": ["Joke about cats - hohoho"]}, + {"jokes": ["Joke about turtles - hohoho"]}, + ] }, - ), - PregelTask( - AnyStr(), - "generate_joke", - state={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": AnyStr("generate_joke:"), - } - }, - ), + "step": 1, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, ), - config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } + StateSnapshot( + values={"subjects": ["cats", "dogs"], "jokes": []}, + next=("generate_joke", "generate_joke"), + tasks=( + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + PregelTask( + AnyStr(), + "generate_joke", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("generate_joke:"), + } + }, + ), + ), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + ), + StateSnapshot( + values={"jokes": []}, + tasks=(PregelTask(AnyStr(), "__start__"),), + next=("__start__",), + config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "parents": {}, + "source": "input", + "writes": {"__start__": {"subjects": ["cats", "dogs"]}}, + "step": -1, + }, + created_at=AnyStr(), + parent_config=None, + ), + ] + assert actual_history == expected_history + + +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_weather_subgraph( + checkpointer_name: str, snapshot: SnapshotAssertion +) -> None: + from langchain_core.language_models.fake_chat_models import ( + FakeMessagesListChatModel, + ) + from langchain_core.messages import AIMessage, HumanMessage, ToolCall + from langchain_core.tools import tool + + from langgraph.graph import MessagesState + + # setup subgraph + + @tool + def get_weather(city: str): + """Get the weather for a specific city""" + return f"I'ts sunny in {city}!" + + weather_model = FakeMessagesListChatModel( + responses=[ + AIMessage( + content="", + tool_calls=[ + ToolCall( + id="tool_call123", + name="get_weather", + args={"city": "San Francisco"}, + ) + ], + ) + ] + ) + + class SubGraphState(MessagesState): + city: str + + def model_node(state: SubGraphState): + result = weather_model.invoke(state["messages"]) + return {"city": cast(AIMessage, result).tool_calls[0]["args"]["city"]} + + def weather_node(state: SubGraphState): + result = get_weather.invoke({"city": state["city"]}) + return {"messages": [{"role": "assistant", "content": result}]} + + subgraph = StateGraph(SubGraphState) + subgraph.add_node(model_node) + subgraph.add_node(weather_node) + subgraph.add_edge(START, "model_node") + subgraph.add_edge("model_node", "weather_node") + subgraph.add_edge("weather_node", END) + subgraph = subgraph.compile(interrupt_before=["weather_node"]) + + # setup main graph + + class RouterState(MessagesState): + route: Literal["weather", "other"] + + class Router(TypedDict): + route: Literal["weather", "other"] + + router_model = FakeMessagesListChatModel( + responses=[ + AIMessage( + content="", + tool_calls=[ + ToolCall( + id="tool_call123", + name="router", + args={"dest": "weather"}, + ) + ], + ) + ] + ) + + def router_node(state: RouterState): + system_message = "Classify the incoming query as either about weather or not." + messages = [{"role": "system", "content": system_message}] + state["messages"] + route = router_model.invoke(messages) + return {"route": cast(AIMessage, route).tool_calls[0]["args"]["dest"]} + + def normal_llm_node(state: RouterState): + return {"messages": [AIMessage("Hello!")]} + + def route_after_prediction(state: RouterState): + if state["route"] == "weather": + return "weather_graph" + else: + return "normal_llm_node" + + async def weather_graph(state: RouterState): + return await subgraph.ainvoke(state) + + graph = StateGraph(RouterState) + graph.add_node(router_node) + graph.add_node(normal_llm_node) + graph.add_node("weather_graph", weather_graph) + graph.add_edge(START, "router_node") + graph.add_conditional_edges("router_node", route_after_prediction) + graph.add_edge("normal_llm_node", END) + graph.add_edge("weather_graph", END) + + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = graph.compile(checkpointer=checkpointer) + + assert graph.get_graph(xray=1).draw_mermaid() == snapshot + + config = {"configurable": {"thread_id": "1"}} + inputs = {"messages": [{"role": "user", "content": "what's the weather in sf"}]} + + # run until interrupt + assert [ + c + async for c in graph.astream( + inputs, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ((), {"router_node": {"route": "weather"}}), + ((AnyStr("weather_graph:"),), {"model_node": {"city": "San Francisco"}}), + ] + + # check current state + state = await graph.aget_state(config) + assert state == StateSnapshot( + values={ + "messages": [ + HumanMessage(content="what's the weather in sf", id=AnyStr()) + ], + "route": "weather", }, - metadata={"parents": {}, "source": "loop", "writes": None, "step": 0}, - created_at=AnyStr(), - parent_config={ - "configurable": { - "thread_id": "1", - "checkpoint_ns": "", - "checkpoint_id": AnyStr(), - } - }, - ), - StateSnapshot( - values={"jokes": []}, - tasks=(PregelTask(AnyStr(), "__start__"),), - next=("__start__",), + next=("weather_graph",), config={ "configurable": { "thread_id": "1", @@ -8563,16 +8815,264 @@ async def test_send_to_nested_graphs( } }, metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, "parents": {}, - "source": "input", - "writes": {"__start__": {"subjects": ["cats", "dogs"]}}, - "step": -1, }, created_at=AnyStr(), - parent_config=None, - ), - ] - assert actual_history == expected_history + parent_config={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state={ + "configurable": { + "thread_id": "1", + "checkpoint_ns": AnyStr("weather_graph:"), + } + }, + ), + ), + ) + + # update + await graph.aupdate_state(state.tasks[0].state, {"city": "la"}) + + # run after update + assert [ + c + async for c in graph.astream( + None, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ( + (AnyStr("weather_graph:"),), + { + "weather_node": { + "messages": [ + {"role": "assistant", "content": "I'ts sunny in la!"} + ] + } + }, + ), + ( + (), + { + "weather_graph": { + "messages": [ + HumanMessage( + content="what's the weather in sf", id=AnyStr() + ), + AIMessage(content="I'ts sunny in la!", id=AnyStr()), + ] + } + }, + ), + ] + + # try updating acting as weather node + config = {"configurable": {"thread_id": "14"}} + inputs = {"messages": [{"role": "user", "content": "what's the weather in sf"}]} + assert [ + c + async for c in graph.astream( + inputs, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ((), {"router_node": {"route": "weather"}}), + ((AnyStr("weather_graph:"),), {"model_node": {"city": "San Francisco"}}), + ] + state = await graph.aget_state(config, subgraphs=True) + assert state == StateSnapshot( + values={ + "messages": [ + HumanMessage(content="what's the weather in sf", id=AnyStr()) + ], + "route": "weather", + }, + next=("weather_graph",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state=StateSnapshot( + values={ + "messages": [ + HumanMessage( + content="what's the weather in sf", id=AnyStr() + ) + ], + "city": "San Francisco", + }, + next=("weather_node",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("weather_graph:"): AnyStr(), + } + ), + } + }, + metadata={ + "source": "loop", + "writes": {"model_node": {"city": "San Francisco"}}, + "step": 1, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(PregelTask(id=AnyStr(), name="weather_node"),), + ), + ), + ), + ) + await graph.aupdate_state( + state.tasks[0].state.config, + {"messages": [{"role": "assistant", "content": "rainy"}]}, + as_node="weather_node", + ) + state = await graph.aget_state(config, subgraphs=True) + assert state == StateSnapshot( + values={ + "messages": [ + HumanMessage(content="what's the weather in sf", id=AnyStr()) + ], + "route": "weather", + }, + next=("weather_graph",), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + metadata={ + "source": "loop", + "writes": {"router_node": {"route": "weather"}}, + "step": 1, + "parents": {}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": "", + "checkpoint_id": AnyStr(), + } + }, + tasks=( + PregelTask( + id=AnyStr(), + name="weather_graph", + state=StateSnapshot( + values={ + "messages": [ + HumanMessage( + content="what's the weather in sf", id=AnyStr() + ), + AIMessage(content="rainy", id=AnyStr()), + ], + "city": "San Francisco", + }, + next=(), + config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + "checkpoint_map": AnyDict( + { + "": AnyStr(), + AnyStr("weather_graph:"): AnyStr(), + } + ), + } + }, + metadata={ + "source": "update", + "step": 2, + "writes": { + "weather_node": { + "messages": [ + {"role": "assistant", "content": "rainy"} + ] + } + }, + "parents": {"": AnyStr()}, + }, + created_at=AnyStr(), + parent_config={ + "configurable": { + "thread_id": "14", + "checkpoint_ns": AnyStr("weather_graph:"), + "checkpoint_id": AnyStr(), + } + }, + tasks=(), + ), + ), + ), + ) + assert [ + c + async for c in graph.astream( + None, config=config, stream_mode="updates", subgraphs=True + ) + ] == [ + ( + (), + { + "weather_graph": { + "messages": [ + HumanMessage( + content="what's the weather in sf", id=AnyStr() + ), + AIMessage(content="rainy", id=AnyStr()), + ] + } + }, + ), + ] async def test_checkpoint_metadata() -> None: @@ -8731,3 +9231,26 @@ async def test_checkpoint_metadata() -> None: assert chkpnt_tuple.metadata["thread_id"] == "2" assert chkpnt_tuple.metadata["test_config_3"] == "foo" assert chkpnt_tuple.metadata["test_config_4"] == "bar" + + +async def test_checkpointer_null_pending_writes() -> None: + class Node: + def __init__(self, name: str): + self.name = name + setattr(self, "__name__", name) + + def __call__(self, state): + return [self.name] + + builder = StateGraph(Annotated[list, operator.add]) + builder.add_node(Node("1")) + builder.add_edge(START, "1") + graph = builder.compile(checkpointer=MemorySaverNoPending()) + assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"] + assert graph.invoke([], {"configurable": {"thread_id": "foo"}}) == ["1"] * 2 + assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [ + "1" + ] * 3 + assert (await graph.ainvoke([], {"configurable": {"thread_id": "foo"}})) == [ + "1" + ] * 4 diff --git a/libs/langgraph/tests/test_state.py b/libs/langgraph/tests/test_state.py index 4088e38c9..392a3f22a 100644 --- a/libs/langgraph/tests/test_state.py +++ b/libs/langgraph/tests/test_state.py @@ -1,3 +1,4 @@ +import warnings from typing import Annotated as Annotated2 from typing import Any @@ -45,7 +46,8 @@ def test_warns_invalid_schema(schema: Any): ) def test_doesnt_warn_valid_schema(schema: Any): # Assert the function does not raise a warning - with pytest.warns(None): + with warnings.catch_warnings(): + warnings.simplefilter("error") _warn_invalid_state_schema(schema) diff --git a/libs/langgraph/tests/test_store.py b/libs/langgraph/tests/test_store.py index cd53ee407..71494adaf 100644 --- a/libs/langgraph/tests/test_store.py +++ b/libs/langgraph/tests/test_store.py @@ -1,11 +1,14 @@ import asyncio from typing import Any, Optional +import pytest from pytest_mock import MockerFixture from langgraph.store.base import BaseStore from langgraph.store.batch import AsyncBatchedStore +pytestmark = pytest.mark.anyio + async def test_async_batch_store(mocker: MockerFixture) -> None: aget = mocker.stub() diff --git a/libs/langgraph/tests/test_utils.py b/libs/langgraph/tests/test_utils.py index 1e3bf8ea1..3a914c23b 100644 --- a/libs/langgraph/tests/test_utils.py +++ b/libs/langgraph/tests/test_utils.py @@ -11,6 +11,8 @@ from langgraph.graph import END, StateGraph from langgraph.graph.graph import CompiledGraph from langgraph.utils import is_async_callable, is_async_generator +pytestmark = pytest.mark.anyio + def test_is_async() -> None: async def func() -> None: From ddeb9d73142f14034f948369119273cd468398e7 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 Sep 2024 17:42:37 -0700 Subject: [PATCH 225/287] Disable test in old versions of py --- libs/langgraph/tests/test_pregel_async.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 4d004075d..d3a6c422f 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -2,6 +2,7 @@ import asyncio import json import operator import re +import sys from collections import Counter from contextlib import asynccontextmanager, contextmanager from typing import ( @@ -8675,6 +8676,10 @@ async def test_send_to_nested_graphs(checkpointer_name: str) -> None: assert actual_history == expected_history +@pytest.mark.skipif( + sys.version_info < (3, 11), + reason="Python 3.11+ is required for async contextvars support", +) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_weather_subgraph( checkpointer_name: str, snapshot: SnapshotAssertion From 0cc37992986359d83541fbc1df6c46e6bd413424 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 Sep 2024 17:43:34 -0700 Subject: [PATCH 226/287] checkpoint: Implement sync methods in async checkpointers - These are needed when subgraphs are invoked synchronously in nodes --- .../langgraph/checkpoint/postgres/aio.py | 96 ++++++++++++++++++- .../langgraph/checkpoint/sqlite/aio.py | 88 +++++++++++------ libs/langgraph/tests/test_pregel_async.py | 11 ++- 3 files changed, 165 insertions(+), 30 deletions(-) diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py index 63a2038a3..a931181e1 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py @@ -1,6 +1,6 @@ import asyncio from contextlib import asynccontextmanager -from typing import Any, AsyncIterator, Optional, Union +from typing import Any, AsyncIterator, Iterator, List, Optional, Union from langchain_core.runnables import RunnableConfig from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline @@ -51,6 +51,7 @@ class AsyncPostgresSaver(BasePostgresSaver): self.conn = conn self.pipe = pipe self.lock = asyncio.Lock() + self.loop = asyncio.get_running_loop() @classmethod @asynccontextmanager @@ -329,3 +330,96 @@ class AsyncPostgresSaver(BasePostgresSaver): binary=True, row_factory=dict_row ) as cur: yield cur + + def list( + self, + config: RunnableConfig | None, + *, + filter: Optional[dict[str, Any]] = None, + before: Optional[RunnableConfig] = None, + limit: Optional[int] = None, + ) -> Iterator[CheckpointTuple]: + """List checkpoints from the database. + + This method retrieves a list of checkpoint tuples from the Postgres database based + on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first). + + Args: + config (Optional[RunnableConfig]): Base configuration for filtering checkpoints. + filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. + before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None. + limit (Optional[int]): Maximum number of checkpoints to return. + + Yields: + Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples. + """ + aiter_ = self.alist(config, filter=filter, before=before, limit=limit) + while True: + try: + yield asyncio.run_coroutine_threadsafe( + anext(aiter_), self.loop + ).result() + except StopAsyncIteration: + break + + def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: + """Get a checkpoint tuple from the database. + + This method retrieves a checkpoint tuple from the Postgres database based on the + provided config. If the config contains a "checkpoint_id" key, the checkpoint with + the matching thread ID and "checkpoint_id" is retrieved. Otherwise, the latest checkpoint + for the given thread ID is retrieved. + + Args: + config (RunnableConfig): The config to use for retrieving the checkpoint. + + Returns: + Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found. + """ + return asyncio.run_coroutine_threadsafe( + self.aget_tuple(config), self.loop + ).result() + + def put( + self, + config: RunnableConfig, + checkpoint: Checkpoint, + metadata: CheckpointMetadata, + new_versions: ChannelVersions, + ) -> RunnableConfig: + """Save a checkpoint to the database. + + This method saves a checkpoint to the Postgres database. The checkpoint is associated + with the provided config and its parent config (if any). + + Args: + config (RunnableConfig): The config to associate with the checkpoint. + checkpoint (Checkpoint): The checkpoint to save. + metadata (CheckpointMetadata): Additional metadata to save with the checkpoint. + new_versions (ChannelVersions): New channel versions as of this write. + + Returns: + RunnableConfig: Updated configuration after storing the checkpoint. + """ + return asyncio.run_coroutine_threadsafe( + self.aput(config, checkpoint, metadata, new_versions), self.loop + ).result() + + def put_writes( + self, + config: RunnableConfig, + writes: List[tuple[str, Any]], + task_id: str, + ) -> None: + """Store intermediate writes linked to a checkpoint. + + This method saves intermediate writes associated with a checkpoint to the database. + + Args: + config (RunnableConfig): Configuration of the related checkpoint. + writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair. + task_id (str): Identifier for the task creating the writes. + """ + return asyncio.run_coroutine_threadsafe( + self.aput_writes(config, writes, task_id), self.loop + ).result() diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py index dc68a0403..7cdc7c8fb 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py @@ -1,11 +1,11 @@ import asyncio -import functools from contextlib import asynccontextmanager from typing import ( Any, AsyncIterator, Dict, Iterator, + List, Optional, Sequence, Tuple, @@ -31,20 +31,6 @@ from langgraph.checkpoint.sqlite.utils import search_where T = TypeVar("T", bound=callable) -def not_implemented_sync_method(func: T) -> T: - @functools.wraps(func) - def wrapper(*args, **kwargs): - raise NotImplementedError( - "The AsyncSqliteSaver does not support synchronous methods. " - "Consider using the SqliteSaver instead.\n" - "from langgraph.checkpoint.sqlite import SqliteSaver\n" - "See https://langchain-ai.github.io/langgraph/reference/checkpoints/langgraph.checkpoint.sqlite.SqliteSaver " - "for more information." - ) - - return wrapper - - class AsyncSqliteSaver(BaseCheckpointSaver): """An asynchronous checkpoint saver that stores checkpoints in a SQLite database. @@ -132,6 +118,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver): self.jsonplus_serde = JsonPlusSerializer() self.conn = conn self.lock = asyncio.Lock() + self.loop = asyncio.get_running_loop() self.is_setup = False @classmethod @@ -150,16 +137,24 @@ class AsyncSqliteSaver(BaseCheckpointSaver): async with aiosqlite.connect(conn_string) as conn: yield AsyncSqliteSaver(conn) - @not_implemented_sync_method def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: """Get a checkpoint tuple from the database. - Note: - This method is not implemented for the AsyncSqliteSaver. Use `aget` instead. - Or consider using the [SqliteSaver][sqlitesaver] checkpointer. - """ + This method retrieves a checkpoint tuple from the SQLite database based on the + provided config. If the config contains a "checkpoint_id" key, the checkpoint with + the matching thread ID and checkpoint ID is retrieved. Otherwise, the latest checkpoint + for the given thread ID is retrieved. + + Args: + config (RunnableConfig): The config to use for retrieving the checkpoint. + + Returns: + Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found. + """ + return asyncio.run_coroutine_threadsafe( + self.aget_tuple(config), self.loop + ).result() - @not_implemented_sync_method def list( self, config: Optional[RunnableConfig], @@ -168,21 +163,60 @@ class AsyncSqliteSaver(BaseCheckpointSaver): before: Optional[RunnableConfig] = None, limit: Optional[int] = None, ) -> Iterator[CheckpointTuple]: - """List checkpoints from the database. + """List checkpoints from the database asynchronously. - Note: - This method is not implemented for the AsyncSqliteSaver. Use `alist` instead. - Or consider using the [SqliteSaver][sqlitesaver] checkpointer. + This method retrieves a list of checkpoint tuples from the SQLite database based + on the provided config. The checkpoints are ordered by checkpoint ID in descending order (newest first). + + Args: + config (Optional[RunnableConfig]): Base configuration for filtering checkpoints. + filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. + before (Optional[RunnableConfig]): If provided, only checkpoints before the specified checkpoint ID are returned. Defaults to None. + limit (Optional[int]): Maximum number of checkpoints to return. + + Yields: + Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples. """ + aiter_ = self.alist(config, filter=filter, before=before, limit=limit) + while True: + try: + yield asyncio.run_coroutine_threadsafe( + anext(aiter_), self.loop + ).result() + except StopAsyncIteration: + break - @not_implemented_sync_method def put( self, config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, + new_versions: ChannelVersions, ) -> RunnableConfig: - """Save a checkpoint to the database. FOO""" + """Save a checkpoint to the database. + + This method saves a checkpoint to the SQLite database. The checkpoint is associated + with the provided config and its parent config (if any). + + Args: + config (RunnableConfig): The config to associate with the checkpoint. + checkpoint (Checkpoint): The checkpoint to save. + metadata (CheckpointMetadata): Additional metadata to save with the checkpoint. + new_versions (ChannelVersions): New channel versions as of this write. + + Returns: + RunnableConfig: Updated configuration after storing the checkpoint. + """ + return asyncio.run_coroutine_threadsafe( + self.aput(config, checkpoint, metadata, new_versions), self.loop + ).result() + + def put_writes( + self, config: RunnableConfig, writes: List[Tuple[str, Any]], task_id: str + ) -> None: + return asyncio.run_coroutine_threadsafe( + self.aput_writes(config, writes, task_id), self.loop + ).result() async def setup(self) -> None: """Set up the checkpoint database asynchronously. diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index d3a6c422f..7901ad2bc 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -8771,8 +8771,10 @@ async def test_weather_subgraph( else: return "normal_llm_node" - async def weather_graph(state: RouterState): - return await subgraph.ainvoke(state) + def weather_graph(state: RouterState): + # this tests that all async checkpointers tested also implement sync methods + # as the subgraph called with sync invoke will use sync checkpointer methods + return subgraph.invoke(state) graph = StateGraph(RouterState) graph.add_node(router_node) @@ -8783,6 +8785,9 @@ async def test_weather_subgraph( graph.add_edge("normal_llm_node", END) graph.add_edge("weather_graph", END) + def get_first_in_list(): + return [*graph.get_state_history(config, limit=1)][0] + async with awith_checkpointer(checkpointer_name) as checkpointer: graph = graph.compile(checkpointer=checkpointer) @@ -8846,6 +8851,8 @@ async def test_weather_subgraph( ), ), ) + # confirm that list() delegates to alist() correctly + assert await asyncio.to_thread(get_first_in_list) == state # update await graph.aupdate_state(state.tasks[0].state, {"city": "la"}) From 3b2a237deb8c225f573c112d923fdaf9e38b249f Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 Sep 2024 17:45:57 -0700 Subject: [PATCH 227/287] Lint --- libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py index a931181e1..6140af756 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py @@ -333,7 +333,7 @@ class AsyncPostgresSaver(BasePostgresSaver): def list( self, - config: RunnableConfig | None, + config: Optional[RunnableConfig], *, filter: Optional[dict[str, Any]] = None, before: Optional[RunnableConfig] = None, From dd4a15b569e3eec24c04f33b77b6dca0286e1876 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 Sep 2024 17:57:33 -0700 Subject: [PATCH 228/287] Require latest version of langgraph-checkpoint --- libs/checkpoint-postgres/poetry.lock | 7 +++---- libs/checkpoint-postgres/pyproject.toml | 2 +- libs/checkpoint-sqlite/poetry.lock | 7 +++---- libs/checkpoint-sqlite/pyproject.toml | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/libs/checkpoint-postgres/poetry.lock b/libs/checkpoint-postgres/poetry.lock index f9ca0f97e..520300e91 100644 --- a/libs/checkpoint-postgres/poetry.lock +++ b/libs/checkpoint-postgres/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -266,7 +266,7 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0" [[package]] name = "langgraph-checkpoint" -version = "1.0.6" +version = "1.0.8" description = "Library with base interfaces for LangGraph checkpoint savers." optional = false python-versions = "^3.9.0,<4.0" @@ -766,7 +766,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -972,4 +971,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0,<4.0" -content-hash = "b139531e8c6f4e24cea4bdfc29d111d1ea000a6a8a604ba81be4bff0977a2466" +content-hash = "e294b6996aa6c8f671e6aaf65be8b4aba94c18e2f237dd3ee0e1b777849ce8a8" diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml index 077f8dd09..fb68cc181 100644 --- a/libs/checkpoint-postgres/pyproject.toml +++ b/libs/checkpoint-postgres/pyproject.toml @@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }] [tool.poetry.dependencies] python = "^3.9.0,<4.0" -langgraph-checkpoint = "^1.0.1" +langgraph-checkpoint = "^1.0.8" orjson = ">=3.10.1" psycopg = "^3.0.0" psycopg-pool = "^3.0.0" diff --git a/libs/checkpoint-sqlite/poetry.lock b/libs/checkpoint-sqlite/poetry.lock index 0ea747b2c..8d5dbe3af 100644 --- a/libs/checkpoint-sqlite/poetry.lock +++ b/libs/checkpoint-sqlite/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiosqlite" @@ -252,7 +252,7 @@ tenacity = ">=8.1.0,<8.4.0 || >8.4.0,<9.0.0" [[package]] name = "langgraph-checkpoint" -version = "1.0.1" +version = "1.0.8" description = "Library with base interfaces for LangGraph checkpoint savers." optional = false python-versions = "^3.9.0,<4.0" @@ -651,7 +651,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -835,4 +834,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.9.0" -content-hash = "d50ec7c6b55075d19193e080cc95d3b0998b9efa1a0ed407bd6055b5b5e867e0" +content-hash = "752a22dc2b57a0818a3a4d9bf5f62226ba7f8e0c458892551bf8b4c93723dbc1" diff --git a/libs/checkpoint-sqlite/pyproject.toml b/libs/checkpoint-sqlite/pyproject.toml index 536895f77..4b052d387 100644 --- a/libs/checkpoint-sqlite/pyproject.toml +++ b/libs/checkpoint-sqlite/pyproject.toml @@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }] [tool.poetry.dependencies] python = "^3.9.0" -langgraph-checkpoint = "^1.0.1" +langgraph-checkpoint = "^1.0.8" aiosqlite = "^0.20.0" [tool.poetry.group.dev.dependencies] From 6e9f54e5ae9bd70ac0a114ddbe9b1169b9d9b89f Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 Sep 2024 17:57:55 -0700 Subject: [PATCH 229/287] postgres 1.0.6 --- libs/checkpoint-postgres/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint-postgres/pyproject.toml b/libs/checkpoint-postgres/pyproject.toml index fb68cc181..6e3640e7c 100644 --- a/libs/checkpoint-postgres/pyproject.toml +++ b/libs/checkpoint-postgres/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint-postgres" -version = "1.0.5" +version = "1.0.6" description = "Library with a Postgres implementation of LangGraph checkpoint saver." authors = [] license = "MIT" From af666a87fbf805d1942b5912ebab78822aa100f7 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 Sep 2024 17:58:01 -0700 Subject: [PATCH 230/287] sqlite 1.0.2 --- libs/checkpoint-sqlite/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint-sqlite/pyproject.toml b/libs/checkpoint-sqlite/pyproject.toml index 4b052d387..9d8818619 100644 --- a/libs/checkpoint-sqlite/pyproject.toml +++ b/libs/checkpoint-sqlite/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint-sqlite" -version = "1.0.1" +version = "1.0.2" description = "Library with a SQLite implementation of LangGraph checkpoint saver." authors = [] license = "MIT" From 322d6c8dde4a99a88c76eb0a13d157545a2bfec2 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Mon, 2 Sep 2024 18:48:50 -0700 Subject: [PATCH 231/287] patch(docs): Update docs with JS cloud instructions (#1560) * Update docs with JS cloud instructions * Add to index * Add md fence * nits * nits * Update setup_javascript.md --------- Co-authored-by: isaac hershenson --- .../docs/cloud/deployment/setup_javascript.md | 200 ++++++++++++++++++ docs/docs/cloud/faq/studio.md | 18 +- .../cloud/how-tos/interrupt_concurrent.md | 2 +- docs/docs/cloud/quick_start.md | 199 ++++++++++++++--- docs/mkdocs.yml | 1 + 5 files changed, 383 insertions(+), 37 deletions(-) create mode 100644 docs/docs/cloud/deployment/setup_javascript.md diff --git a/docs/docs/cloud/deployment/setup_javascript.md b/docs/docs/cloud/deployment/setup_javascript.md new file mode 100644 index 000000000..fa4c7b823 --- /dev/null +++ b/docs/docs/cloud/deployment/setup_javascript.md @@ -0,0 +1,200 @@ +# How to Set Up a LangGraph.js Application for Deployment + +A [LangGraph.js](https://langchain-ai.github.io/langgraphjs/) application must be configured with a [LangGraph API configuration file](../reference/cli.md#configuration-file) in order to be deployed to LangGraph Cloud (or to be self-hosted). This how-to guide discusses the basic steps to setup a LangGraph.js application for deployment using `package.json` to specify project dependencies. + +This walkthrough is based on [this repository](https://github.com/langchain-ai/langgraphjs-studio-starter), which you can play around with to learn more about how to setup your LangGraph application for deployment. + +The final repo structure will look something like this: + +```bash +my-app/ +├── src # all project code lies within here +│ ├── utils # optional utilities for your graph +│ │ ├── tools.ts # tools for your graph +│ │ ├── nodes.ts # node functions for you graph +│ │ └── state.ts # state definition of your graph +│   └── agent.ts # code for constructing your graph +├── package.json # package dependencies +├── .env # environment variables +└── langgraph.json # configuration file for LangGraph +``` + +After each step, an example file directory is provided to demonstrate how code can be organized. + +## Specify Dependencies + +Dependencies can be specified in a `package.json`. If none of these files is created, then dependencies can be specified later in the [LangGraph API configuration file](#create-langgraph-api-config). + +Example `package.json` file: + +```json +{ + "name": "langgraphjs-studio-starter", + "packageManager": "yarn@1.22.22", + "dependencies": { + "@langchain/community": "^0.2.31", + "@langchain/core": "^0.2.31", + "@langchain/langgraph": "^0.2.0", + "@langchain/openai": "^0.2.8" + } +} +``` + +Example file directory: + +```bash +my-app/ +└── package.json # package dependencies +``` + +## Specify Environment Variables + +Environment variables can optionally be specified in a file (e.g. `.env`). See the [Environment Variables reference](../reference/env_var.md) to configure additional variables for a deployment. + +Example `.env` file: + +``` +MY_ENV_VAR_1=foo +MY_ENV_VAR_2=bar +OPENAI_API_KEY=key +TAVILY_API_KEY=key_2 +``` + +Example file directory: + +```bash +my-app/ +├── package.json +└── .env # environment variables +``` + +## Define Graphs + +Implement your graphs! Graphs can be defined in a single file or multiple files. Make note of the variable names of each compiled graph to be included in the LangGraph application. The variable names will be used later when creating the [LangGraph API configuration file](../reference/cli.md#configuration-file). + +Here is an example `agent.ts`: + +```ts +import type { AIMessage } from "@langchain/core/messages"; +import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; +import { ChatOpenAI } from "@langchain/openai"; + +import { MessagesAnnotation, StateGraph } from "@langchain/langgraph"; +import { ToolNode } from "@langchain/langgraph/prebuilt"; + +const tools = [ + new TavilySearchResults({ maxResults: 3, }), +]; + +// Define the function that calls the model +async function callModel( + state: typeof MessagesAnnotation.State, +) { + /** + * Call the LLM powering our agent. + * Feel free to customize the prompt, model, and other logic! + */ + const model = new ChatOpenAI({ + model: "gpt-4o", + }).bindTools(tools); + + const response = await model.invoke([ + { + role: "system", + content: `You are a helpful assistant. The current date is ${new Date().getTime()}.` + }, + ...state.messages + ]); + + // MessagesAnnotation supports returning a single message or array of messages + return { messages: response }; +} + +// Define the function that determines whether to continue or not +function routeModelOutput(state: typeof MessagesAnnotation.State) { + const messages = state.messages; + const lastMessage: AIMessage = messages[messages.length - 1]; + // If the LLM is invoking tools, route there. + if ((lastMessage?.tool_calls?.length ?? 0) > 0) { + return "tools"; + } + // Otherwise end the graph. + return "__end__"; +} + +// Define a new graph. +// See https://langchain-ai.github.io/langgraphjs/how-tos/define-state/#getting-started for +// more on defining custom graph states. +const workflow = new StateGraph(MessagesAnnotation) + // Define the two nodes we will cycle between + .addNode("callModel", callModel) + .addNode("tools", new ToolNode(tools)) + // Set the entrypoint as `callModel` + // This means that this node is the first one called + .addEdge("__start__", "callModel") + .addConditionalEdges( + // First, we define the edges' source node. We use `callModel`. + // This means these are the edges taken after the `callModel` node is called. + "callModel", + // Next, we pass in the function that will determine the sink node(s), which + // will be called after the source node is called. + routeModelOutput, + // List of the possible destinations the conditional edge can route to. + // Required for conditional edges to properly render the graph in Studio + [ + "tools", + "__end__" + ], + ) + // This means that after `tools` is called, `callModel` node is called next. + .addEdge("tools", "callModel"); + +// Finally, we compile it! +// This compiles it into a graph you can invoke and deploy. +export const graph = workflow.compile(); +``` + +!!! info "Assign `CompiledGraph` to Variable" + The build process for LangGraph Cloud requires that the `CompiledGraph` object be assigned to a variable at the top-level of a JavaScript module (alternatively, you can provide [a function that creates a graph](./graph_rebuild.md)). + +Example file directory: + +```bash +my-app/ +├── src # all project code lies within here +│ ├── utils # optional utilities for your graph +│ │ ├── tools.ts # tools for your graph +│ │ ├── nodes.ts # node functions for you graph +│ │ └── state.ts # state definition of your graph +│   └── agent.ts # code for constructing your graph +├── package.json # package dependencies +├── .env # environment variables +└── langgraph.json # configuration file for LangGraph +``` + +## Create LangGraph API Config + +Create a [LangGraph API configuration file](../reference/cli.md#configuration-file) called `langgraph.json`. See the [LangGraph CLI reference](../reference/cli.md#configuration-file) for detailed explanations of each key in the JSON object of the configuration file. + +Example `langgraph.json` file: + +```json +{ + "node_version": "20", + "dockerfile_lines": [], + "dependencies": ["."], + "graphs": { + "agent": "./src/agent.ts:graph" + }, + "env": ".env" +} +``` + +Note that the variable name of the `CompiledGraph` appears at the end of the value of each subkey in the top-level `graphs` key (i.e. `:`). + +!!! info "Configuration Location" + The LangGraph API configuration file must be placed in a directory that is at the same level or higher than the TypeScript files that contain compiled graphs and associated dependencies. + +## Next + +After you setup your project and place it in a github repo, it's time to [deploy your app](./cloud.md). diff --git a/docs/docs/cloud/faq/studio.md b/docs/docs/cloud/faq/studio.md index cef962f34..070014ce7 100644 --- a/docs/docs/cloud/faq/studio.md +++ b/docs/docs/cloud/faq/studio.md @@ -43,15 +43,23 @@ If you don't define your conditional edges carefully, you might notice extra edg ### Solution 1: Include a path map -The first way to solve this is to add path maps to your conditional edges. A path map is just a dictionary that maps the possible outputs of your router function with the names of the nodes that each output corresponds to. The path map is passed as the third argument to the `add_conditional_edges` function like so: +The first way to solve this is to add path maps to your conditional edges. A path map is just a dictionary or array that maps the possible outputs of your router function with the names of the nodes that each output corresponds to. The path map is passed as the third argument to the `add_conditional_edges` function like so: -```python -graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"}) -``` +=== "Python" + + ```python + graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"}) + ``` + +=== "Javascript" + + ```ts + graph.addConditionalEdges("node_a", routingFunction, ["node_b", "node_c"]); + ``` In this case, the routing function returns either True or False, which map to `node_b` and `node_c` respectively. -### Solution 2: Update the typing of the router +### Solution 2: Update the typing of the router (Python only) Instead of passing a path map, you can also be explicit about the typing of your routing function by specifying the nodes it can map to using the `Literal` python definition. Here is an example of how to define a routing function in that way: diff --git a/docs/docs/cloud/how-tos/interrupt_concurrent.md b/docs/docs/cloud/how-tos/interrupt_concurrent.md index e67fb4ce5..b09f908ef 100644 --- a/docs/docs/cloud/how-tos/interrupt_concurrent.md +++ b/docs/docs/cloud/how-tos/interrupt_concurrent.md @@ -44,7 +44,7 @@ Now, let's import our required packages and instantiate our client, assistant, a const thread = await client.threads.create(); ``` -Now we can start our two runs and join the second on euntil it has completed: +Now we can start our two runs and join the second one until it has completed: === "Python" diff --git a/docs/docs/cloud/quick_start.md b/docs/docs/cloud/quick_start.md index c59a6fb85..755ce0df4 100644 --- a/docs/docs/cloud/quick_start.md +++ b/docs/docs/cloud/quick_start.md @@ -14,13 +14,25 @@ This tutorial will use: 1. Create a new application with the following directory and files: +=== "Python" + / |-- agent.py # code for your LangGraph agent |-- requirements.txt # Python packages required for your graph |-- langgraph.json # configuration file for LangGraph |-- .env # environment files with API keys -2. The `agent.py` file should contain Python code for defining your graph. The following code is a simple example, the important thing is that at some point in your file you compile your graph and assign the compiled graph to a variable (in this case the `graph` variable). This example code uses `create_react_agent`, a prebuilt agent, read more about it [here](..//concepts/agentic_concepts.md#react-agent). +=== "Javascript" + + / + |-- agent.ts # code for your LangGraph agent + |-- package.json # Javascript packages required for your graph + |-- langgraph.json # configuration file for LangGraph + |-- .env # environment files with API keys + +2. The `agent.py`/`agent.ts` file should contain code for defining your graph. The following code is a simple example, the important thing is that at some point in your file you compile your graph and assign the compiled graph to a variable (in this case the `graph` variable). This example code uses `create_react_agent`, a prebuilt agent. You can read more about it [here](../concepts/agentic_concepts.md#react-agent). + +=== "Python" ```python from langchain_anthropic import ChatAnthropic @@ -34,14 +46,53 @@ This tutorial will use: graph = create_react_agent(model, tools) ``` -3. The `requirements.txt` file should contain any dependencies for your graph(s). In this case we only require four packages for our graph to run: +=== "Javascript" - langgraph - langchain_anthropic - tavily-python - langchain_community + ```ts + import { ChatAnthropic } from "@langchain/anthropic"; + import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; + import { createReactAgent } from "@langchain/langgraph/prebuilt"; -4. The [`langgraph.json`][langgraph.json] file is a configuration file that describes what graph(s) you are going to host. In this case we only have one graph to host: the compiled `graph` object from `agent.py`. + const model = new ChatAnthropic({ + model: "claude-3-5-sonnet-20240620", + }); + + const tools = [ + new TavilySearchResults({ maxResults: 3, }), + ]; + + export const graph = createReactAgent({ llm: model, tools }); + ``` + +3. The `requirements.txt`/`package.json` file should contain any dependencies for your graph(s). In this case we only require four packages for our graph to run: + +=== "Python" + + ```python + langgraph + langchain_anthropic + tavily-python + langchain_community + ``` + +=== "Javascript" + + ```js + { + "name": "my-app", + "packageManager": "yarn@1.22.22", + "dependencies": { + "@langchain/community": "^0.2.31", + "@langchain/core": "^0.2.31", + "@langchain/langgraph": "0.2.0", + "@langchain/openai": "^0.2.8" + } + } + ``` + +4. The [`langgraph.json`][langgraph.json] file is a configuration file that describes what graph(s) you are going to host. In this case we only have one graph to host: the compiled `graph` object from `agent.py`/`agent.ts`. + +=== "Python" ```json { @@ -53,7 +104,21 @@ This tutorial will use: } ``` - Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file). +=== "Javascript" + + ```json + { + "node_version": "20", + "dockerfile_lines": [], + "dependencies": ["."], + "graphs": { + "agent": "./src/agent.ts:graph" + }, + "env": ".env" + } + ``` + +Learn more about the LangGraph CLI configuration file [here](./reference/cli.md#configuration-file). 5. The `.env` file should have any environment variables needed to run your graph. This will only be used for local testing, so if you are not testing locally you can skip this step. NOTE: if you do add this, you should NOT check this into git. For this graph, we need two environment variables: @@ -206,36 +271,108 @@ export LANGSMITH_API_KEY=... The first thing to do when using the SDK is to setup our client, access our assistant, and create a thread to execute a run on: -```python -from langgraph_sdk import get_client +=== "Python" -# Replace this with the URL of your own deployed graph -URL = "https://chatbot-23a570f3210f52a7b167f09f6158e3b3-ffoprvkqsa-uc.a.run.app" -client = get_client(url=URL) + ```python + from langgraph_sdk import get_client -# Search all hosted graphs -assistants = await client.assistants.search() -# In this example we select the first assistant since we are only hosting a single graph -assistant = assistants[0] + client = get_client(url=) + # get default assistant + assistants = await client.assistants.search() + assistant = [a for a in assistants if not a["config"]][0] + # create thread + thread = await client.threads.create() + print(thread) + ``` -# We create a thread for tracking the state of our run -thread = await client.threads.create() -``` +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // get default assistant + const assistants = await client.assistants.search(); + const assistant = assistants.find(a => !a.config); + // create thread + const thread = await client.threads.create(); + console.log(thread) + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \ + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` We can then execute a run on the thread: -```python -input = {"messages":[{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}]} +=== "Python" -async for chunk in client.runs.stream( - thread['thread_id'], - assistant["assistant_id"], - input=input, - stream_mode="updates", - ): - if chunk.data and chunk.event != "metadata": - print(chunk.data) -``` + ```python + input = {"messages":[{"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."}]} + + async for chunk in client.runs.stream( + thread['thread_id'], + assistant["assistant_id"], + input=input, + stream_mode="updates", + ): + if chunk.data and chunk.event != "metadata": + print(chunk.data) + ``` + +=== "Javascript" + + ```js + const input = { "messages":[{ "role": "user", "content": "Hello! My name is Bagatur and I am 26 years old." }] }; + + const streamResponse = client.runs.stream( + thread["thread_id"], + assistant["assistant_id"], + { + input, + } + ); + for await (const chunk of streamResponse) { + if (chunk.data && chunk.event !== "metadata" ) { + console.log(chunk.data); + } + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": , + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]}, + }" | sed 's/\r$//' | awk ' + /^event:/ { event = $2 } + /^data:/ { + json_data = substr($0, index($0, $2)) + + if (event != "metadata") { + print json_data + } + }' + ``` + + +Output: {'agent': {'messages': [{'content': "Hi Bagatur! It's nice to meet you. How can I assist you today?", 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_9cb5d38cf7'}, 'type': 'ai', 'name': None, 'id': 'run-c89118b7-1b1e-42b9-a85d-c43fe99881cd', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}} diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index eb6d5d36f..56a9435b2 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -197,6 +197,7 @@ nav: - Setup: - Setup App: "cloud/deployment/setup.md" - Setup App (pyproject.toml): "cloud/deployment/setup_pyproject.md" + - Setup App (JavaScript): "cloud/deployment/setup_javascript.md" - Rebuild Graph at Runtime: "cloud/deployment/graph_rebuild.md" - Test App Locally: "cloud/deployment/test_locally.md" - Deployment: From 8c0b27a85bf58d503c6b79e9f6acc68263e09334 Mon Sep 17 00:00:00 2001 From: gbaian10 <34255899+gbaian10@users.noreply.github.com> Date: Tue, 3 Sep 2024 21:50:00 +0800 Subject: [PATCH 232/287] docs: fix Corrective RAG (CRAG) miss function (#1563) `Corrective RAG (CRAG) using local LLMs` miss `find_tool_calls_react` function --- examples/rag/langgraph_crag_local.ipynb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/rag/langgraph_crag_local.ipynb b/examples/rag/langgraph_crag_local.ipynb index dbac6f697..d6e791ae3 100644 --- a/examples/rag/langgraph_crag_local.ipynb +++ b/examples/rag/langgraph_crag_local.ipynb @@ -429,6 +429,14 @@ "]\n", "\n", "\n", + "def find_tool_calls_react(messages):\n", + " \"\"\"\n", + " Find all tool calls in the messages returned\n", + " \"\"\"\n", + " tool_calls = [tc['name'] for m in messages['messages'] for tc in getattr(m, 'tool_calls', [])]\n", + " return tool_calls\n", + "\n", + "\n", "def check_trajectory_react(root_run: Run, example: Example) -> dict:\n", " \"\"\"\n", " Check if all expected tools are called in exact order and without any additional tool calls.\n", From c1d7cbd67c49fbfc7554d4e7c020c1ca964b2297 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Tue, 3 Sep 2024 07:47:31 -0700 Subject: [PATCH 233/287] Adds JS deploy guide to index (#1590) --- docs/docs/cloud/how-tos/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/cloud/how-tos/index.md b/docs/docs/cloud/how-tos/index.md index 255490d32..90b13c35e 100644 --- a/docs/docs/cloud/how-tos/index.md +++ b/docs/docs/cloud/how-tos/index.md @@ -13,6 +13,7 @@ LangGraph Cloud gives you best in class observability, testing, and hosting serv - [How to set up app for deployment (requirements.txt)](../deployment/setup.md) - [How to set up app for deployment (pyproject.toml)](../deployment/setup_pyproject.md) +- [How to set up app for deployment (JavaScript)](../deployment/setup_javascript.md) - [How to test locally](../deployment/test_locally.md) - [How to deploy to LangGraph cloud](../deployment/cloud.md) - [How to self-host](../deployment/self_hosted.md) From e5b09b8c202de3798044b2067a143be8ba193dec Mon Sep 17 00:00:00 2001 From: gbaian10 <34255899+gbaian10@users.noreply.github.com> Date: Tue, 3 Sep 2024 23:00:14 +0800 Subject: [PATCH 234/287] docs: update `Postgres checkpointer for persistence` (#1556) --- examples/persistence_postgres.ipynb | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/examples/persistence_postgres.ipynb b/examples/persistence_postgres.ipynb index 5ea838cee..b4fa1e2fe 100644 --- a/examples/persistence_postgres.ipynb +++ b/examples/persistence_postgres.ipynb @@ -132,8 +132,6 @@ "metadata": {}, "outputs": [], "source": [ - "from psycopg.rows import dict_row\n", - "\n", "connection_kwargs = {\n", " \"autocommit\": True,\n", " \"prepare_threshold\": 0,\n", @@ -161,15 +159,13 @@ "source": [ "from psycopg_pool import ConnectionPool\n", "\n", - "pool = ConnectionPool(\n", + "with ConnectionPool(\n", " # Example configuration\n", " conninfo=DB_URI,\n", " max_size=20,\n", " kwargs=connection_kwargs,\n", - ")\n", - "\n", - "with pool.connection() as conn:\n", - " checkpointer = PostgresSaver(conn)\n", + ") as pool:\n", + " checkpointer = PostgresSaver(pool)\n", "\n", " # NOTE: you need to call .setup() the first time you're using your checkpointer\n", " checkpointer.setup()\n", @@ -394,8 +390,8 @@ " conninfo=DB_URI,\n", " max_size=20,\n", " kwargs=connection_kwargs,\n", - ") as pool, pool.connection() as conn:\n", - " checkpointer = AsyncPostgresSaver(conn)\n", + ") as pool:\n", + " checkpointer = AsyncPostgresSaver(pool)\n", "\n", " # NOTE: you need to call .setup() the first time you're using your checkpointer\n", " # await checkpointer.setup()\n", From d492475c6133b39ed6aa14af99883e9c7b46906c Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Tue, 3 Sep 2024 11:00:37 -0400 Subject: [PATCH 235/287] checkpoint-sqlite: close connection in from_conn_string (#1589) --- .../langgraph/checkpoint/sqlite/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py index 04493a381..a5f657824 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py @@ -104,10 +104,12 @@ class SqliteSaver(BaseCheckpointSaver): with SqliteSaver.from_conn_string("checkpoints.sqlite") as memory: ... """ - with sqlite3.connect( - conn_string, - # https://ricardoanderegg.com/posts/python-sqlite-thread-safety/ - check_same_thread=False, + with closing( + sqlite3.connect( + conn_string, + # https://ricardoanderegg.com/posts/python-sqlite-thread-safety/ + check_same_thread=False, + ) ) as conn: yield SqliteSaver(conn) From 43d5dbf1741fb81d5fa34b1e34146930115c6702 Mon Sep 17 00:00:00 2001 From: Sreenivasan R S <101383058+sreeniSudharsan@users.noreply.github.com> Date: Tue, 3 Sep 2024 21:42:31 +0530 Subject: [PATCH 236/287] examples: fix missing import introduction (#1584) --- examples/introduction.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb index 3d0287d05..52cd1018b 100644 --- a/examples/introduction.ipynb +++ b/examples/introduction.ipynb @@ -1504,7 +1504,7 @@ "from langgraph.checkpoint.memory import MemorySaver\n", "from langgraph.graph import StateGraph\n", "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ToolNode\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", "\n", "\n", "class State(TypedDict):\n", From 3c5913aa29d2c43c20f6378c909b050e92f47f28 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 Sep 2024 19:17:38 -0700 Subject: [PATCH 237/287] Remove pydantic base class from Pregel/PregelNode --- libs/langgraph/langgraph/graph/graph.py | 4 + libs/langgraph/langgraph/graph/state.py | 2 +- libs/langgraph/langgraph/pregel/__init__.py | 89 +++++++------ libs/langgraph/langgraph/pregel/read.py | 126 ++++++++++++------ .../tests/__snapshots__/test_pregel.ambr | 60 ++------- libs/langgraph/tests/test_pregel.py | 13 +- 6 files changed, 155 insertions(+), 139 deletions(-) diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index d466a3c96..570c06bad 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -424,6 +424,10 @@ class Graph: class CompiledGraph(Pregel): builder: Graph + def __init__(self, *, builder: Graph, **kwargs): + super().__init__(**kwargs) + self.builder = builder + def attach_node(self, key: str, node: NodeSpec) -> None: self.channels[key] = EphemeralValue(Any) self.nodes[key] = ( diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index f576a98cd..307b672aa 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -17,10 +17,10 @@ from typing import ( overload, ) -from langchain_core.pydantic_v1 import BaseModel from langchain_core.runnables import Runnable, RunnableConfig from langchain_core.runnables.base import RunnableLike from langchain_core.runnables.utils import create_model +from pydantic import BaseModel from langgraph.channels.base import BaseChannel from langgraph.channels.binop import BinaryOperatorAggregate diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 01f0ad91d..c5fa760c5 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -25,12 +25,10 @@ from uuid import UUID, uuid5 from langchain_core.globals import get_debug from langchain_core.load.dump import dumpd -from langchain_core.pydantic_v1 import BaseModel, Field, root_validator from langchain_core.runnables import ( Runnable, RunnableLambda, RunnableSequence, - RunnableSerializable, ) from langchain_core.runnables.base import Input, Output, coerce_to_runnable from langchain_core.runnables.config import ( @@ -48,6 +46,7 @@ from langchain_core.runnables.utils import ( get_unique_config_specs, ) from langchain_core.tracers._streaming import _StreamingCallbackHandler +from pydantic import BaseModel from typing_extensions import Self from langgraph.channels.base import ( @@ -186,16 +185,10 @@ class Channel: ) -class Pregel( - RunnableSerializable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]] -): +class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): nodes: Mapping[str, PregelNode] - channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]] = Field( - default_factory=dict - ) - - auto_validate: bool = True + channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]] stream_mode: StreamMode = "values" """Mode to stream output, defaults to 'values'.""" @@ -205,16 +198,16 @@ class Pregel( stream_channels: Optional[Union[str, Sequence[str]]] = None """Channels to stream, defaults to all channels not in reserved channels""" - interrupt_after_nodes: Union[All, Sequence[str]] = Field(default_factory=list) + interrupt_after_nodes: Union[All, Sequence[str]] - interrupt_before_nodes: Union[All, Sequence[str]] = Field(default_factory=list) + interrupt_before_nodes: Union[All, Sequence[str]] input_channels: Union[str, Sequence[str]] step_timeout: Optional[float] = None """Maximum time to wait for a step to complete, in seconds. Defaults to None.""" - debug: bool = Field(default_factory=get_debug) + debug: bool """Whether to print debug information during execution. Defaults to False.""" checkpointer: Optional[BaseCheckpointSaver] = None @@ -232,36 +225,50 @@ class Pregel( name: str = "LangGraph" - class Config: - arbitrary_types_allowed = True + def __init__( + self, + *, + nodes: Mapping[str, PregelNode], + channels: Mapping[str, Union[BaseChannel, ManagedValueSpec]] = None, + auto_validate: bool = True, + stream_mode: StreamMode = "values", + output_channels: Union[str, Sequence[str]], + stream_channels: Optional[Union[str, Sequence[str]]] = None, + interrupt_after_nodes: Union[All, Sequence[str]] = (), + interrupt_before_nodes: Union[All, Sequence[str]] = (), + input_channels: Union[str, Sequence[str]], + step_timeout: Optional[float] = None, + debug: bool = False, + checkpointer: Optional[BaseCheckpointSaver] = None, + store: Optional[BaseStore] = None, + retry_policy: Optional[RetryPolicy] = None, + config_type: Optional[Type[Any]] = None, + config: Optional[RunnableConfig] = None, + name: str = "LangGraph", + ) -> None: + self.nodes = nodes + self.channels = channels or {} + self.stream_mode = stream_mode + self.output_channels = output_channels + self.stream_channels = stream_channels + self.interrupt_after_nodes = interrupt_after_nodes + self.interrupt_before_nodes = interrupt_before_nodes + self.input_channels = input_channels + self.step_timeout = step_timeout + self.debug = debug if debug is not None else get_debug() + self.checkpointer = checkpointer + self.store = store + self.retry_policy = retry_policy + self.config_type = config_type + self.config = config + self.name = name + if auto_validate: + self.validate() def with_config(self, config: RunnableConfig | None = None, **kwargs: Any) -> Self: - return self.copy( - update={"config": cast(RunnableConfig, {**(config or {}), **kwargs})} - ) - - @classmethod - def is_lc_serializable(cls) -> bool: - """Return whether the graph can be serialized by Langchain.""" - return True - - @root_validator(skip_on_failure=True) - def validate_on_init(cls, values: dict[str, Any]) -> dict[str, Any]: - if not values["auto_validate"]: - return values - validate_graph( - values["nodes"], - values["channels"], - values["input_channels"], - values["output_channels"], - values["stream_channels"], - values["interrupt_after_nodes"], - values["interrupt_before_nodes"], - ) - if values["interrupt_after_nodes"] or values["interrupt_before_nodes"]: - if not values["checkpointer"]: - raise ValueError("Interrupts require a checkpointer") - return values + attrs = {**self.__dict__} + attrs["config"] = merge_configs(self.config, config, kwargs) + return self.__class__(**attrs) def validate(self) -> Self: validate_graph( diff --git a/libs/langgraph/langgraph/pregel/read.py b/libs/langgraph/langgraph/pregel/read.py index 163665e2e..ca0828fda 100644 --- a/libs/langgraph/langgraph/pregel/read.py +++ b/libs/langgraph/langgraph/pregel/read.py @@ -1,8 +1,16 @@ from __future__ import annotations -from typing import Any, Callable, Mapping, Optional, Sequence, Union +from typing import ( + Any, + AsyncIterator, + Callable, + Iterator, + Mapping, + Optional, + Sequence, + Union, +) -from langchain_core.pydantic_v1 import Field from langchain_core.runnables import ( Runnable, RunnableConfig, @@ -10,7 +18,7 @@ from langchain_core.runnables import ( RunnableSequence, RunnableSerializable, ) -from langchain_core.runnables.base import Other, RunnableBindingBase, coerce_to_runnable +from langchain_core.runnables.base import Input, Other, Output, coerce_to_runnable from langchain_core.runnables.config import merge_configs from langchain_core.runnables.utils import ConfigurableFieldSpec @@ -99,20 +107,47 @@ class ChannelRead(RunnableCallable): DEFAULT_BOUND: RunnablePassthrough = RunnablePassthrough() -class PregelNode(RunnableBindingBase): +class PregelNode(Runnable): channels: Union[list[str], Mapping[str, str]] - triggers: list[str] = Field(default_factory=list) + triggers: list[str] - mapper: Optional[Callable[[Any], Any]] = None + mapper: Optional[Callable[[Any], Any]] - writers: list[Runnable] = Field(default_factory=list) + writers: list[Runnable] - bound: Runnable[Any, Any] = Field(default=DEFAULT_BOUND) + bound: Runnable[Any, Any] - kwargs: Mapping[str, Any] = Field(default_factory=dict) + retry_policy: Optional[RetryPolicy] - retry_policy: Optional[RetryPolicy] = None + config: RunnableConfig + + def __init__( + self, + *, + channels: Union[list[str], Mapping[str, str]], + triggers: Sequence[str], + mapper: Optional[Callable[[Any], Any]] = None, + writers: Optional[list[Runnable]] = None, + tags: Optional[list[str]] = None, + metadata: Optional[Mapping[str, Any]] = None, + bound: Optional[Runnable[Any, Any]] = None, + retry_policy: Optional[RetryPolicy] = None, + config: Optional[RunnableConfig] = None, + ) -> None: + self.channels = channels + self.triggers = list(triggers) + self.mapper = mapper + self.writers = writers or [] + self.bound = bound if bound is not None else DEFAULT_BOUND + self.retry_policy = retry_policy + self.config = merge_configs( + config, {"tags": tags or [], "metadata": metadata or {}} + ) + + def copy(self, update: dict[str, Any]) -> PregelNode: + attrs = {**self.__dict__, **update} + return PregelNode(**attrs) def get_writers(self) -> list[Runnable]: """Get writers with optimizations applied.""" @@ -145,38 +180,6 @@ class PregelNode(RunnableBindingBase): else: return self.bound - def __init__( - self, - *, - channels: Union[list[str], Mapping[str, str]], - triggers: Sequence[str], - mapper: Optional[Callable[[Any], Any]] = None, - writers: Optional[list[Runnable]] = None, - tags: Optional[list[str]] = None, - metadata: Optional[Mapping[str, Any]] = None, - bound: Optional[Runnable[Any, Any]] = None, - kwargs: Optional[Mapping[str, Any]] = None, - config: Optional[RunnableConfig] = None, - retry_policy: Optional[RetryPolicy] = None, - **other_kwargs: Any, - ) -> None: - super().__init__( - channels=channels, - triggers=triggers, - mapper=mapper, - writers=writers or [], - bound=bound or DEFAULT_BOUND, - kwargs=kwargs or {}, - retry_policy=retry_policy, - config=merge_configs( - config, {"tags": tags or [], "metadata": metadata or {}} - ), - **other_kwargs, - ) - - def __repr_args__(self) -> Any: - return [(k, v) for k, v in super().__repr_args__() if k != "bound"] - def join(self, channels: Sequence[str]) -> PregelNode: assert isinstance(channels, list) or isinstance( channels, tuple @@ -226,3 +229,42 @@ class PregelNode(RunnableBindingBase): ], ) -> RunnableSerializable: raise NotImplementedError() + + def invoke( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> Output: + return self.bound.invoke(input, merge_configs(self.config, config), **kwargs) + + async def ainvoke( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> Output: + return await self.bound.ainvoke( + input, merge_configs(self.config, config), **kwargs + ) + + def stream( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> Iterator[Output]: + yield from self.bound.stream( + input, merge_configs(self.config, config), **kwargs + ) + + async def astream( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> AsyncIterator[Output]: + async for item in self.bound.astream( + input, merge_configs(self.config, config), **kwargs + ): + yield item diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index 3c3dd49d0..5ed4cf469 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -3300,11 +3300,7 @@ 'type': 'string', }), }), - 'required': list([ - 'query', - 'inner', - ]), - 'title': 'Input', + 'title': 'LangGraphInput', 'type': 'object', }) # --- @@ -3323,11 +3319,7 @@ 'type': 'array', }), }), - 'required': list([ - 'answer', - 'docs', - ]), - 'title': 'Output', + 'title': 'LangGraphOutput', 'type': 'object', }) # --- @@ -3370,11 +3362,7 @@ 'type': 'string', }), }), - 'required': list([ - 'query', - 'inner', - ]), - 'title': 'Input', + 'title': 'LangGraphInput', 'type': 'object', }) # --- @@ -3393,11 +3381,7 @@ 'type': 'array', }), }), - 'required': list([ - 'answer', - 'docs', - ]), - 'title': 'Output', + 'title': 'LangGraphOutput', 'type': 'object', }) # --- @@ -3440,11 +3424,7 @@ 'type': 'string', }), }), - 'required': list([ - 'query', - 'inner', - ]), - 'title': 'Input', + 'title': 'LangGraphInput', 'type': 'object', }) # --- @@ -3463,11 +3443,7 @@ 'type': 'array', }), }), - 'required': list([ - 'answer', - 'docs', - ]), - 'title': 'Output', + 'title': 'LangGraphOutput', 'type': 'object', }) # --- @@ -3510,11 +3486,7 @@ 'type': 'string', }), }), - 'required': list([ - 'query', - 'inner', - ]), - 'title': 'Input', + 'title': 'LangGraphInput', 'type': 'object', }) # --- @@ -3533,11 +3505,7 @@ 'type': 'array', }), }), - 'required': list([ - 'answer', - 'docs', - ]), - 'title': 'Output', + 'title': 'LangGraphOutput', 'type': 'object', }) # --- @@ -3580,11 +3548,7 @@ 'type': 'string', }), }), - 'required': list([ - 'query', - 'inner', - ]), - 'title': 'Input', + 'title': 'LangGraphInput', 'type': 'object', }) # --- @@ -3603,11 +3567,7 @@ 'type': 'array', }), }), - 'required': list([ - 'answer', - 'docs', - ]), - 'title': 'Output', + 'title': 'LangGraphOutput', 'type': 'object', }) # --- diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 3ca3906d1..6109f6ce4 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -1903,7 +1903,7 @@ def test_invoke_two_processes_no_in(mocker: MockerFixture) -> None: one = Channel.subscribe_to("between") | add_one | Channel.write_to("output") two = Channel.subscribe_to("between") | add_one - with pytest.raises(ValueError): + with pytest.raises(TypeError): Pregel(nodes={"one": one, "two": two}) @@ -9784,6 +9784,7 @@ def test_doubly_nested_graph_state( ] +@pytest.mark.repeat(10) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_send_to_nested_graphs( request: pytest.FixtureRequest, checkpointer_name: str @@ -9943,10 +9944,12 @@ def test_send_to_nested_graphs( graph.update_state(outer_state.tasks[1].state, {"subject": "turtles - hohoho"}) # continue past interrupt - assert graph.invoke(None, config=config) == { - "subjects": ["cats", "dogs"], - "jokes": ["Joke about cats - hohoho", "Joke about turtles - hohoho"], - } + assert sorted( + graph.stream(None, config=config), key=lambda d: d["generate_joke"]["jokes"][0] + ) == [ + {"generate_joke": {"jokes": ["Joke about cats - hohoho"]}}, + {"generate_joke": {"jokes": ["Joke about turtles - hohoho"]}}, + ] actual_snapshot = graph.get_state(config) expected_snapshot = StateSnapshot( From 4793b3f5e1c690a29b0773e36a4fe37bc53b2b78 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Mon, 2 Sep 2024 19:40:02 -0700 Subject: [PATCH 238/287] Use a lock for all operations on sqlite checkpointer - Otherwise when used in multiple subgraphs in parallel separate queries can interfere w each other --- .../langgraph/checkpoint/sqlite/__init__.py | 21 ++++++++++--------- .../langgraph/checkpoint/sqlite/aio.py | 10 +++++---- libs/langgraph/tests/test_pregel.py | 1 - 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py index a5f657824..31bffd19c 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/__init__.py @@ -165,14 +165,15 @@ class SqliteSaver(BaseCheckpointSaver): Yields: sqlite3.Cursor: A cursor for the SQLite database. """ - self.setup() - cur = self.conn.cursor() - try: - yield cur - finally: - if transaction: - self.conn.commit() - cur.close() + with self.lock: + self.setup() + cur = self.conn.cursor() + try: + yield cur + finally: + if transaction: + self.conn.commit() + cur.close() def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]: """Get a checkpoint tuple from the database. @@ -398,7 +399,7 @@ class SqliteSaver(BaseCheckpointSaver): checkpoint_ns = config["configurable"]["checkpoint_ns"] type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint) serialized_metadata = self.jsonplus_serde.dumps(metadata) - with self.lock, self.cursor() as cur: + with self.cursor() as cur: cur.execute( "INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)", ( @@ -434,7 +435,7 @@ class SqliteSaver(BaseCheckpointSaver): writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair. task_id (str): Identifier for the task creating the writes. """ - with self.lock, self.cursor() as cur: + with self.cursor() as cur: cur.executemany( "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [ diff --git a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py index 7cdc7c8fb..28ad76109 100644 --- a/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py +++ b/libs/checkpoint-sqlite/langgraph/checkpoint/sqlite/aio.py @@ -276,7 +276,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver): """ await self.setup() checkpoint_ns = config["configurable"].get("checkpoint_ns", "") - async with self.conn.cursor() as cur: + async with self.lock, self.conn.cursor() as cur: # find the latest checkpoint for the thread_id if checkpoint_id := get_checkpoint_id(config): await cur.execute( @@ -371,7 +371,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver): ORDER BY checkpoint_id DESC""" if limit: query += f" LIMIT {limit}" - async with self.conn.execute(query, params) as cur, self.conn.cursor() as wcur: + async with self.lock, self.conn.execute( + query, params + ) as cur, self.conn.cursor() as wcur: async for ( thread_id, checkpoint_ns, @@ -438,7 +440,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver): checkpoint_ns = config["configurable"]["checkpoint_ns"] type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint) serialized_metadata = self.jsonplus_serde.dumps(metadata) - async with self.conn.execute( + async with self.lock, self.conn.execute( "INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)", ( str(config["configurable"]["thread_id"]), @@ -475,7 +477,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver): task_id (str): Identifier for the task creating the writes. """ await self.setup() - async with self.conn.cursor() as cur: + async with self.lock, self.conn.cursor() as cur: await cur.executemany( "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [ diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 6109f6ce4..96696fa13 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -9784,7 +9784,6 @@ def test_doubly_nested_graph_state( ] -@pytest.mark.repeat(10) @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC) def test_send_to_nested_graphs( request: pytest.FixtureRequest, checkpointer_name: str From a99b66ae3f1d08a848aa4e0eb710744ee465f5e6 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 3 Sep 2024 09:13:21 -0700 Subject: [PATCH 239/287] Update --- libs/langgraph/langgraph/pregel/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index c5fa760c5..d7c265b2d 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -238,7 +238,7 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): interrupt_before_nodes: Union[All, Sequence[str]] = (), input_channels: Union[str, Sequence[str]], step_timeout: Optional[float] = None, - debug: bool = False, + debug: Optional[bool] = None, checkpointer: Optional[BaseCheckpointSaver] = None, store: Optional[BaseStore] = None, retry_policy: Optional[RetryPolicy] = None, From 4ddb4ae5a037b329cd820a40453aeaaff2b61644 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 3 Sep 2024 09:26:45 -0700 Subject: [PATCH 240/287] Update --- .../tests/__snapshots__/test_pregel.ambr | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index 5ed4cf469..3937546f5 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -3300,6 +3300,10 @@ 'type': 'string', }), }), + 'required': list([ + 'query', + 'inner', + ]), 'title': 'LangGraphInput', 'type': 'object', }) @@ -3362,6 +3366,10 @@ 'type': 'string', }), }), + 'required': list([ + 'query', + 'inner', + ]), 'title': 'LangGraphInput', 'type': 'object', }) @@ -3424,6 +3432,10 @@ 'type': 'string', }), }), + 'required': list([ + 'query', + 'inner', + ]), 'title': 'LangGraphInput', 'type': 'object', }) @@ -3486,6 +3498,10 @@ 'type': 'string', }), }), + 'required': list([ + 'query', + 'inner', + ]), 'title': 'LangGraphInput', 'type': 'object', }) @@ -3548,6 +3564,10 @@ 'type': 'string', }), }), + 'required': list([ + 'query', + 'inner', + ]), 'title': 'LangGraphInput', 'type': 'object', }) From 5d1ab66535814d7b7787f75f0fb2794ae5c30436 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 3 Sep 2024 09:41:04 -0700 Subject: [PATCH 241/287] sqlite 1.0.3 --- libs/checkpoint-sqlite/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint-sqlite/pyproject.toml b/libs/checkpoint-sqlite/pyproject.toml index 9d8818619..1b9462085 100644 --- a/libs/checkpoint-sqlite/pyproject.toml +++ b/libs/checkpoint-sqlite/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-checkpoint-sqlite" -version = "1.0.2" +version = "1.0.3" description = "Library with a SQLite implementation of LangGraph checkpoint saver." authors = [] license = "MIT" From 1ae60dccc5ff8183b87cf13a08e1f6a65d4192f5 Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:12:36 -0700 Subject: [PATCH 242/287] test (#1592) --- libs/sdk-js/src/client.mts | 32 ++++++++++++++++++-------------- libs/sdk-js/src/types.mts | 30 ++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/libs/sdk-js/src/client.mts b/libs/sdk-js/src/client.mts index 03024299d..459e9e6b1 100644 --- a/libs/sdk-js/src/client.mts +++ b/libs/sdk-js/src/client.mts @@ -133,6 +133,7 @@ export class CronsClient extends BaseClient { interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, webhook: payload?.webhook, + multitask_strategy: payload?.multitaskStrategy, }; return this.fetch(`/threads/${threadId}/runs/crons`, { method: "POST", @@ -159,6 +160,7 @@ export class CronsClient extends BaseClient { interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, webhook: payload?.webhook, + multitask_strategy: payload?.multitaskStrategy, }; return this.fetch(`/runs/crons`, { method: "POST", @@ -237,6 +239,8 @@ export class AssistantsClient extends BaseClient { graphId: string; config?: Config; metadata?: Metadata; + assistantId?: string; + ifExists?: OnConflictBehavior; }): Promise { return this.fetch("/assistants", { method: "POST", @@ -244,6 +248,8 @@ export class AssistantsClient extends BaseClient { graph_id: payload.graphId, config: payload.config, metadata: payload.metadata, + assistant_id: payload.assistantId, + if_exists: payload.ifExists, }, }); } @@ -257,7 +263,7 @@ export class AssistantsClient extends BaseClient { async update( assistantId: string, payload: { - graphId: string; + graphId?: string; config?: Config; metadata?: Metadata; }, @@ -518,7 +524,7 @@ export class RunsClient extends BaseClient { stream( threadId: null, assistantId: string, - payload?: Omit, + payload?: Omit, ): AsyncGenerator<{ event: StreamEvent; data: any; @@ -546,8 +552,6 @@ export class RunsClient extends BaseClient { payload?: RunsStreamPayload, ): AsyncGenerator<{ event: StreamEvent; - // TODO: figure out a better way to - // type this without any data: any; }> { const json: Record = { @@ -560,10 +564,11 @@ export class RunsClient extends BaseClient { interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, checkpoint_id: payload?.checkpointId, + webhook: payload?.webhook, + multitask_strategy: payload?.multitaskStrategy, + on_completion: payload?.onCompletion, + on_disconnect: payload?.onDisconnect, }; - if (payload?.multitaskStrategy != null) { - json["multitask_strategy"] = payload?.multitaskStrategy; - } const endpoint = threadId == null ? `/runs/stream` : `/threads/${threadId}/runs/stream`; @@ -640,10 +645,8 @@ export class RunsClient extends BaseClient { interrupt_after: payload?.interruptAfter, webhook: payload?.webhook, checkpoint_id: payload?.checkpointId, + multitask_strategy: payload?.multitaskStrategy, }; - if (payload?.multitaskStrategy != null) { - json["multitask_strategy"] = payload?.multitaskStrategy; - } return this.fetch(`/threads/${threadId}/runs`, { method: "POST", json, @@ -654,7 +657,7 @@ export class RunsClient extends BaseClient { async wait( threadId: null, assistantId: string, - payload?: Omit, + payload?: Omit, ): Promise; async wait( @@ -684,10 +687,11 @@ export class RunsClient extends BaseClient { interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, checkpoint_id: payload?.checkpointId, + webhook: payload?.webhook, + multitask_strategy: payload?.multitaskStrategy, + on_completion: payload?.onCompletion, + on_disconnect: payload?.onDisconnect, }; - if (payload?.multitaskStrategy != null) { - json["multitask_strategy"] = payload?.multitaskStrategy; - } const endpoint = threadId == null ? `/runs/wait` : `/threads/${threadId}/runs/wait`; return this.fetch(endpoint, { diff --git a/libs/sdk-js/src/types.mts b/libs/sdk-js/src/types.mts index 872cc5723..2a5d6c4f1 100644 --- a/libs/sdk-js/src/types.mts +++ b/libs/sdk-js/src/types.mts @@ -3,6 +3,8 @@ import { Config, Metadata } from "./schema.js"; export type StreamMode = "values" | "messages" | "updates" | "events" | "debug"; export type MultitaskStrategy = "reject" | "interrupt" | "rollback" | "enqueue"; export type OnConflictBehavior = "raise" | "do_nothing"; +export type OnCompletionBehavior = "complete" | "continue"; +export type DisconnectMode = "cancel" | "continue"; export type StreamEvent = | "events" | "metadata" @@ -61,6 +63,27 @@ interface RunsInvokePayload { * Abort controller signal to cancel the run. */ signal?: AbortController["signal"]; + + /** + * Behavior to handle run completion. Only relevant if + * there is a pending/inflight run on the same thread. One of: + * - "complete": Complete the run. + * - "continue": Continue the run. + */ + onCompletion?: OnCompletionBehavior; + + /** + * Webhook to call when the run is complete. + */ + webhook?: string; + + /** + * Behavior to handle disconnection. Only relevant if + * there is a pending/inflight run on the same thread. One of: + * - "cancel": Cancel the run. + * - "continue": Continue the run. + */ + onDisconnect?: DisconnectMode; } export interface RunsStreamPayload extends RunsInvokePayload { @@ -82,12 +105,7 @@ export interface RunsStreamPayload extends RunsInvokePayload { feedbackKeys?: string[]; } -export interface RunsCreatePayload extends RunsInvokePayload { - /** - * Webhook to call when the run is complete. - */ - webhook?: string; -} +export interface RunsCreatePayload extends RunsInvokePayload {} export interface CronsCreatePayload extends RunsCreatePayload { /** From ec51760f5715affcf997a1f464f97de1f1079066 Mon Sep 17 00:00:00 2001 From: isaac hershenson Date: Tue, 3 Sep 2024 13:45:22 -0700 Subject: [PATCH 243/287] x --- libs/langgraph/tests/test_pregel_async.py | 88 +++++++++++------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 3da3b0886..d8747bd0a 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -5491,17 +5491,17 @@ async def test_branch_then(checkpointer_name: str) -> None: "triggers": ["start:prepare"], }, }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": AnyStr(), - "name": "prepare", - "result": [("my_key", " prepared")], - "error": None, - "interrupts": [], + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": AnyStr(), + "name": "prepare", + "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], + }, }, { "type": "checkpoint", @@ -5546,17 +5546,17 @@ async def test_branch_then(checkpointer_name: str) -> None: "triggers": ["branch:prepare:condition:tool_two_slow"], }, }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 2, - "payload": { - "id": AnyStr(), - "name": "tool_two_slow", - "result": [("my_key", " slow")], - "error": None, - "interrupts": [], + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": AnyStr(), + "name": "tool_two_slow", + "result": [("my_key", " slow")], + "error": None, + "interrupts": [], + }, }, { "type": "checkpoint", @@ -5599,17 +5599,17 @@ async def test_branch_then(checkpointer_name: str) -> None: "triggers": ["branch:prepare:condition::then"], }, }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 3, - "payload": { - "id": AnyStr(), - "name": "finish", - "result": [("my_key", " finished")], - "error": None, - "interrupts": [], + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": AnyStr(), + "name": "finish", + "result": [("my_key", " finished")], + "error": None, + "interrupts": [], + }, }, { "type": "checkpoint", @@ -5728,17 +5728,17 @@ async def test_branch_then(checkpointer_name: str) -> None: "triggers": ["start:prepare"], }, }, - }, - { - "type": "task_result", - "timestamp": AnyStr(), - "step": 1, - "payload": { - "id": AnyStr(), - "name": "prepare", - "result": [("my_key", " prepared")], - "error": None, - "interrupts": [], + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": AnyStr(), + "name": "prepare", + "result": [("my_key", " prepared")], + "error": None, + "interrupts": [], + }, }, { "type": "checkpoint", From e15019349181da6b71980a74a62c1efafc1c35fc Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 3 Sep 2024 15:13:37 -0700 Subject: [PATCH 244/287] Avoid deepcopy --- libs/langgraph/tests/test_pregel.py | 6 ++---- libs/langgraph/tests/test_pregel_async.py | 11 +++++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 0a41b3dbc..3f0b2a2b9 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -1957,8 +1957,6 @@ def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: def test_conditional_graph( snapshot: SnapshotAssertion, request: pytest.FixtureRequest, checkpointer_name: str ) -> None: - from copy import deepcopy - from langchain_core.agents import AgentAction, AgentFinish from langchain_core.language_models.fake import FakeStreamingListLLM from langchain_core.prompts import PromptTemplate @@ -2000,6 +1998,7 @@ def test_conditional_graph( # Define tool execution logic def execute_tools(data: dict) -> dict: + data = data.copy() agent_action: AgentAction = data.pop("agent_outcome") observation = {t.name: t for t in tools}[agent_action.tool].invoke( agent_action.tool_input @@ -2066,8 +2065,7 @@ def test_conditional_graph( ), } - # deepcopy because the nodes mutate the data - assert [deepcopy(c) for c in app.stream({"input": "what is weather in sf"})] == [ + assert [c for c in app.stream({"input": "what is weather in sf"})] == [ { "agent": { "input": "what is weather in sf", diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index d8747bd0a..dd89901b8 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -514,6 +514,7 @@ async def test_cancel_graph_astream_events_v2(checkpointer_name: Optional[str]) if chunk["event"] == "on_chain_stream" and not chunk["parent_ids"]: got_event = True assert chunk["data"]["chunk"] == {"alittlewhile": {"value": 2}} + await asyncio.sleep(0.1) break # did break @@ -2204,8 +2205,6 @@ async def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_conditional_graph(checkpointer_name: str) -> None: - from copy import deepcopy - from langchain_core.agents import AgentAction, AgentFinish from langchain_core.language_models.fake import FakeStreamingListLLM from langchain_core.prompts import PromptTemplate @@ -2243,12 +2242,15 @@ async def test_conditional_graph(checkpointer_name: str) -> None: # Define tool execution logic async def execute_tools(data: dict) -> dict: + data = data.copy() agent_action: AgentAction = data.pop("agent_outcome") observation = await {t.name: t for t in tools}[agent_action.tool].ainvoke( agent_action.tool_input ) if data.get("intermediate_steps") is None: data["intermediate_steps"] = [] + else: + data["intermediate_steps"] = data["intermediate_steps"].copy() data["intermediate_steps"].append([agent_action, observation]) return data @@ -2301,10 +2303,7 @@ async def test_conditional_graph(checkpointer_name: str) -> None: ), } - # deepcopy because the nodes mutate the data - assert [ - deepcopy(c) async for c in app.astream({"input": "what is weather in sf"}) - ] == [ + assert [c async for c in app.astream({"input": "what is weather in sf"})] == [ { "agent": { "input": "what is weather in sf", From b99a101b2c0c807ac2ed13bb0e426c7f64e46e2f Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 3 Sep 2024 15:34:11 -0700 Subject: [PATCH 245/287] Add large parallel graph test --- libs/langgraph/tests/test_pregel_async.py | 68 ++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index dd89901b8..7707e2405 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1,6 +1,7 @@ import asyncio import json import operator +import random import re import sys from collections import Counter @@ -514,7 +515,6 @@ async def test_cancel_graph_astream_events_v2(checkpointer_name: Optional[str]) if chunk["event"] == "on_chain_stream" and not chunk["parent_ids"]: got_event = True assert chunk["data"]["chunk"] == {"alittlewhile": {"value": 2}} - await asyncio.sleep(0.1) break # did break @@ -8431,6 +8431,72 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None: ] +@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) +async def test_large_graph(checkpointer_name: str) -> None: + class OverallState(TypedDict): + subjects: list[str] + jokes: Annotated[list[str], operator.add] + + async def continue_to_jokes(state: OverallState): + return [Send("generate_joke", {"subject": s}) for s in state["subjects"]] + + class JokeInput(TypedDict): + subject: str + + class JokeOutput(TypedDict): + jokes: list[str] + + async def edit(state: JokeInput): + subject = state["subject"] + return {"subject": f"{subject} - hohoho"} + + # subgraph + subgraph = StateGraph(input=JokeInput, output=JokeOutput) + subgraph.add_node("edit", edit) + subgraph.add_node( + "generate", lambda state: {"jokes": [f"Joke about {state['subject']}"]} + ) + subgraph.add_node( + "bump", lambda state: {"jokes": [state["jokes"][0] + " a"]}, input=JokeOutput + ) + subgraph.set_entry_point("edit") + subgraph.add_edge("edit", "generate") + subgraph.add_edge("generate", "bump") + subgraph.add_conditional_edges( + "bump", lambda state: END if state["jokes"][0].endswith(" a" * 10) else "bump" + ) + subgraph.set_finish_point("generate") + + # parent graph + builder = StateGraph(OverallState) + builder.add_node("generate_joke", subgraph.compile()) + builder.add_conditional_edges(START, continue_to_jokes) + builder.add_edge("generate_joke", END) + + async with awith_checkpointer(checkpointer_name) as checkpointer: + graph = builder.compile(checkpointer=checkpointer) + config = {"configurable": {"thread_id": "1"}} + + # invoke and pause at nested interrupt + assert ( + len( + [ + c + async for c in graph.astream( + { + "subjects": [ + random.choice("abcdefghijklmnopqrstuvwxyz") + for _ in range(1000) + ] + }, + config=config, + ) + ] + ) + == 1000 + ) + + @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_send_to_nested_graphs(checkpointer_name: str) -> None: class OverallState(TypedDict): From 0d51c8614daf9cb61acb96b4049c6cba6102d4f2 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 3 Sep 2024 16:27:59 -0700 Subject: [PATCH 246/287] Move runner code to standalone class, use in Pregel.stream/astream --- libs/langgraph/langgraph/pregel/__init__.py | 268 +++----------------- libs/langgraph/langgraph/pregel/loop.py | 111 +++++--- libs/langgraph/langgraph/pregel/runner.py | 193 ++++++++++++++ libs/langgraph/tests/test_pregel_async.py | 67 ----- 4 files changed, 304 insertions(+), 335 deletions(-) create mode 100644 libs/langgraph/langgraph/pregel/runner.py diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index d7c265b2d..93eb1a07d 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -1,8 +1,6 @@ from __future__ import annotations import asyncio -import concurrent.futures -import time from collections import deque from functools import partial from typing import ( @@ -66,12 +64,11 @@ from langgraph.constants import ( CONFIG_KEY_SEND, CONFIG_KEY_STREAM, CONFIG_KEY_TASK_ID, - ERROR, INTERRUPT, NS_END, NS_SEP, ) -from langgraph.errors import GraphInterrupt, GraphRecursionError, InvalidUpdateError +from langgraph.errors import GraphRecursionError, InvalidUpdateError from langgraph.managed.base import ManagedValueSpec from langgraph.pregel.algo import ( apply_writes, @@ -80,17 +77,13 @@ from langgraph.pregel.algo import ( prepare_next_tasks, ) from langgraph.pregel.config import patch_checkpoint_map, patch_configurable -from langgraph.pregel.debug import ( - print_step_checkpoint, - print_step_tasks, - print_step_writes, - tasks_w_writes, -) +from langgraph.pregel.debug import tasks_w_writes from langgraph.pregel.io import read_channels from langgraph.pregel.loop import AsyncPregelLoop, SyncPregelLoop from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager from langgraph.pregel.read import PregelNode -from langgraph.pregel.retry import RetryPolicy, arun_with_retry, run_with_retry +from langgraph.pregel.retry import RetryPolicy +from langgraph.pregel.runner import PregelRunner from langgraph.pregel.types import ( All, PregelExecutableTask, @@ -1170,9 +1163,11 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): ``` """ + stream = deque() + def output() -> Iterator: - while loop.stream: - ns, mode, payload = loop.stream.popleft() + while stream: + ns, mode, payload = stream.popleft() if mode in stream_modes: if subgraphs and isinstance(stream_mode, list): yield (tuple(ns.split(NS_SEP)) if ns else (), mode, payload) @@ -1217,6 +1212,7 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): with SyncPregelLoop( input, + stream=stream.append, config=config, store=self.store, checkpointer=checkpointer, @@ -1224,7 +1220,14 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): specs=self.channels, output_keys=output_keys, stream_keys=self.stream_channels_asis, + debug=debug, ) as loop: + # create runner + runner = PregelRunner( + submit=loop.submit, + put_writes=loop.put_writes, + ) + # enable subgraph streaming if subgraphs: loop.config["configurable"][CONFIG_KEY_STREAM] = loop.stream # Similarly to Bulk Synchronous Parallel / Pregel model @@ -1238,85 +1241,14 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): interrupt_after=interrupt_after, manager=run_manager, ): - # debug flag - if debug: - print_step_checkpoint( - loop.checkpoint_metadata, - loop.channels, - self.stream_channels_list, - ) - # emit output - yield from output() - # debug flag - if debug: - print_step_tasks(loop.step, loop.tasks) - - # execute tasks, and wait for one to fail or all to finish. - # each task is independent from all other concurrent tasks - # yield updates/debug output as each task finishes - futures = { - loop.submit( - run_with_retry, - task, - self.retry_policy, - ): task - for task in loop.tasks - if not task.writes - } - all_futures = futures.copy() - end_time = ( - self.step_timeout + time.monotonic() - if self.step_timeout - else None - ) - if not futures: - done, inflight = set(), set() - while futures: - done, inflight = concurrent.futures.wait( - futures, - return_when=concurrent.futures.FIRST_COMPLETED, - timeout=( - max(0, end_time - time.monotonic()) - if end_time - else None - ), - ) - if not done: - break # timed out - for fut in done: - task = futures.pop(fut) - if exc := _exception(fut): - # save error to checkpointer - if isinstance(exc, GraphInterrupt): - loop.put_writes( - task.id, [(INTERRUPT, i) for i in exc.args[0]] - ) - else: - loop.put_writes(task.id, [(ERROR, exc)]) - - else: - # save task writes to checkpointer - loop.put_writes(task.id, task.writes) - else: - # remove references to loop vars - del fut, task + for _ in runner.tick( + loop.tasks, + timeout=self.step_timeout, + retry_policy=self.retry_policy, + ): # emit output - yield from output() - # maybe stop other tasks - if _should_stop_others(done): - break - - # panic on failure or timeout - _panic_or_proceed(all_futures, loop.step) - # don't keep futures around in memory longer than needed - del done, inflight, futures - # debug flag - if debug: - print_step_writes( - loop.step, - [w for t in loop.tasks for w in t.writes], - self.stream_channels_list, - ) + for o in output(): + yield o # emit output yield from output() # handle exit @@ -1412,9 +1344,11 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): ``` """ + stream = deque() + def output() -> Iterator: - while loop.stream: - ns, mode, payload = loop.stream.popleft() + while stream: + ns, mode, payload = stream.popleft() if mode in stream_modes: if subgraphs and isinstance(stream_mode, list): yield (tuple(ns.split(NS_SEP)) if ns else (), mode, payload) @@ -1467,6 +1401,7 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): ) async with AsyncPregelLoop( input, + stream=stream.append, config=config, store=self.store, checkpointer=checkpointer, @@ -1475,9 +1410,15 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): output_keys=output_keys, stream_keys=self.stream_channels_asis, ) as loop: + # create runner + runner = PregelRunner( + submit=loop.submit, + put_writes=loop.put_writes, + use_astream=do_stream is not None, + ) + # enable subgraph streaming if subgraphs: loop.config["configurable"][CONFIG_KEY_STREAM] = loop.stream - aioloop = asyncio.get_event_loop() # Similarly to Bulk Synchronous Parallel / Pregel model # computation proceeds in steps, while there are channel updates # channel updates from step N are only visible in step N+1 @@ -1489,89 +1430,14 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): interrupt_after=interrupt_after, manager=run_manager, ): - # debug flag - if debug: - print_step_checkpoint( - loop.checkpoint_metadata, - loop.channels, - self.stream_channels_list, - ) - # emit output - for o in output(): - yield o - # debug flag - if debug: - print_step_tasks(loop.step, loop.tasks) - - # execute tasks, and wait for one to fail or all to finish. - # each task is independent from all other concurrent tasks - # yield updates/debug output as each task finishes - futures = { - loop.submit( - arun_with_retry, - task, - self.retry_policy, - stream=do_stream, - __name__=task.name, - __cancel_on_exit__=True, - ): task - for task in loop.tasks - if not task.writes - } - all_futures = futures.copy() - end_time = ( - self.step_timeout + aioloop.time() - if self.step_timeout - else None - ) - if not futures: - done, inflight = set(), set() - while futures: - done, inflight = await asyncio.wait( - futures, - return_when=asyncio.FIRST_COMPLETED, - timeout=( - max(0, end_time - aioloop.time()) if end_time else None - ), - ) - if not done: - break # timed out - - for fut in done: - task = futures.pop(fut) - if exc := _exception(fut): - # save error to checkpointer - if isinstance(exc, GraphInterrupt): - loop.put_writes( - task.id, [(INTERRUPT, i) for i in exc.args[0]] - ) - else: - loop.put_writes(task.id, [(ERROR, exc)]) - - else: - # save task writes to checkpointer - loop.put_writes(task.id, task.writes) - else: - # remove references to loop vars - del fut, task + async for _ in runner.atick( + loop.tasks, + timeout=self.step_timeout, + retry_policy=self.retry_policy, + ): # emit output for o in output(): yield o - # maybe stop other tasks - if _should_stop_others(done): - break - - # panic on failure or timeout - _panic_or_proceed(all_futures, loop.step, asyncio.TimeoutError) - # don't keep futures around in memory longer than needed - del done, inflight, futures - # debug flag - if debug: - print_step_writes( - loop.step, - [w for t in loop.tasks for w in t.writes], - self.stream_channels_list, - ) # emit output for o in output(): yield o @@ -1692,57 +1558,3 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): return latest else: return chunks - - -def _should_stop_others( - done: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], -) -> bool: - for fut in done: - if fut.cancelled(): - return True - if exc := fut.exception(): - return not isinstance(exc, GraphInterrupt) - else: - return False - - -def _exception( - fut: Union[concurrent.futures.Future[Any], asyncio.Task[Any]], -) -> Optional[BaseException]: - if fut.cancelled(): - if isinstance(fut, asyncio.Task): - return asyncio.CancelledError() - else: - return concurrent.futures.CancelledError() - else: - return fut.exception() - - -def _panic_or_proceed( - futs: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], - step: int, - timeout_exc_cls: Type[Exception] = TimeoutError, -) -> None: - done: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set() - inflight: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set() - for fut in futs: - if fut.done(): - done.add(fut) - else: - inflight.add(fut) - while done: - # if any task failed - if exc := _exception(done.pop()): - # cancel all pending tasks - while inflight: - inflight.pop().cancel() - # raise the exception - raise exc - - if inflight: - # if we got here means we timed out - while inflight: - # cancel all pending tasks - inflight.pop().cancel() - # raise timeout error - raise timeout_exc_cls(f"Timed out at step {step}") diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 3e55d0df8..c903730dd 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -2,7 +2,6 @@ import asyncio import concurrent.futures from collections import deque from contextlib import AsyncExitStack, ExitStack -from itertools import tee from types import TracebackType from typing import ( Any, @@ -65,6 +64,9 @@ from langgraph.pregel.debug import ( map_debug_checkpoint, map_debug_task_results, map_debug_tasks, + print_step_checkpoint, + print_step_tasks, + print_step_writes, ) from langgraph.pregel.executor import ( AsyncBackgroundExecutor, @@ -92,24 +94,16 @@ EMPTY_SEQ = () class StreamProtocol(Protocol): - def extend(self, values: Iterable[Tuple[str, str, Any]]) -> None: ... - def popleft(self) -> Tuple[str, str, Any]: ... - def __bool__(self) -> bool: ... + def __call__(self, values: Iterable[Tuple[str, str, Any]]) -> None: ... class DuplexStream(StreamProtocol): - def __init__(self, *streams: StreamProtocol) -> None: - self.streams = streams + def __init__(self, *queues: StreamProtocol) -> None: + self.queues = queues - def extend(self, values: Iterable[Tuple[str, str, Any]]) -> None: - for stream, vv in zip(self.streams, tee(values, len(self.streams))): - stream.extend(vv) - - def popleft(self) -> Tuple[str, str, Any]: - return self.streams[0].popleft() - - def __bool__(self) -> bool: - return bool(self.streams[0]) + def __call__(self, value: Tuple[str, str, Any]) -> None: + for queue in self.queues: + queue(value) class PregelLoop: @@ -121,8 +115,9 @@ class PregelLoop: specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]] output_keys: Union[str, Sequence[str]] stream_keys: Union[str, Sequence[str]] - is_nested: bool + stream: Optional[StreamProtocol] skip_done_tasks: bool + is_nested: bool checkpointer_get_next_version: Callable[[Optional[V]], V] checkpointer_put_writes: Optional[ @@ -154,7 +149,6 @@ class PregelLoop: "pending", "done", "interrupt_before", "interrupt_after", "out_of_steps" ] tasks: Sequence[PregelExecutableTask] - stream: StreamProtocol output: Union[None, dict[str, Any], Any] = None # public @@ -163,6 +157,7 @@ class PregelLoop: self, input: Optional[Any], *, + stream: Optional[StreamProtocol], config: RunnableConfig, store: Optional[BaseStore], checkpointer: Optional[BaseCheckpointSaver], @@ -170,8 +165,9 @@ class PregelLoop: specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], output_keys: Union[str, Sequence[str]], stream_keys: Union[str, Sequence[str]], + debug: bool = False, ) -> None: - self.stream = deque() + self.stream = stream self.input = input self.config = config self.store = store @@ -182,6 +178,7 @@ class PregelLoop: self.stream_keys = stream_keys self.is_nested = CONFIG_KEY_TASK_ID in self.config.get("configurable", {}) self.skip_done_tasks = "checkpoint_id" not in config["configurable"] + self.debug = debug if CONFIG_KEY_STREAM in config["configurable"]: self.stream = DuplexStream( self.stream, config["configurable"][CONFIG_KEY_STREAM] @@ -229,24 +226,6 @@ class PregelLoop: ) self._output_writes(task_id, writes) - def _output_writes( - self, task_id: str, writes: Sequence[tuple[str, Any]], *, cached: bool = False - ) -> None: - if task := next((t for t in self.tasks if t.id == task_id), None): - self.stream.extend( - (self.config["configurable"].get("checkpoint_ns", ""), "updates", v) - for v in map_output_updates(self.output_keys, [(task, writes)], cached) - ) - if not cached: - self.stream.extend( - (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) - for v in map_debug_task_results( - self.step, - [(task, writes)], - self.stream_keys, - ) - ) - def tick( self, *, @@ -265,6 +244,15 @@ class PregelLoop: self._first(input_keys=input_keys) elif all(task.writes for task in self.tasks): writes = [w for t in self.tasks for w in t.writes] + # debug flag + if self.debug: + print_step_writes( + self.step, + writes, + [self.stream_keys] + if isinstance(self.stream_keys, str) + else self.stream_keys, + ) # all tasks have finished mv_writes = apply_writes( self.checkpoint, @@ -276,7 +264,7 @@ class PregelLoop: for key, values in mv_writes.items(): self._update_mv(key, values) # produce values output - self.stream.extend( + self._emit( (self.config["configurable"].get("checkpoint_ns", ""), "values", v) for v in map_output_values(self.output_keys, writes, self.channels) ) @@ -324,7 +312,7 @@ class PregelLoop: # produce debug output if self._checkpointer_put_after_previous is not None: - self.stream.extend( + self._emit( (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) for v in map_debug_checkpoint( self.step - 1, # printing checkpoint for previous step @@ -373,11 +361,15 @@ class PregelLoop: return False # produce debug output - self.stream.extend( + self._emit( (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) for v in map_debug_tasks(self.step, self.tasks) ) + # debug flag + if self.debug: + print_step_tasks(self.step, self.tasks) + return True # private @@ -399,7 +391,7 @@ class PregelLoop: version = self.checkpoint["channel_versions"][k] self.checkpoint["versions_seen"][INTERRUPT][k] = version # produce values output - self.stream.extend( + self._emit( (self.config["configurable"].get("checkpoint_ns", ""), "values", v) for v in map_output_values(self.output_keys, True, self.channels) ) @@ -436,6 +428,15 @@ class PregelLoop: metadata["parents"] = self.config["configurable"].get( CONFIG_KEY_CHECKPOINT_MAP, {} ) + # debug flag + if self.debug: + print_step_checkpoint( + metadata, + self.channels, + [self.stream_keys] + if isinstance(self.stream_keys, str) + else self.stream_keys, + ) # bail if no checkpointer if self._checkpointer_put_after_previous is not None: # create new checkpoint @@ -499,12 +500,35 @@ class PregelLoop: # suppress interrupt return True + def _emit(self, values: Sequence[tuple[str, str, Any]]) -> None: + if self.stream is None: + return + for v in values: + self.stream(v) + + def _output_writes( + self, task_id: str, writes: Sequence[tuple[str, Any]], *, cached: bool = False + ) -> None: + if task := next((t for t in self.tasks if t.id == task_id), None): + self._emit( + (self.config["configurable"].get("checkpoint_ns", ""), "updates", v) + for v in map_output_updates(self.output_keys, [(task, writes)], cached) + ) + if not cached: + self._emit( + (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) + for v in map_debug_task_results( + self.step, [(task, writes)], self.stream_keys + ) + ) + class SyncPregelLoop(PregelLoop, ContextManager): def __init__( self, input: Optional[Any], *, + stream: Optional[StreamProtocol], config: RunnableConfig, store: Optional[BaseStore], checkpointer: Optional[BaseCheckpointSaver], @@ -512,9 +536,11 @@ class SyncPregelLoop(PregelLoop, ContextManager): specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], output_keys: Union[str, Sequence[str]] = EMPTY_SEQ, stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ, + debug: bool = False, ) -> None: super().__init__( input, + stream=stream, config=config, checkpointer=checkpointer, store=store, @@ -522,6 +548,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): specs=specs, output_keys=output_keys, stream_keys=stream_keys, + debug=debug, ) self.stack = ExitStack() if checkpointer: @@ -596,6 +623,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): self, input: Optional[Any], *, + stream: Optional[StreamProtocol], config: RunnableConfig, store: Optional[BaseStore], checkpointer: Optional[BaseCheckpointSaver], @@ -603,9 +631,11 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]], output_keys: Union[str, Sequence[str]] = EMPTY_SEQ, stream_keys: Union[str, Sequence[str]] = EMPTY_SEQ, + debug: bool = False, ) -> None: super().__init__( input, + stream=stream, config=config, checkpointer=checkpointer, store=store, @@ -613,6 +643,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): specs=specs, output_keys=output_keys, stream_keys=stream_keys, + debug=debug, ) self.store = AsyncBatchedStore(self.store) if self.store else None self.stack = AsyncExitStack() diff --git a/libs/langgraph/langgraph/pregel/runner.py b/libs/langgraph/langgraph/pregel/runner.py new file mode 100644 index 000000000..a2a148d18 --- /dev/null +++ b/libs/langgraph/langgraph/pregel/runner.py @@ -0,0 +1,193 @@ +import asyncio +import concurrent.futures +import time +from typing import ( + Any, + AsyncIterator, + Callable, + Iterator, + Optional, + Sequence, + Type, + Union, +) + +from langgraph.constants import ERROR, INTERRUPT +from langgraph.errors import GraphInterrupt +from langgraph.pregel.executor import Submit +from langgraph.pregel.retry import arun_with_retry, run_with_retry +from langgraph.pregel.types import PregelExecutableTask, RetryPolicy + + +class PregelRunner: + def __init__( + self, + *, + submit: Submit, + put_writes: Callable[[str, Sequence[tuple[str, Any]]], None], + use_astream: bool = False, + ) -> None: + self.submit = submit + self.put_writes = put_writes + self.use_astream = use_astream + + def tick( + self, + tasks: list[PregelExecutableTask], + *, + timeout: Optional[float] = None, + retry_policy: Optional[RetryPolicy] = None, + ) -> Iterator[None]: + # execute tasks, and wait for one to fail or all to finish. + # each task is independent from all other concurrent tasks + # yield updates/debug output as each task finishes + futures = { + self.submit( + run_with_retry, + task, + retry_policy, + ): task + for task in tasks + if not task.writes + } + all_futures = futures.copy() + end_time = timeout + time.monotonic() if timeout else None + while futures: + done, _ = concurrent.futures.wait( + futures, + return_when=concurrent.futures.FIRST_COMPLETED, + timeout=(max(0, end_time - time.monotonic()) if end_time else None), + ) + if not done: + break # timed out + for fut in done: + task = futures.pop(fut) + if exc := _exception(fut): + if isinstance(exc, GraphInterrupt): + # save interrupt to checkpointer + self.put_writes(task.id, [(INTERRUPT, i) for i in exc.args[0]]) + else: + # save error to checkpointer + self.put_writes(task.id, [(ERROR, exc)]) + + else: + # save task writes to checkpointer + self.put_writes(task.id, task.writes) + else: + # remove references to loop vars + del fut, task + # maybe stop other tasks + if _should_stop_others(done): + break + # give control back to the caller + yield + # panic on failure or timeout + _panic_or_proceed(all_futures) + + async def atick( + self, + tasks: list[PregelExecutableTask], + *, + timeout: Optional[float] = None, + retry_policy: Optional[RetryPolicy] = None, + ) -> AsyncIterator[None]: + loop = asyncio.get_event_loop() + # execute tasks, and wait for one to fail or all to finish. + # each task is independent from all other concurrent tasks + # yield updates/debug output as each task finishes + futures = { + self.submit( + arun_with_retry, + task, + retry_policy, + stream=self.use_astream, + __name__=task.name, + __cancel_on_exit__=True, + ): task + for task in tasks + if not task.writes + } + all_futures = futures.copy() + end_time = timeout + loop.time() if timeout else None + while futures: + done, _ = await asyncio.wait( + futures, + return_when=asyncio.FIRST_COMPLETED, + timeout=(max(0, end_time - loop.time()) if end_time else None), + ) + if not done: + break # timed out + for fut in done: + task = futures.pop(fut) + if exc := _exception(fut): + if isinstance(exc, GraphInterrupt): + # save interrupt to checkpointer + self.put_writes(task.id, [(INTERRUPT, i) for i in exc.args[0]]) + else: + # save error to checkpointer + self.put_writes(task.id, [(ERROR, exc)]) + else: + # save task writes to checkpointer + self.put_writes(task.id, task.writes) + else: + # remove references to loop vars + del fut, task + # maybe stop other tasks + if _should_stop_others(done): + break + # give control back to the caller + yield + # panic on failure or timeout + _panic_or_proceed(all_futures, asyncio.TimeoutError) + + +def _should_stop_others( + done: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], +) -> bool: + for fut in done: + if fut.cancelled(): + return True + if exc := fut.exception(): + return not isinstance(exc, GraphInterrupt) + else: + return False + + +def _exception( + fut: Union[concurrent.futures.Future[Any], asyncio.Task[Any]], +) -> Optional[BaseException]: + if fut.cancelled(): + if isinstance(fut, asyncio.Task): + return asyncio.CancelledError() + else: + return concurrent.futures.CancelledError() + else: + return fut.exception() + + +def _panic_or_proceed( + futs: Union[set[concurrent.futures.Future[Any]], set[asyncio.Task[Any]]], + timeout_exc_cls: Type[Exception] = TimeoutError, +) -> None: + done: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set() + inflight: set[Union[concurrent.futures.Future[Any], asyncio.Task[Any]]] = set() + for fut in futs: + if fut.done(): + done.add(fut) + else: + inflight.add(fut) + while done: + # if any task failed + if exc := _exception(done.pop()): + # cancel all pending tasks + while inflight: + inflight.pop().cancel() + # raise the exception + raise exc + if inflight: + # if we got here means we timed out + while inflight: + # cancel all pending tasks + inflight.pop().cancel() + # raise timeout error + raise timeout_exc_cls("Timed out") diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 7707e2405..9d9c975d4 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1,7 +1,6 @@ import asyncio import json import operator -import random import re import sys from collections import Counter @@ -8431,72 +8430,6 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None: ] -@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) -async def test_large_graph(checkpointer_name: str) -> None: - class OverallState(TypedDict): - subjects: list[str] - jokes: Annotated[list[str], operator.add] - - async def continue_to_jokes(state: OverallState): - return [Send("generate_joke", {"subject": s}) for s in state["subjects"]] - - class JokeInput(TypedDict): - subject: str - - class JokeOutput(TypedDict): - jokes: list[str] - - async def edit(state: JokeInput): - subject = state["subject"] - return {"subject": f"{subject} - hohoho"} - - # subgraph - subgraph = StateGraph(input=JokeInput, output=JokeOutput) - subgraph.add_node("edit", edit) - subgraph.add_node( - "generate", lambda state: {"jokes": [f"Joke about {state['subject']}"]} - ) - subgraph.add_node( - "bump", lambda state: {"jokes": [state["jokes"][0] + " a"]}, input=JokeOutput - ) - subgraph.set_entry_point("edit") - subgraph.add_edge("edit", "generate") - subgraph.add_edge("generate", "bump") - subgraph.add_conditional_edges( - "bump", lambda state: END if state["jokes"][0].endswith(" a" * 10) else "bump" - ) - subgraph.set_finish_point("generate") - - # parent graph - builder = StateGraph(OverallState) - builder.add_node("generate_joke", subgraph.compile()) - builder.add_conditional_edges(START, continue_to_jokes) - builder.add_edge("generate_joke", END) - - async with awith_checkpointer(checkpointer_name) as checkpointer: - graph = builder.compile(checkpointer=checkpointer) - config = {"configurable": {"thread_id": "1"}} - - # invoke and pause at nested interrupt - assert ( - len( - [ - c - async for c in graph.astream( - { - "subjects": [ - random.choice("abcdefghijklmnopqrstuvwxyz") - for _ in range(1000) - ] - }, - config=config, - ) - ] - ) - == 1000 - ) - - @pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_ASYNC) async def test_send_to_nested_graphs(checkpointer_name: str) -> None: class OverallState(TypedDict): From e42e1a9f6bc0b02bf582be4ef2f3cf4ffe3524cd Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 10:56:41 -0700 Subject: [PATCH 247/287] Fix test --- libs/langgraph/tests/test_pregel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 3f0b2a2b9..10018450c 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -2005,6 +2005,8 @@ def test_conditional_graph( ) if data.get("intermediate_steps") is None: data["intermediate_steps"] = [] + else: + data["intermediate_steps"] = data["intermediate_steps"].copy() data["intermediate_steps"].append([agent_action, observation]) return data From f2e0dc104246caa0100900d8f9a7b319020c5d88 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Tue, 3 Sep 2024 18:13:20 -0700 Subject: [PATCH 248/287] Reduce cpu time spent on langchain-core utilities - shaves off 2s of 11s runtime of a simple graph with 1,000 subgraphs --- libs/langgraph/langgraph/graph/graph.py | 2 +- libs/langgraph/langgraph/graph/state.py | 6 +- .../langgraph/prebuilt/tool_executor.py | 2 +- .../langgraph/langgraph/prebuilt/tool_node.py | 2 +- .../langgraph/prebuilt/tool_validator.py | 2 +- libs/langgraph/langgraph/pregel/__init__.py | 11 +- libs/langgraph/langgraph/pregel/algo.py | 7 +- libs/langgraph/langgraph/pregel/config.py | 34 ---- libs/langgraph/langgraph/pregel/loop.py | 2 +- libs/langgraph/langgraph/pregel/manager.py | 7 +- libs/langgraph/langgraph/pregel/read.py | 4 +- libs/langgraph/langgraph/pregel/write.py | 2 +- libs/langgraph/langgraph/utils/__init__.py | 0 libs/langgraph/langgraph/utils/config.py | 150 ++++++++++++++ libs/langgraph/langgraph/utils/fields.py | 101 ++++++++++ .../langgraph/{utils.py => utils/runnable.py} | 184 +++++++----------- libs/langgraph/tests/test_utils.py | 8 +- 17 files changed, 349 insertions(+), 175 deletions(-) delete mode 100644 libs/langgraph/langgraph/pregel/config.py create mode 100644 libs/langgraph/langgraph/utils/__init__.py create mode 100644 libs/langgraph/langgraph/utils/config.py create mode 100644 libs/langgraph/langgraph/utils/fields.py rename libs/langgraph/langgraph/{utils.py => utils/runnable.py} (58%) diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index 570c06bad..af6700c2a 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -38,7 +38,7 @@ from langgraph.pregel import Channel, Pregel from langgraph.pregel.read import PregelNode from langgraph.pregel.types import All from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry -from langgraph.utils import RunnableCallable, coerce_to_runnable +from langgraph.utils.runnable import RunnableCallable, coerce_to_runnable logger = logging.getLogger(__name__) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 307b672aa..ce25f6c5d 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -44,7 +44,11 @@ from langgraph.pregel.read import ChannelRead, PregelNode from langgraph.pregel.types import All, RetryPolicy from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry from langgraph.store.base import BaseStore -from langgraph.utils import RunnableCallable, coerce_to_runnable, get_field_default +from langgraph.utils.fields import get_field_default +from langgraph.utils.runnable import ( + RunnableCallable, + coerce_to_runnable, +) logger = logging.getLogger(__name__) diff --git a/libs/langgraph/langgraph/prebuilt/tool_executor.py b/libs/langgraph/langgraph/prebuilt/tool_executor.py index 7590b3836..341f4874d 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_executor.py +++ b/libs/langgraph/langgraph/prebuilt/tool_executor.py @@ -6,7 +6,7 @@ from langchain_core.tools import BaseTool from langchain_core.tools import tool as create_tool from langgraph._api.deprecation import deprecated -from langgraph.utils import RunnableCallable +from langgraph.utils.runnable import RunnableCallable INVALID_TOOL_MSG_TEMPLATE = ( "{requested_tool_name} is not a valid tool, " diff --git a/libs/langgraph/langgraph/prebuilt/tool_node.py b/libs/langgraph/langgraph/prebuilt/tool_node.py index 05ac1d8e0..54fc1c6f3 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_node.py +++ b/libs/langgraph/langgraph/prebuilt/tool_node.py @@ -21,7 +21,7 @@ from langchain_core.tools import BaseTool, InjectedToolArg from langchain_core.tools import tool as create_tool from typing_extensions import get_args -from langgraph.utils import RunnableCallable +from langgraph.utils.runnable import RunnableCallable INVALID_TOOL_NAME_ERROR_TEMPLATE = ( "Error: {requested_tool} is not a valid tool, try one of [{available_tools}]." diff --git a/libs/langgraph/langgraph/prebuilt/tool_validator.py b/libs/langgraph/langgraph/prebuilt/tool_validator.py index 73b2cce2b..95c84a0a5 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_validator.py +++ b/libs/langgraph/langgraph/prebuilt/tool_validator.py @@ -33,7 +33,7 @@ from langchain_core.tools import BaseTool, create_schema_from_function from pydantic import BaseModel as BaseModelV2 from pydantic import ValidationError as ValidationErrorV2 -from langgraph.utils import RunnableCallable +from langgraph.utils.runnable import RunnableCallable def _default_format_error( diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 93eb1a07d..81b298d87 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -34,8 +34,6 @@ from langchain_core.runnables.config import ( ensure_config, get_async_callback_manager_for_config, get_callback_manager_for_config, - merge_configs, - patch_config, ) from langchain_core.runnables.utils import ( ConfigurableFieldSpec, @@ -76,7 +74,6 @@ from langgraph.pregel.algo import ( local_write, prepare_next_tasks, ) -from langgraph.pregel.config import patch_checkpoint_map, patch_configurable from langgraph.pregel.debug import tasks_w_writes from langgraph.pregel.io import read_channels from langgraph.pregel.loop import AsyncPregelLoop, SyncPregelLoop @@ -96,7 +93,13 @@ from langgraph.pregel.utils import ( from langgraph.pregel.validate import validate_graph, validate_keys from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry from langgraph.store.base import BaseStore -from langgraph.utils import RunnableCallable +from langgraph.utils.config import ( + merge_configs, + patch_checkpoint_map, + patch_config, + patch_configurable, +) +from langgraph.utils.runnable import RunnableCallable WriteValue = Union[ Runnable[Input, Output], diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index f6301a379..9c57e707a 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -17,11 +17,7 @@ from typing import ( from uuid import UUID, uuid5 from langchain_core.callbacks.manager import AsyncParentRunManager, ParentRunManager -from langchain_core.runnables.config import ( - RunnableConfig, - merge_configs, - patch_config, -) +from langchain_core.runnables.config import RunnableConfig from langgraph.channels.base import BaseChannel from langgraph.checkpoint.base import ( @@ -51,6 +47,7 @@ from langgraph.pregel.log import logger from langgraph.pregel.manager import ChannelsManager from langgraph.pregel.read import PregelNode from langgraph.pregel.types import All, PregelExecutableTask, PregelTask +from langgraph.utils.config import merge_configs, patch_config class WritesProtocol(Protocol): diff --git a/libs/langgraph/langgraph/pregel/config.py b/libs/langgraph/langgraph/pregel/config.py deleted file mode 100644 index f4c64eb39..000000000 --- a/libs/langgraph/langgraph/pregel/config.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Any, Optional - -from langchain_core.runnables import RunnableConfig - -from langgraph.checkpoint.base import CheckpointMetadata -from langgraph.constants import CONFIG_KEY_CHECKPOINT_MAP - - -def patch_configurable( - config: Optional[RunnableConfig], patch: dict[str, Any] -) -> RunnableConfig: - if config is None: - return {"configurable": patch} - else: - return {**config, "configurable": {**config["configurable"], **patch}} - - -def patch_checkpoint_map( - config: RunnableConfig, metadata: Optional[CheckpointMetadata] -) -> RunnableConfig: - if parents := (metadata.get("parents") if metadata else None): - return patch_configurable( - config, - { - CONFIG_KEY_CHECKPOINT_MAP: { - **parents, - config["configurable"]["checkpoint_ns"]: config["configurable"][ - "checkpoint_id" - ], - }, - }, - ) - else: - return config diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index c903730dd..d2052b17a 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -59,7 +59,6 @@ from langgraph.pregel.algo import ( prepare_next_tasks, should_interrupt, ) -from langgraph.pregel.config import patch_configurable from langgraph.pregel.debug import ( map_debug_checkpoint, map_debug_task_results, @@ -86,6 +85,7 @@ from langgraph.pregel.types import PregelExecutableTask from langgraph.pregel.utils import get_new_channel_versions from langgraph.store.base import BaseStore from langgraph.store.batch import AsyncBatchedStore +from langgraph.utils.config import patch_configurable V = TypeVar("V") INPUT_DONE = object() diff --git a/libs/langgraph/langgraph/pregel/manager.py b/libs/langgraph/langgraph/pregel/manager.py index ccfc8de8a..f70c86d46 100644 --- a/libs/langgraph/langgraph/pregel/manager.py +++ b/libs/langgraph/langgraph/pregel/manager.py @@ -2,7 +2,7 @@ import asyncio from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager from typing import AsyncIterator, Iterator, Mapping, Optional, Union -from langchain_core.runnables import RunnableConfig, patch_config +from langchain_core.runnables import RunnableConfig from langgraph.channels.base import BaseChannel from langgraph.checkpoint.base import Checkpoint @@ -14,6 +14,7 @@ from langgraph.managed.base import ( ) from langgraph.managed.context import Context from langgraph.store.base import BaseStore +from langgraph.utils.config import patch_configurable @contextmanager @@ -26,7 +27,7 @@ def ChannelsManager( skip_context: bool = False, ) -> Iterator[tuple[Mapping[str, BaseChannel], ManagedValueMapping]]: """Manage channels for the lifetime of a Pregel invocation (multiple steps).""" - config_for_managed = patch_config(config, configurable={CONFIG_KEY_STORE: store}) + config_for_managed = patch_configurable(config, {CONFIG_KEY_STORE: store}) channel_specs: Mapping[str, BaseChannel] = {} managed_specs: Mapping[str, ManagedValueSpec] = {} for k, v in specs.items(): @@ -69,7 +70,7 @@ async def AsyncChannelsManager( skip_context: bool = False, ) -> AsyncIterator[Mapping[str, BaseChannel]]: """Manage channels for the lifetime of a Pregel invocation (multiple steps).""" - config_for_managed = patch_config(config, configurable={CONFIG_KEY_STORE: store}) + config_for_managed = patch_configurable(config, {CONFIG_KEY_STORE: store}) channel_specs: Mapping[str, BaseChannel] = {} managed_specs: Mapping[str, ManagedValueSpec] = {} for k, v in specs.items(): diff --git a/libs/langgraph/langgraph/pregel/read.py b/libs/langgraph/langgraph/pregel/read.py index ca0828fda..e111ad820 100644 --- a/libs/langgraph/langgraph/pregel/read.py +++ b/libs/langgraph/langgraph/pregel/read.py @@ -19,13 +19,13 @@ from langchain_core.runnables import ( RunnableSerializable, ) from langchain_core.runnables.base import Input, Other, Output, coerce_to_runnable -from langchain_core.runnables.config import merge_configs from langchain_core.runnables.utils import ConfigurableFieldSpec from langgraph.constants import CONFIG_KEY_READ from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.write import ChannelWrite -from langgraph.utils import RunnableCallable +from langgraph.utils.config import merge_configs +from langgraph.utils.runnable import RunnableCallable READ_TYPE = Callable[[str, bool], Union[Any, dict[str, Any]]] diff --git a/libs/langgraph/langgraph/pregel/write.py b/libs/langgraph/langgraph/pregel/write.py index e85885cc9..9fcb44696 100644 --- a/libs/langgraph/langgraph/pregel/write.py +++ b/libs/langgraph/langgraph/pregel/write.py @@ -18,7 +18,7 @@ from langchain_core.runnables.utils import ConfigurableFieldSpec from langgraph.constants import CONFIG_KEY_SEND, TASKS, Send from langgraph.errors import InvalidUpdateError -from langgraph.utils import RunnableCallable +from langgraph.utils.runnable import RunnableCallable TYPE_SEND = Callable[[Sequence[tuple[str, Any]]], None] R = TypeVar("R", bound=Runnable) diff --git a/libs/langgraph/langgraph/utils/__init__.py b/libs/langgraph/langgraph/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/libs/langgraph/langgraph/utils/config.py b/libs/langgraph/langgraph/utils/config.py new file mode 100644 index 000000000..44fb29d56 --- /dev/null +++ b/libs/langgraph/langgraph/utils/config.py @@ -0,0 +1,150 @@ +from typing import Any, Optional + +from langchain_core.callbacks import Callbacks +from langchain_core.runnables import RunnableConfig +from langchain_core.runnables.config import COPIABLE_KEYS, DEFAULT_RECURSION_LIMIT + +from langgraph.checkpoint.base import CheckpointMetadata +from langgraph.constants import CONFIG_KEY_CHECKPOINT_MAP + + +def patch_configurable( + config: Optional[RunnableConfig], patch: dict[str, Any] +) -> RunnableConfig: + if config is None: + return {"configurable": patch} + else: + return {**config, "configurable": {**config["configurable"], **patch}} + + +def patch_checkpoint_map( + config: RunnableConfig, metadata: Optional[CheckpointMetadata] +) -> RunnableConfig: + if parents := (metadata.get("parents") if metadata else None): + return patch_configurable( + config, + { + CONFIG_KEY_CHECKPOINT_MAP: { + **parents, + config["configurable"]["checkpoint_ns"]: config["configurable"][ + "checkpoint_id" + ], + }, + }, + ) + else: + return config + + +def merge_configs(*configs: Optional[RunnableConfig]) -> RunnableConfig: + """Merge multiple configs into one. + + Args: + *configs (Optional[RunnableConfig]): The configs to merge. + + Returns: + RunnableConfig: The merged config. + """ + base: RunnableConfig = {} + # Even though the keys aren't literals, this is correct + # because both dicts are the same type + for config in configs: + if config is None: + continue + for key in config: + if key == "metadata": + base[key] = { # type: ignore + **base.get(key, {}), # type: ignore + **(config.get(key) or {}), # type: ignore + } + elif key == "tags": + base[key] = sorted( # type: ignore + set(base.get(key, []) + (config.get(key) or [])), # type: ignore + ) + elif key == "configurable": + base[key] = { # type: ignore + **base.get(key, {}), # type: ignore + **(config.get(key) or {}), # type: ignore + } + elif key == "callbacks": + base_callbacks = base.get("callbacks") + these_callbacks = config["callbacks"] + # callbacks can be either None, list[handler] or manager + # so merging two callbacks values has 6 cases + if isinstance(these_callbacks, list): + if base_callbacks is None: + base["callbacks"] = these_callbacks.copy() + elif isinstance(base_callbacks, list): + base["callbacks"] = base_callbacks + these_callbacks + else: + # base_callbacks is a manager + mngr = base_callbacks.copy() + for callback in these_callbacks: + mngr.add_handler(callback, inherit=True) + base["callbacks"] = mngr + elif these_callbacks is not None: + # these_callbacks is a manager + if base_callbacks is None: + base["callbacks"] = these_callbacks.copy() + elif isinstance(base_callbacks, list): + mngr = these_callbacks.copy() + for callback in base_callbacks: + mngr.add_handler(callback, inherit=True) + base["callbacks"] = mngr + else: + # base_callbacks is also a manager + base["callbacks"] = base_callbacks.merge(these_callbacks) + elif key == "recursion_limit": + if config["recursion_limit"] != DEFAULT_RECURSION_LIMIT: + base["recursion_limit"] = config["recursion_limit"] + elif key in COPIABLE_KEYS and config[key] is not None: # type: ignore[literal-required] + base[key] = config[key].copy() # type: ignore[literal-required] + else: + base[key] = config[key] or base.get(key) # type: ignore + return base + + +def patch_config( + config: Optional[RunnableConfig], + *, + callbacks: Optional[Callbacks] = None, + recursion_limit: Optional[int] = None, + max_concurrency: Optional[int] = None, + run_name: Optional[str] = None, + configurable: Optional[dict[str, Any]] = None, +) -> RunnableConfig: + """Patch a config with new values. + + Args: + config (Optional[RunnableConfig]): The config to patch. + callbacks (Optional[BaseCallbackManager], optional): The callbacks to set. + Defaults to None. + recursion_limit (Optional[int], optional): The recursion limit to set. + Defaults to None. + max_concurrency (Optional[int], optional): The max concurrency to set. + Defaults to None. + run_name (Optional[str], optional): The run name to set. Defaults to None. + configurable (Optional[Dict[str, Any]], optional): The configurable to set. + Defaults to None. + + Returns: + RunnableConfig: The patched config. + """ + config = config or {} + if callbacks is not None: + # If we're replacing callbacks, we need to unset run_name + # As that should apply only to the same run as the original callbacks + config["callbacks"] = callbacks + if "run_name" in config: + del config["run_name"] + if "run_id" in config: + del config["run_id"] + if recursion_limit is not None: + config["recursion_limit"] = recursion_limit + if max_concurrency is not None: + config["max_concurrency"] = max_concurrency + if run_name is not None: + config["run_name"] = run_name + if configurable is not None: + config["configurable"] = {**config.get("configurable", {}), **configurable} + return config diff --git a/libs/langgraph/langgraph/utils/fields.py b/libs/langgraph/langgraph/utils/fields.py new file mode 100644 index 000000000..55a8e81be --- /dev/null +++ b/libs/langgraph/langgraph/utils/fields.py @@ -0,0 +1,101 @@ +from typing import Any, Optional, Type, Union + +from typing_extensions import ( + Annotated, + NotRequired, + ReadOnly, + Required, + get_origin, +) + + +def _is_optional_type(type_: Any) -> bool: + """Check if a type is Optional.""" + + if hasattr(type_, "__origin__") and hasattr(type_, "__args__"): + origin = get_origin(type_) + if origin is Optional: + return True + if origin is Union: + return any( + arg is type(None) or _is_optional_type(arg) for arg in type_.__args__ + ) + if origin is Annotated: + return _is_optional_type(type_.__args__[0]) + return origin is None + if hasattr(type_, "__bound__") and type_.__bound__ is not None: + return _is_optional_type(type_.__bound__) + return type_ is None + + +def _is_required_type(type_: Any) -> Optional[bool]: + """Check if an annotation is marked as Required/NotRequired. + + Returns: + - True if required + - False if not required + - None if not annotated with either + """ + origin = get_origin(type_) + if origin is Required: + return True + if origin is NotRequired: + return False + if origin is Annotated or getattr(origin, "__args__", None): + # See https://typing.readthedocs.io/en/latest/spec/typeddict.html#interaction-with-annotated + return _is_required_type(type_.__args__[0]) + return None + + +def _is_readonly_type(type_: Any) -> bool: + """Check if an annotation is marked as ReadOnly. + + Returns: + - True if is read only + - False if not read only + """ + + # See: https://typing.readthedocs.io/en/latest/spec/typeddict.html#typing-readonly-type-qualifier + origin = get_origin(type_) + if origin is Annotated: + return _is_readonly_type(type_.__args__[0]) + if origin is ReadOnly: + return True + return False + + +_DEFAULT_KEYS = frozenset() + + +def get_field_default(name: str, type_: Any, schema: Type[Any]) -> Any: + """Determine the default value for a field in a state schema. + + This is based on: + If TypedDict: + - Required/NotRequired + - total=False -> everything optional + - Type annotation (Optional/Union[None]) + """ + optional_keys = getattr(schema, "__optional_keys__", _DEFAULT_KEYS) + irq = _is_required_type(type_) + if name in optional_keys: + # Either total=False or explicit NotRequired. + # No type annotation trumps this. + if irq: + # Unless it's earlier versions of python & explicit Required + return ... + return None + if irq is not None: + if irq: + # Handle Required[] + # (we already handled NotRequired and total=False) + return ... + # Handle NotRequired[] for earlier versions of python + return None + # Note, we ignore ReadOnly attributes, + # as they don't make much sense. (we don't care if you mutate the state in your node) + # and mutating state in your node has no effect on our graph state. + # Base case is the annotation + if _is_optional_type(type_): + return None + return ... diff --git a/libs/langgraph/langgraph/utils.py b/libs/langgraph/langgraph/utils/runnable.py similarity index 58% rename from libs/langgraph/langgraph/utils.py rename to libs/langgraph/langgraph/utils/runnable.py index cc3fd4760..bbe344ed5 100644 --- a/libs/langgraph/langgraph/utils.py +++ b/libs/langgraph/langgraph/utils/runnable.py @@ -4,8 +4,9 @@ import inspect import sys from contextvars import copy_context from functools import partial, wraps -from typing import Any, AsyncIterator, Awaitable, Callable, Optional, Type, Union +from typing import Any, AsyncIterator, Awaitable, Callable, Optional +from langchain_core.load.serializable import to_json_not_implemented from langchain_core.runnables.base import ( Runnable, RunnableConfig, @@ -14,19 +15,16 @@ from langchain_core.runnables.base import ( RunnableParallel, ) from langchain_core.runnables.config import ( - merge_configs, + ensure_config, + get_async_callback_manager_for_config, + get_callback_manager_for_config, run_in_executor, var_child_runnable_config, ) -from langchain_core.runnables.utils import accepts_config -from typing_extensions import ( - Annotated, - NotRequired, - ReadOnly, - Required, - TypeGuard, - get_origin, -) +from langchain_core.runnables.utils import accepts_config, accepts_run_manager +from typing_extensions import TypeGuard + +from langgraph.utils.config import merge_configs, patch_config try: from langchain_core.runnables.config import _set_config_context @@ -42,6 +40,9 @@ class StrEnum(str, enum.Enum): """A string enum.""" +ASYNCIO_ACCEPTS_CONTEXT = sys.version_info >= (3, 11) + + class RunnableCallable(Runnable): """A much simpler version of RunnableLambda that requires sync and async functions.""" @@ -70,11 +71,18 @@ class RunnableCallable(Runnable): except AttributeError: pass self.func = func + if func is not None: + self.func_accepts_config = accepts_config(func) + self.func_accepts_run_manager = accepts_run_manager(func) self.afunc = afunc + if afunc is not None: + self.afunc_accepts_config = accepts_config(afunc) + self.afunc_accepts_run_manager = accepts_run_manager(afunc) self.config: Optional[RunnableConfig] = {"tags": tags} if tags else None self.kwargs = kwargs self.trace = trace self.recurse = recurse + self.serialized = to_json_not_implemented(self) def __repr__(self) -> str: repr_args = { @@ -94,15 +102,34 @@ class RunnableCallable(Runnable): " via the async API (ainvoke, astream, etc.)" ) kwargs = {**self.kwargs, **kwargs} + config = ensure_config(merge_configs(self.config, config)) + context = copy_context() if self.trace: - ret = self._call_with_config( - self.func, input, merge_configs(self.config, config), **kwargs + config = ensure_config(config) + callback_manager = get_callback_manager_for_config(config) + run_manager = callback_manager.on_chain_start( + self.serialized, + input, + name=config.get("run_name") or self.get_name(), + run_id=config.pop("run_id", None), ) + try: + child_config = patch_config(config, callbacks=run_manager.get_child()) + context = copy_context() + context.run(_set_config_context, child_config) + if self.func_accepts_config: + kwargs["config"] = config + if self.func_accepts_run_manager: + kwargs["run_manager"] = run_manager + ret = context.run(self.func, input, **kwargs) + except BaseException as e: + run_manager.on_chain_error(e) + raise + else: + run_manager.on_chain_end(ret) else: - config = merge_configs(self.config, config) - context = copy_context() context.run(_set_config_context, config) - if accepts_config(self.func): + if self.func_accepts_config: kwargs["config"] = config ret = context.run(self.func, input, **kwargs) if isinstance(ret, Runnable) and self.recurse: @@ -115,17 +142,38 @@ class RunnableCallable(Runnable): if not self.afunc: return self.invoke(input, config) kwargs = {**self.kwargs, **kwargs} + config = ensure_config(merge_configs(self.config, config)) + context = copy_context() if self.trace: - ret = await self._acall_with_config( - self.afunc, input, merge_configs(self.config, config), **kwargs + callback_manager = get_async_callback_manager_for_config(config) + run_manager = await callback_manager.on_chain_start( + self.serialized, + input, + name=config.get("run_name") or self.name, + run_id=config.pop("run_id", None), ) + try: + child_config = patch_config(config, callbacks=run_manager.get_child()) + context.run(_set_config_context, child_config) + if self.afunc_accepts_config: + kwargs["config"] = config + if self.afunc_accepts_run_manager: + kwargs["run_manager"] = run_manager + coro = self.afunc(input, **kwargs) + if ASYNCIO_ACCEPTS_CONTEXT: + ret = await asyncio.create_task(coro, context=context) + else: + ret = await coro + except BaseException as e: + await run_manager.on_chain_error(e) + raise + else: + await run_manager.on_chain_end(ret) else: - config = merge_configs(self.config, config) - context = copy_context() context.run(_set_config_context, config) - if accepts_config(self.afunc): + if self.afunc_accepts_config: kwargs["config"] = config - if sys.version_info >= (3, 11): + if ASYNCIO_ACCEPTS_CONTEXT: ret = await asyncio.create_task( self.afunc(input, **kwargs), context=context ) @@ -188,95 +236,3 @@ def coerce_to_runnable(thing: RunnableLike, *, name: str, trace: bool) -> Runnab f"Expected a Runnable, callable or dict." f"Instead got an unsupported type: {type(thing)}" ) - - -def _is_optional_type(type_: Any) -> bool: - """Check if a type is Optional.""" - - if hasattr(type_, "__origin__") and hasattr(type_, "__args__"): - origin = get_origin(type_) - if origin is Optional: - return True - if origin is Union: - return any( - arg is type(None) or _is_optional_type(arg) for arg in type_.__args__ - ) - if origin is Annotated: - return _is_optional_type(type_.__args__[0]) - return origin is None - if hasattr(type_, "__bound__") and type_.__bound__ is not None: - return _is_optional_type(type_.__bound__) - return type_ is None - - -def _is_required_type(type_: Any) -> Optional[bool]: - """Check if an annotation is marked as Required/NotRequired. - - Returns: - - True if required - - False if not required - - None if not annotated with either - """ - origin = get_origin(type_) - if origin is Required: - return True - if origin is NotRequired: - return False - if origin is Annotated or getattr(origin, "__args__", None): - # See https://typing.readthedocs.io/en/latest/spec/typeddict.html#interaction-with-annotated - return _is_required_type(type_.__args__[0]) - return None - - -def _is_readonly_type(type_: Any) -> bool: - """Check if an annotation is marked as ReadOnly. - - Returns: - - True if is read only - - False if not read only - """ - - # See: https://typing.readthedocs.io/en/latest/spec/typeddict.html#typing-readonly-type-qualifier - origin = get_origin(type_) - if origin is Annotated: - return _is_readonly_type(type_.__args__[0]) - if origin is ReadOnly: - return True - return False - - -_DEFAULT_KEYS = frozenset() - - -def get_field_default(name: str, type_: Any, schema: Type[Any]) -> Any: - """Determine the default value for a field in a state schema. - - This is based on: - If TypedDict: - - Required/NotRequired - - total=False -> everything optional - - Type annotation (Optional/Union[None]) - """ - optional_keys = getattr(schema, "__optional_keys__", _DEFAULT_KEYS) - irq = _is_required_type(type_) - if name in optional_keys: - # Either total=False or explicit NotRequired. - # No type annotation trumps this. - if irq: - # Unless it's earlier versions of python & explicit Required - return ... - return None - if irq is not None: - if irq: - # Handle Required[] - # (we already handled NotRequired and total=False) - return ... - # Handle NotRequired[] for earlier versions of python - return None - # Note, we ignore ReadOnly attributes, - # as they don't make much sense. (we don't care if you mutate the state in your node) - # and mutating state in your node has no effect on our graph state. - # Base case is the annotation - if _is_optional_type(type_): - return None - return ... diff --git a/libs/langgraph/tests/test_utils.py b/libs/langgraph/tests/test_utils.py index bee8fcd9c..e8ea94fff 100644 --- a/libs/langgraph/tests/test_utils.py +++ b/libs/langgraph/tests/test_utils.py @@ -21,12 +21,8 @@ from typing_extensions import Annotated, NotRequired, Required from langgraph.graph import END, StateGraph from langgraph.graph.graph import CompiledGraph -from langgraph.utils import ( - _is_optional_type, - get_field_default, - is_async_callable, - is_async_generator, -) +from langgraph.utils.fields import _is_optional_type, get_field_default +from langgraph.utils.runnable import is_async_callable, is_async_generator pytestmark = pytest.mark.anyio From 535dbd5b06eb7976316badd3c2891f99bc2e02f6 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 10:47:16 -0700 Subject: [PATCH 249/287] Reduce from 9s to 4s on benchmark graph - Use a simpler version of RunnableSequence without tracing serialization - Remove accepts_run_manager check in RunnableCallable - Remove creation of ChannelWrite dynamically every time conditional edge runs --- libs/langgraph/langgraph/graph/graph.py | 22 +- libs/langgraph/langgraph/graph/state.py | 25 +- libs/langgraph/langgraph/pregel/__init__.py | 22 +- libs/langgraph/langgraph/pregel/algo.py | 4 +- libs/langgraph/langgraph/pregel/read.py | 18 +- libs/langgraph/langgraph/pregel/write.py | 94 +++-- libs/langgraph/langgraph/utils/config.py | 4 +- libs/langgraph/langgraph/utils/runnable.py | 321 ++++++++++++++++-- libs/langgraph/poetry.lock | 15 +- .../tests/__snapshots__/test_pregel.ambr | 39 +++ libs/langgraph/tests/test_pregel.py | 26 +- libs/langgraph/tests/test_pregel_async.py | 4 +- 12 files changed, 446 insertions(+), 148 deletions(-) diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index af6700c2a..41175c170 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -55,7 +55,7 @@ class Branch(NamedTuple): def run( self, - writer: Callable[[list[str]], Optional[Runnable]], + writer: Callable[[list[str], RunnableConfig], None], reader: Optional[Callable[[RunnableConfig], Any]] = None, ) -> None: return ChannelWrite.register_writer( @@ -75,7 +75,7 @@ class Branch(NamedTuple): config: RunnableConfig, *, reader: Optional[Callable[[], Any]], - writer: Callable[[list[str]], Optional[Runnable]], + writer: Callable[[list[str], RunnableConfig], None], ) -> Runnable: if reader: value = reader(config) @@ -86,7 +86,7 @@ class Branch(NamedTuple): else: value = input result = self.path.invoke(value, config) - return self._finish(writer, input, result) + return self._finish(writer, input, result, config) async def _aroute( self, @@ -94,7 +94,7 @@ class Branch(NamedTuple): config: RunnableConfig, *, reader: Optional[Callable[[], Any]], - writer: Callable[[list[str]], Optional[Runnable]], + writer: Callable[[list[str], RunnableConfig], Optional[Runnable]], ) -> Runnable: if reader: value = reader(config) @@ -105,10 +105,14 @@ class Branch(NamedTuple): else: value = input result = await self.path.ainvoke(value, config) - return self._finish(writer, input, result) + return self._finish(writer, input, result, config) def _finish( - self, writer: Callable[[list[str]], Optional[Runnable]], input: Any, result: Any + self, + writer: Callable[[list[str], RunnableConfig], None], + input: Any, + result: Any, + config: RunnableConfig, ): if not isinstance(result, list): result = [result] @@ -120,7 +124,7 @@ class Branch(NamedTuple): raise ValueError("Branch did not return a valid destination") if any(p.node == END for p in destinations if isinstance(p, Send)): raise InvalidUpdateError("Cannot send a packet to the END node") - return writer(destinations) or input + return writer(destinations, config) or input class Graph: @@ -449,7 +453,9 @@ class CompiledGraph(Pregel): self.nodes[end].channels.append(start) def attach_branch(self, start: str, name: str, branch: Branch) -> None: - def branch_writer(packets: list[Union[str, Send]]) -> Optional[ChannelWrite]: + def branch_writer( + packets: list[Union[str, Send]], config: RunnableConfig + ) -> Optional[ChannelWrite]: writes = [ ( ChannelWriteEntry(f"branch:{start}:{name}:{p}" if p != END else END) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index ce25f6c5d..644ae7adb 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -45,10 +45,7 @@ from langgraph.pregel.types import All, RetryPolicy from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry from langgraph.store.base import BaseStore from langgraph.utils.fields import get_field_default -from langgraph.utils.runnable import ( - RunnableCallable, - coerce_to_runnable, -) +from langgraph.utils.runnable import coerce_to_runnable logger = logging.getLogger(__name__) @@ -534,9 +531,7 @@ class CompiledStateGraph(CompiledGraph): if is_writable_managed_value(v) ] - def _get_state_key( - input: Union[None, dict, Any], config: RunnableConfig, *, key: str - ) -> Any: + def _get_state_key(input: Union[None, dict, Any], *, key: str) -> Any: if input is None: return SKIP_WRITE elif isinstance(input, dict): @@ -552,12 +547,7 @@ class CompiledStateGraph(CompiledGraph): [ChannelWriteEntry("__root__", skip_none=True)] if output_keys == ["__root__"] else [ - ChannelWriteEntry( - key, - mapper=RunnableCallable( - _get_state_key, key=key, trace=False, recurse=False - ), - ) + ChannelWriteEntry(key, mapper=partial(_get_state_key, key=key)) for key in output_keys ] ) @@ -600,7 +590,8 @@ class CompiledStateGraph(CompiledGraph): ], metadata=node.metadata, retry_policy=node.retry_policy, - ).pipe(node.runnable) + bound=node.runnable, + ) def attach_edge(self, starts: Union[str, Sequence[str]], end: str) -> None: if isinstance(starts, str): @@ -630,7 +621,9 @@ class CompiledStateGraph(CompiledGraph): ) def attach_branch(self, start: str, name: str, branch: Branch) -> None: - def branch_writer(packets: list[Union[str, Send]]) -> Optional[ChannelWrite]: + def branch_writer( + packets: list[Union[str, Send]], config: RunnableConfig + ) -> Optional[ChannelWrite]: if filtered := [p for p in packets if p != END]: writes = [ ( @@ -649,7 +642,7 @@ class CompiledStateGraph(CompiledGraph): ), ) ) - return ChannelWrite(writes, tags=[TAG_HIDDEN]) + ChannelWrite.do_write(config, writes) # attach branch publisher schema = ( diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 81b298d87..089b729fa 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -6,7 +6,6 @@ from functools import partial from typing import ( Any, AsyncIterator, - Awaitable, Callable, Dict, Iterator, @@ -28,7 +27,7 @@ from langchain_core.runnables import ( RunnableLambda, RunnableSequence, ) -from langchain_core.runnables.base import Input, Output, coerce_to_runnable +from langchain_core.runnables.base import Input, Output from langchain_core.runnables.config import ( RunnableConfig, ensure_config, @@ -101,12 +100,7 @@ from langgraph.utils.config import ( ) from langgraph.utils.runnable import RunnableCallable -WriteValue = Union[ - Runnable[Input, Output], - Callable[[Input], Output], - Callable[[Input], Awaitable[Output]], - Any, -] +WriteValue = Union[Callable[[Input], Output], Any] class Channel: @@ -171,11 +165,9 @@ class Channel: return ChannelWrite( [ChannelWriteEntry(c) for c in channels] + [ - ( - ChannelWriteEntry(k, skip_none=True, mapper=coerce_to_runnable(v)) - if isinstance(v, Runnable) or callable(v) - else ChannelWriteEntry(k, value=v) - ) + ChannelWriteEntry(k, mapper=v) + if callable(v) + else ChannelWriteEntry(k, value=v) for k, v in kwargs.items() ] ) @@ -812,7 +804,7 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): managed, ): # create task to run all writers of the chosen node - writers = self.nodes[as_node].get_writers() + writers = self.nodes[as_node].flat_writers if not writers: raise InvalidUpdateError(f"Node {as_node} has no writers") task = PregelExecutableTask( @@ -976,7 +968,7 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): managed, ): # create task to run all writers of the chosen node - writers = self.nodes[as_node].get_writers() + writers = self.nodes[as_node].flat_writers if not writers: raise InvalidUpdateError(f"Node {as_node} has no writers") task = PregelExecutableTask( diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 9c57e707a..3e465f115 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -297,7 +297,7 @@ def prepare_next_tasks( ) if for_execution: proc = processes[packet.node] - if node := proc.get_node(): + if node := proc.node: managed.replace_runtime_placeholders(step, packet.arg) writes = deque() task_checkpoint_ns = f"{checkpoint_ns}:{task_id}" @@ -403,7 +403,7 @@ def prepare_next_tasks( ) if for_execution: - if node := proc.get_node(): + if node := proc.node: writes = deque() task_checkpoint_ns = f"{checkpoint_ns}:{task_id}" tasks.append( diff --git a/libs/langgraph/langgraph/pregel/read.py b/libs/langgraph/langgraph/pregel/read.py index e111ad820..4d9944661 100644 --- a/libs/langgraph/langgraph/pregel/read.py +++ b/libs/langgraph/langgraph/pregel/read.py @@ -1,5 +1,6 @@ from __future__ import annotations +from functools import cached_property from typing import ( Any, AsyncIterator, @@ -15,7 +16,6 @@ from langchain_core.runnables import ( Runnable, RunnableConfig, RunnablePassthrough, - RunnableSequence, RunnableSerializable, ) from langchain_core.runnables.base import Input, Other, Output, coerce_to_runnable @@ -25,7 +25,7 @@ from langgraph.constants import CONFIG_KEY_READ from langgraph.pregel.retry import RetryPolicy from langgraph.pregel.write import ChannelWrite from langgraph.utils.config import merge_configs -from langgraph.utils.runnable import RunnableCallable +from langgraph.utils.runnable import RunnableCallable, RunnableSeq READ_TYPE = Callable[[str, bool], Union[Any, dict[str, Any]]] @@ -149,7 +149,8 @@ class PregelNode(Runnable): attrs = {**self.__dict__, **update} return PregelNode(**attrs) - def get_writers(self) -> list[Runnable]: + @cached_property + def flat_writers(self) -> list[Runnable]: """Get writers with optimizations applied.""" writers = self.writers.copy() while ( @@ -167,16 +168,17 @@ class PregelNode(Runnable): writers.pop() return writers - def get_node(self) -> Optional[Runnable[Any, Any]]: - writers = self.get_writers() + @cached_property + def node(self) -> Optional[Runnable[Any, Any]]: + writers = self.flat_writers if self.bound is DEFAULT_BOUND and not writers: return None elif self.bound is DEFAULT_BOUND and len(writers) == 1: return writers[0] elif self.bound is DEFAULT_BOUND: - return RunnableSequence(*writers) + return RunnableSeq(*writers) elif writers: - return RunnableSequence(self.bound, *writers) + return RunnableSeq(self.bound, *writers) else: return self.bound @@ -209,7 +211,7 @@ class PregelNode(Runnable): elif self.bound is DEFAULT_BOUND: return self.copy(update=dict(bound=coerce_to_runnable(other))) else: - return self.copy(update=dict(bound=self.bound | other)) + return self.copy(update=dict(bound=RunnableSeq(self.bound, other))) def pipe( self, diff --git a/libs/langgraph/langgraph/pregel/write.py b/libs/langgraph/langgraph/pregel/write.py index 9fcb44696..fd732966a 100644 --- a/libs/langgraph/langgraph/pregel/write.py +++ b/libs/langgraph/langgraph/pregel/write.py @@ -4,11 +4,9 @@ import asyncio from typing import ( Any, Callable, - List, NamedTuple, Optional, Sequence, - Tuple, TypeVar, Union, ) @@ -32,7 +30,7 @@ class ChannelWriteEntry(NamedTuple): channel: str value: Any = PASSTHROUGH skip_none: bool = False - mapper: Optional[Runnable] = None + mapper: Optional[Callable] = None class ChannelWrite(RunnableCallable): @@ -59,9 +57,6 @@ class ChannelWrite(RunnableCallable): self.writes = writes self.require_at_least_one_of = require_at_least_one_of - def __repr_args__(self) -> Any: - return [("writes", self.writes)] - def get_name( self, suffix: Optional[str] = None, *, name: Optional[str] = None ) -> str: @@ -82,65 +77,29 @@ class ChannelWrite(RunnableCallable): ] def _write(self, input: Any, config: RunnableConfig) -> None: - # split packets and entries - writes = [(TASKS, packet) for packet in self.writes if isinstance(packet, Send)] - entries = [ - write for write in self.writes if isinstance(write, ChannelWriteEntry) + writes = [ + ChannelWriteEntry(write.channel, input, write.skip_none, write.mapper) + if isinstance(write, ChannelWriteEntry) and write.value is PASSTHROUGH + else write + for write in self.writes ] - for entry in entries: - if entry.channel == TASKS: - raise InvalidUpdateError("Cannot write to the reserved channel TASKS") - # process entries into values - values = [ - input if write.value is PASSTHROUGH else write.value for write in entries - ] - values = [ - val if write.mapper is None else write.mapper.invoke(val, config) - for val, write in zip(values, entries) - ] - values = [ - (write.channel, val) - for val, write in zip(values, entries) - if not write.skip_none or val is not None - ] - # write packets and values self.do_write( config, - writes + values, + writes, self.require_at_least_one_of if input is not None else None, ) return input async def _awrite(self, input: Any, config: RunnableConfig) -> None: - # split packets and entries - writes = [(TASKS, packet) for packet in self.writes if isinstance(packet, Send)] - entries = [ - write for write in self.writes if isinstance(write, ChannelWriteEntry) + writes = [ + ChannelWriteEntry(write.channel, input, write.skip_none, write.mapper) + if isinstance(write, ChannelWriteEntry) and write.value is PASSTHROUGH + else write + for write in self.writes ] - for entry in entries: - if entry.channel == TASKS: - raise InvalidUpdateError("Cannot write to the reserved channel TASKS") - # process entries into values - values = [ - input if write.value is PASSTHROUGH else write.value for write in entries - ] - values = await asyncio.gather( - *( - _mk_future(val) - if write.mapper is None - else write.mapper.ainvoke(val, config) - for val, write in zip(values, entries) - ) - ) - values = [ - (write.channel, val) - for val, write in zip(values, entries) - if not write.skip_none or val is not None - ] - # write packets and values self.do_write( config, - writes + values, + writes, self.require_at_least_one_of if input is not None else None, ) return input @@ -148,9 +107,32 @@ class ChannelWrite(RunnableCallable): @staticmethod def do_write( config: RunnableConfig, - values: List[Tuple[str, Any]], + writes: Sequence[Union[ChannelWriteEntry, Send]], require_at_least_one_of: Optional[Sequence[str]] = None, ) -> None: + # validate + for w in writes: + if isinstance(w, ChannelWriteEntry): + if w.channel == TASKS: + raise InvalidUpdateError( + "Cannot write to the reserved channel TASKS" + ) + if w.value is PASSTHROUGH: + raise InvalidUpdateError("PASSTHROUGH value must be replaced") + # split packets and entries + sends = [(TASKS, packet) for packet in writes if isinstance(packet, Send)] + entries = [write for write in writes if isinstance(write, ChannelWriteEntry)] + # process entries into values + values = [ + write.mapper(write.value) if write.mapper is not None else write.value + for write in entries + ] + values = [ + (write.channel, val) + for val, write in zip(values, entries) + if not write.skip_none or val is not None + ] + # filter out SKIP_WRITE values filtered = [(chan, val) for chan, val in values if val is not SKIP_WRITE] if require_at_least_one_of is not None: if not {chan for chan, _ in filtered} & set(require_at_least_one_of): @@ -158,7 +140,7 @@ class ChannelWrite(RunnableCallable): f"Must write to at least one of {require_at_least_one_of}" ) write: TYPE_SEND = config["configurable"][CONFIG_KEY_SEND] - write(filtered) + write(sends + filtered) @staticmethod def is_writer(runnable: Runnable) -> bool: diff --git a/libs/langgraph/langgraph/utils/config.py b/libs/langgraph/langgraph/utils/config.py index 44fb29d56..461c89024 100644 --- a/libs/langgraph/langgraph/utils/config.py +++ b/libs/langgraph/langgraph/utils/config.py @@ -13,6 +13,8 @@ def patch_configurable( ) -> RunnableConfig: if config is None: return {"configurable": patch} + elif "configurable" not in config: + return {**config, "configurable": patch} else: return {**config, "configurable": {**config["configurable"], **patch}} @@ -130,7 +132,7 @@ def patch_config( Returns: RunnableConfig: The patched config. """ - config = config or {} + config = config.copy() or {} if callbacks is not None: # If we're replacing callbacks, we need to unset run_name # As that should apply only to the same run as the original callbacks diff --git a/libs/langgraph/langgraph/utils/runnable.py b/libs/langgraph/langgraph/utils/runnable.py index bbe344ed5..3f3395e25 100644 --- a/libs/langgraph/langgraph/utils/runnable.py +++ b/libs/langgraph/langgraph/utils/runnable.py @@ -2,17 +2,18 @@ import asyncio import enum import inspect import sys +from contextlib import AsyncExitStack from contextvars import copy_context from functools import partial, wraps -from typing import Any, AsyncIterator, Awaitable, Callable, Optional +from typing import Any, AsyncIterator, Awaitable, Callable, Iterator, Optional -from langchain_core.load.serializable import to_json_not_implemented from langchain_core.runnables.base import ( Runnable, RunnableConfig, RunnableLambda, RunnableLike, RunnableParallel, + RunnableSequence, ) from langchain_core.runnables.config import ( ensure_config, @@ -21,7 +22,8 @@ from langchain_core.runnables.config import ( run_in_executor, var_child_runnable_config, ) -from langchain_core.runnables.utils import accepts_config, accepts_run_manager +from langchain_core.runnables.utils import Input, Output, accepts_config +from langchain_core.tracers._streaming import _StreamingCallbackHandler from typing_extensions import TypeGuard from langgraph.utils.config import merge_configs, patch_config @@ -73,16 +75,13 @@ class RunnableCallable(Runnable): self.func = func if func is not None: self.func_accepts_config = accepts_config(func) - self.func_accepts_run_manager = accepts_run_manager(func) self.afunc = afunc if afunc is not None: self.afunc_accepts_config = accepts_config(afunc) - self.afunc_accepts_run_manager = accepts_run_manager(afunc) self.config: Optional[RunnableConfig] = {"tags": tags} if tags else None self.kwargs = kwargs self.trace = trace self.recurse = recurse - self.serialized = to_json_not_implemented(self) def __repr__(self) -> str: repr_args = { @@ -102,13 +101,15 @@ class RunnableCallable(Runnable): " via the async API (ainvoke, astream, etc.)" ) kwargs = {**self.kwargs, **kwargs} + if self.func_accepts_config: + kwargs["config"] = config config = ensure_config(merge_configs(self.config, config)) context = copy_context() if self.trace: config = ensure_config(config) callback_manager = get_callback_manager_for_config(config) run_manager = callback_manager.on_chain_start( - self.serialized, + None, input, name=config.get("run_name") or self.get_name(), run_id=config.pop("run_id", None), @@ -117,10 +118,6 @@ class RunnableCallable(Runnable): child_config = patch_config(config, callbacks=run_manager.get_child()) context = copy_context() context.run(_set_config_context, child_config) - if self.func_accepts_config: - kwargs["config"] = config - if self.func_accepts_run_manager: - kwargs["run_manager"] = run_manager ret = context.run(self.func, input, **kwargs) except BaseException as e: run_manager.on_chain_error(e) @@ -129,8 +126,6 @@ class RunnableCallable(Runnable): run_manager.on_chain_end(ret) else: context.run(_set_config_context, config) - if self.func_accepts_config: - kwargs["config"] = config ret = context.run(self.func, input, **kwargs) if isinstance(ret, Runnable) and self.recurse: return ret.invoke(input, config) @@ -142,12 +137,14 @@ class RunnableCallable(Runnable): if not self.afunc: return self.invoke(input, config) kwargs = {**self.kwargs, **kwargs} + if self.afunc_accepts_config: + kwargs["config"] = config config = ensure_config(merge_configs(self.config, config)) context = copy_context() if self.trace: callback_manager = get_async_callback_manager_for_config(config) run_manager = await callback_manager.on_chain_start( - self.serialized, + None, input, name=config.get("run_name") or self.name, run_id=config.pop("run_id", None), @@ -155,10 +152,6 @@ class RunnableCallable(Runnable): try: child_config = patch_config(config, callbacks=run_manager.get_child()) context.run(_set_config_context, child_config) - if self.afunc_accepts_config: - kwargs["config"] = config - if self.afunc_accepts_run_manager: - kwargs["run_manager"] = run_manager coro = self.afunc(input, **kwargs) if ASYNCIO_ACCEPTS_CONTEXT: ret = await asyncio.create_task(coro, context=context) @@ -171,8 +164,6 @@ class RunnableCallable(Runnable): await run_manager.on_chain_end(ret) else: context.run(_set_config_context, config) - if self.afunc_accepts_config: - kwargs["config"] = config if ASYNCIO_ACCEPTS_CONTEXT: ret = await asyncio.create_task( self.afunc(input, **kwargs), context=context @@ -236,3 +227,293 @@ def coerce_to_runnable(thing: RunnableLike, *, name: str, trace: bool) -> Runnab f"Expected a Runnable, callable or dict." f"Instead got an unsupported type: {type(thing)}" ) + + +class RunnableSeq(Runnable): + """A simpler version of RunnableSequence.""" + + def __init__( + self, + *steps: RunnableLike, + name: Optional[str] = None, + ) -> None: + """Create a new RunnableSequence. + + Args: + steps: The steps to include in the sequence. + name: The name of the Runnable. Defaults to None. + first: The first Runnable in the sequence. Defaults to None. + middle: The middle Runnables in the sequence. Defaults to None. + last: The last Runnable in the sequence. Defaults to None. + + Raises: + ValueError: If the sequence has less than 2 steps. + """ + steps_flat: list[Runnable] = [] + for step in steps: + if isinstance(step, RunnableSequence): + steps_flat.extend(step.steps) + elif isinstance(step, RunnableSeq): + steps_flat.extend(step.steps) + else: + steps_flat.append(coerce_to_runnable(step, name=None, trace=True)) + if len(steps_flat) < 2: + raise ValueError( + f"RunnableSeq must have at least 2 steps, got {len(steps_flat)}" + ) + self.steps = steps_flat + self.name = name + + def __or__( + self, + other: Any, + ) -> Runnable: + if isinstance(other, RunnableSequence): + return RunnableSeq( + *self.steps, + other.first, + *other.middle, + other.last, + name=self.name or other.name, + ) + elif isinstance(other, RunnableSeq): + return RunnableSeq( + *self.steps, + *other.steps, + name=self.name or other.name, + ) + else: + return RunnableSeq( + *self.steps, + coerce_to_runnable(other), + name=self.name, + ) + + def __ror__( + self, + other: Any, + ) -> Runnable: + if isinstance(other, RunnableSequence): + return RunnableSequence( + other.first, + *other.middle, + other.last, + *self.steps, + name=other.name or self.name, + ) + elif isinstance(other, RunnableSeq): + return RunnableSeq( + *other.steps, + *self.steps, + name=other.name or self.name, + ) + else: + return RunnableSequence( + coerce_to_runnable(other), + *self.steps, + name=self.name, + ) + + def invoke( + self, input: Input, config: Optional[RunnableConfig] = None, **kwargs: Any + ) -> Output: + # setup callbacks and context + config = ensure_config(config) + callback_manager = get_callback_manager_for_config(config) + # start the root run + run_manager = callback_manager.on_chain_start( + None, + input, + name=config.get("run_name") or self.get_name(), + run_id=config.pop("run_id", None), + ) + + # invoke all steps in sequence + try: + for i, step in enumerate(self.steps): + # mark each step as a child run + config = patch_config( + config, callbacks=run_manager.get_child(f"seq:step:{i+1}") + ) + context = copy_context() + context.run(_set_config_context, config) + if i == 0: + input = context.run(step.invoke, input, config, **kwargs) + else: + input = context.run(step.invoke, input, config) + # finish the root run + except BaseException as e: + run_manager.on_chain_error(e) + raise + else: + run_manager.on_chain_end(input) + return input + + async def ainvoke( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> Output: + # setup callbacks + config = ensure_config(config) + callback_manager = get_async_callback_manager_for_config(config) + # start the root run + run_manager = await callback_manager.on_chain_start( + None, + input, + name=config.get("run_name") or self.get_name(), + run_id=config.pop("run_id", None), + ) + + # invoke all steps in sequence + try: + for i, step in enumerate(self.steps): + # mark each step as a child run + config = patch_config( + config, callbacks=run_manager.get_child(f"seq:step:{i+1}") + ) + context = copy_context() + context.run(_set_config_context, config) + if i == 0: + coro = step.ainvoke(input, config, **kwargs) + else: + coro = step.ainvoke(input, config) + if ASYNCIO_ACCEPTS_CONTEXT: + input = await asyncio.create_task(coro, context=context) + else: + input = await asyncio.create_task(coro) + # finish the root run + except BaseException as e: + await run_manager.on_chain_error(e) + raise + else: + await run_manager.on_chain_end(input) + return input + + def stream( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> Iterator[Output]: + # setup callbacks + config = ensure_config(config) + callback_manager = get_callback_manager_for_config(config) + # start the root run + run_manager = callback_manager.on_chain_start( + None, + input, + name=config.get("run_name") or self.get_name(), + run_id=config.pop("run_id", None), + ) + + try: + # stream the last steps + # transform the input stream of each step with the next + # steps that don't natively support transforming an input stream will + # buffer input in memory until all available, and then start emitting output + for idx, step in enumerate(self.steps): + config = patch_config( + config, + callbacks=run_manager.get_child(f"seq:step:{idx+1}"), + ) + if idx == 0: + iterator = step.stream(input, config, **kwargs) + else: + iterator = step.transform(iterator, config) + if stream_handler := next( + ( + h + 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) + output: Output = None + add_supported = False + for chunk in iterator: + yield chunk + # collect final output + if output is None: + output = chunk + elif add_supported: + try: + output = output + chunk + except TypeError: + output = chunk + add_supported = False + else: + output = chunk + except BaseException as e: + run_manager.on_chain_error(e) + raise + else: + run_manager.on_chain_end(output) + + async def astream( + self, + input: Input, + config: Optional[RunnableConfig] = None, + **kwargs: Optional[Any], + ) -> AsyncIterator[Output]: + # setup callbacks + config = ensure_config(config) + callback_manager = get_async_callback_manager_for_config(config) + # start the root run + run_manager = await callback_manager.on_chain_start( + None, + input, + name=config.get("run_name") or self.get_name(), + run_id=config.pop("run_id", None), + ) + + try: + async with AsyncExitStack() as stack: + # stream the last steps + # transform the input stream of each step with the next + # steps that don't natively support transforming an input stream will + # buffer input in memory until all available, and then start emitting output + for idx, step in enumerate(self.steps): + config = patch_config( + config, + callbacks=run_manager.get_child(f"seq:step:{idx+1}"), + ) + if idx == 0: + aiterator = step.astream(input, config, **kwargs) + else: + aiterator = step.atransform(aiterator, config) + if hasattr(aiterator, "aclose"): + stack.push_async_callback(aiterator.aclose) + if stream_handler := next( + ( + h + 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( + run_manager.run_id, aiterator + ) + output: Output = None + add_supported = False + async for chunk in aiterator: + yield chunk + # collect final output + if add_supported: + try: + output = output + chunk + except TypeError: + output = chunk + add_supported = False + else: + output = chunk + except BaseException as e: + await run_manager.on_chain_error(e) + raise + else: + await run_manager.on_chain_end(output) diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index 50d6c6f89..2061c06b2 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -1884,18 +1884,22 @@ url = "../checkpoint-sqlite" [[package]] name = "langsmith" -version = "0.1.79" +version = "0.1.111" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langsmith-0.1.79-py3-none-any.whl", hash = "sha256:c7f2c23981917713b5515b773f37c84ff68a7adf803476e2ebb5adcb36a04202"}, - {file = "langsmith-0.1.79.tar.gz", hash = "sha256:d215718cfdcdf4a011126b7a3d4a37eee96d887e59ac1e628a57e24b2bfa3163"}, + {file = "langsmith-0.1.111-py3-none-any.whl", hash = "sha256:e5c702764911193c9812fe55136ae01cd0b9ddf5dff0b068ce6fd60eeddbcb40"}, + {file = "langsmith-0.1.111.tar.gz", hash = "sha256:bab24fd6125685f588d682693c4a3253e163804242829b1ff902e1a3e984a94c"}, ] [package.dependencies] +httpx = ">=0.23.0,<1" orjson = ">=3.9.14,<4.0.0" -pydantic = ">=1,<3" +pydantic = [ + {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""}, + {version = ">=2.7.4,<3.0.0", markers = "python_full_version >= \"3.12.4\""}, +] requests = ">=2,<3" [[package]] @@ -3051,6 +3055,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index 3937546f5..4699bb217 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -225,6 +225,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "left" @@ -237,6 +238,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "right" @@ -306,6 +308,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "left" @@ -318,6 +321,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "right" @@ -387,6 +391,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "get_weather" @@ -800,6 +805,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -949,6 +955,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -1081,6 +1088,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tools', @@ -1151,6 +1159,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -1300,6 +1309,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -1432,6 +1442,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tools', @@ -1502,6 +1513,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -1651,6 +1663,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -1783,6 +1796,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tools', @@ -1853,6 +1867,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -2002,6 +2017,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -2134,6 +2150,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tools', @@ -2204,6 +2221,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -2353,6 +2371,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -2485,6 +2504,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tools', @@ -2642,6 +2662,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -2723,6 +2744,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -2804,6 +2826,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -2885,6 +2908,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -2966,6 +2990,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "tools" @@ -3028,6 +3053,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "A" @@ -3040,6 +3066,7 @@ "id": [ "langgraph", "utils", + "runnable", "RunnableCallable" ], "name": "B" @@ -4661,6 +4688,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tool_one', @@ -4678,6 +4706,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tool_two:tool_two_slow', @@ -4690,6 +4719,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tool_two:tool_two_fast', @@ -4707,6 +4737,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'tool_three', @@ -5264,6 +5295,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'ask_question', @@ -5276,6 +5308,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'answer_question', @@ -5323,6 +5356,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'generate_analysts', @@ -5348,6 +5382,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'generate_sections', @@ -5413,6 +5448,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'generate_analysts', @@ -5430,6 +5466,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'conduct_interview:ask_question', @@ -5442,6 +5479,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'conduct_interview:answer_question', @@ -5459,6 +5497,7 @@ 'id': list([ 'langgraph', 'utils', + 'runnable', 'RunnableCallable', ]), 'name': 'generate_sections', diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 10018450c..19db1a992 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -1848,9 +1848,7 @@ def test_invoke_two_processes_one_in_two_out(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) one = ( - Channel.subscribe_to("input") - | add_one - | Channel.write_to(output=RunnablePassthrough(), between=RunnablePassthrough()) + Channel.subscribe_to("input") | add_one | Channel.write_to("output", "between") ) two = Channel.subscribe_to("between") | add_one | Channel.write_to("output") @@ -4824,7 +4822,7 @@ def test_message_graph( content="result for query", name="search_api", tool_call_id="tool_call123", - id="00000000-0000-4000-8000-000000000011", + id="00000000-0000-4000-8000-000000000010", ), AIMessage( content="", @@ -4841,7 +4839,7 @@ def test_message_graph( content="result for another", name="search_api", tool_call_id="tool_call456", - id="00000000-0000-4000-8000-000000000020", + id="00000000-0000-4000-8000-000000000018", ), AIMessage(content="answer", id="ai3"), ] @@ -4866,7 +4864,7 @@ def test_message_graph( content="result for query", name="search_api", tool_call_id="tool_call123", - id="00000000-0000-4000-8000-000000000036", + id="00000000-0000-4000-8000-000000000033", ) ] }, @@ -4889,7 +4887,7 @@ def test_message_graph( content="result for another", name="search_api", tool_call_id="tool_call456", - id="00000000-0000-4000-8000-000000000045", + id="00000000-0000-4000-8000-000000000041", ) ] }, @@ -5558,7 +5556,7 @@ def test_root_graph( content="result for query", name="search_api", tool_call_id="tool_call123", - id="00000000-0000-4000-8000-000000000011", + id="00000000-0000-4000-8000-000000000010", ), AIMessage( content="", @@ -5575,7 +5573,7 @@ def test_root_graph( content="result for another", name="search_api", tool_call_id="tool_call456", - id="00000000-0000-4000-8000-000000000020", + id="00000000-0000-4000-8000-000000000018", ), AIMessage(content="answer", id="ai3"), ] @@ -5600,7 +5598,7 @@ def test_root_graph( content="result for query", name="search_api", tool_call_id="tool_call123", - id="00000000-0000-4000-8000-000000000036", + id="00000000-0000-4000-8000-000000000033", ) ] }, @@ -5623,7 +5621,7 @@ def test_root_graph( content="result for another", name="search_api", tool_call_id="tool_call456", - id="00000000-0000-4000-8000-000000000045", + id="00000000-0000-4000-8000-000000000041", ) ] }, @@ -6223,7 +6221,7 @@ def test_root_graph( "__root__": [ HumanMessage( content="what is weather in sf", - id="00000000-0000-4000-8000-000000000077", + id="00000000-0000-4000-8000-000000000070", ), AIMessage( content="", @@ -6239,12 +6237,12 @@ def test_root_graph( ToolMessage( content="result for a different query", name="search_api", - id="00000000-0000-4000-8000-000000000091", + id="00000000-0000-4000-8000-000000000082", tool_call_id="tool_call123", ), AIMessage(content="answer", id="ai2"), AIMessage( - content="an extra message", id="00000000-0000-4000-8000-000000000101" + content="an extra message", id="00000000-0000-4000-8000-000000000091" ), HumanMessage(content="what is weather in la"), ], diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 9d9c975d4..73de0b953 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -2087,9 +2087,7 @@ async def test_invoke_two_processes_one_in_two_out(mocker: MockerFixture) -> Non add_one = mocker.Mock(side_effect=lambda x: x + 1) one = ( - Channel.subscribe_to("input") - | add_one - | Channel.write_to(output=RunnablePassthrough(), between=RunnablePassthrough()) + Channel.subscribe_to("input") | add_one | Channel.write_to("output", "between") ) two = Channel.subscribe_to("between") | add_one | Channel.write_to("output") From cf345ef716758906c3cc5d868c4ecc345f6b5565 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 12:07:02 -0700 Subject: [PATCH 250/287] Store loop in async bg exec init --- libs/langgraph/langgraph/pregel/executor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/executor.py b/libs/langgraph/langgraph/pregel/executor.py index 3a4175cac..981ebbf7d 100644 --- a/libs/langgraph/langgraph/pregel/executor.py +++ b/libs/langgraph/langgraph/pregel/executor.py @@ -99,6 +99,7 @@ class AsyncBackgroundExecutor(AsyncContextManager): self.context_not_supported = sys.version_info < (3, 11) self.tasks: dict[asyncio.Task, bool] = {} self.sentinel = object() + self.loop = asyncio.get_running_loop() def submit( self, @@ -110,9 +111,9 @@ class AsyncBackgroundExecutor(AsyncContextManager): ) -> asyncio.Task[T]: coro = fn(*args, **kwargs) if self.context_not_supported: - task = asyncio.create_task(coro, name=__name__) + task = self.loop.create_task(coro, name=__name__) else: - task = asyncio.create_task(coro, name=__name__, context=copy_context()) + task = self.loop.create_task(coro, name=__name__, context=copy_context()) self.tasks[task] = __cancel_on_exit__ task.add_done_callback(self.done) return task From 4efab93e2fa65b6502b97c22bcb0bd4c1bb4fafc Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 12:07:19 -0700 Subject: [PATCH 251/287] Use pseudo rng for uuid6 generation --- libs/checkpoint/langgraph/checkpoint/base/id.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/checkpoint/langgraph/checkpoint/base/id.py b/libs/checkpoint/langgraph/checkpoint/base/id.py index 459ac7995..807dc8ad0 100644 --- a/libs/checkpoint/langgraph/checkpoint/base/id.py +++ b/libs/checkpoint/langgraph/checkpoint/base/id.py @@ -3,7 +3,7 @@ https://github.com/oittaa/uuid6-python/blob/main/src/uuid6/__init__.py#L95 Bundled in to avoid install issues with uuid6 package """ -import secrets +import random import time import uuid from typing import Optional, Tuple @@ -96,9 +96,9 @@ def uuid6(node: Optional[int] = None, clock_seq: Optional[int] = None) -> UUID: timestamp = _last_v6_timestamp + 1 _last_v6_timestamp = timestamp if clock_seq is None: - clock_seq = secrets.randbits(14) # instead of stable storage + clock_seq = random.getrandbits(14) # instead of stable storage if node is None: - node = secrets.randbits(48) + node = random.getrandbits(48) time_high_and_time_mid = (timestamp >> 12) & 0xFFFFFFFFFFFF time_low_and_version = timestamp & 0x0FFF uuid_int = time_high_and_time_mid << 80 From b68f9211c2ee7de2c21f75a501aba47db4f71995 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 12:13:38 -0700 Subject: [PATCH 252/287] Run branch.reader in bg thread --- libs/langgraph/langgraph/graph/graph.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/graph/graph.py b/libs/langgraph/langgraph/graph/graph.py index 41175c170..21868bf7c 100644 --- a/libs/langgraph/langgraph/graph/graph.py +++ b/libs/langgraph/langgraph/graph/graph.py @@ -1,3 +1,4 @@ +import asyncio import logging from collections import defaultdict from typing import ( @@ -97,7 +98,7 @@ class Branch(NamedTuple): writer: Callable[[list[str], RunnableConfig], Optional[Runnable]], ) -> Runnable: if reader: - value = reader(config) + value = await asyncio.to_thread(reader, config) # passthrough additional keys from node to branch # only doable when using dict states if isinstance(value, dict) and isinstance(input, dict): From 67a164ca2de8e19b1944b63310057b44a153ae02 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 12:18:42 -0700 Subject: [PATCH 253/287] Run loop tick in bg thread - prepare_next_tasks, apply_writes do some cpu-heavy work --- libs/langgraph/langgraph/pregel/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 089b729fa..2a132381b 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -1419,7 +1419,8 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]): # channel updates from step N are only visible in step N+1 # channels are guaranteed to be immutable for the duration of the step, # with channel updates applied only at the transition between steps - while loop.tick( + while await asyncio.to_thread( + loop.tick, input_keys=self.input_channels, interrupt_before=interrupt_before, interrupt_after=interrupt_after, From 26b4d9139ea26b405c5880fa6454dae8a8da44a0 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 12:19:14 -0700 Subject: [PATCH 254/287] Avoid using json encoding when generating task ids --- libs/langgraph/langgraph/pregel/algo.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 3e465f115..c3dfd1b80 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -1,4 +1,3 @@ -import json from collections import defaultdict, deque from functools import partial from typing import ( @@ -270,16 +269,19 @@ def prepare_next_tasks( checkpointer: Optional[BaseCheckpointSaver] = None, manager: Union[None, ParentRunManager, AsyncParentRunManager] = None, ) -> Union[list[PregelTask], list[PregelExecutableTask]]: + checkpoint_id = UUID(checkpoint["id"]) configurable = config.get("configurable", {}) parent_ns = configurable.get("checkpoint_ns", "") tasks: Union[list[PregelTask], list[PregelExecutableTask]] = [] # Consume pending packets for packet in checkpoint["pending_sends"]: if not isinstance(packet, Send): - logger.warn(f"Ignoring invalid packet type {type(packet)} in pending sends") + logger.warning( + f"Ignoring invalid packet type {type(packet)} in pending sends" + ) continue if packet.node not in processes: - logger.warn(f"Ignoring unknown node name {packet.node} in pending sends") + logger.warning(f"Ignoring unknown node name {packet.node} in pending sends") continue # create task id triggers = [TASKS] @@ -293,7 +295,12 @@ def prepare_next_tasks( f"{parent_ns}{NS_SEP}{packet.node}" if parent_ns else packet.node ) task_id = str( - uuid5(UUID(checkpoint["id"]), json.dumps((checkpoint_ns, metadata))) + uuid5( + checkpoint_id, + "".join( + (checkpoint_ns, str(step), packet.node, *triggers, str(len(tasks))) + ), + ) ) if for_execution: proc = processes[packet.node] @@ -397,8 +404,10 @@ def prepare_next_tasks( checkpoint_ns = f"{parent_ns}{NS_SEP}{name}" if parent_ns else name task_id = str( uuid5( - UUID(checkpoint["id"]), - json.dumps((checkpoint_ns, metadata)), + checkpoint_id, + "".join( + (checkpoint_ns, str(step), name, *triggers, str(len(tasks))) + ), ) ) From b9361d69b4eb64694d9078ddc8ead26a08e4afc4 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 12:30:27 -0700 Subject: [PATCH 255/287] Try to fix test --- libs/langgraph/tests/test_pregel_async.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 73de0b953..7afd292ae 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -514,6 +514,7 @@ async def test_cancel_graph_astream_events_v2(checkpointer_name: Optional[str]) if chunk["event"] == "on_chain_stream" and not chunk["parent_ids"]: got_event = True assert chunk["data"]["chunk"] == {"alittlewhile": {"value": 2}} + await asyncio.sleep(0) break # did break From 3a1b90c8821288e21486e92f200b75e0eea81c63 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 12:34:42 -0700 Subject: [PATCH 256/287] Fix --- libs/langgraph/tests/test_pregel_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 7afd292ae..f0a81ceb0 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -514,7 +514,7 @@ async def test_cancel_graph_astream_events_v2(checkpointer_name: Optional[str]) if chunk["event"] == "on_chain_stream" and not chunk["parent_ids"]: got_event = True assert chunk["data"]["chunk"] == {"alittlewhile": {"value": 2}} - await asyncio.sleep(0) + await asyncio.sleep(0.1) break # did break From dc10b3aa8e4f6eabfebc160654a2954548a38734 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 12:56:32 -0700 Subject: [PATCH 257/287] Add test to confirm only one root run is produced --- libs/langgraph/tests/test_pregel.py | 8 +++++++- libs/langgraph/tests/test_pregel_async.py | 8 ++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 19db1a992..74dfa2e85 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -9823,12 +9823,18 @@ def test_send_to_nested_graphs( graph = builder.compile(checkpointer=checkpointer) config = {"configurable": {"thread_id": "1"}} + tracer = FakeTracer() # invoke and pause at nested interrupt - assert graph.invoke({"subjects": ["cats", "dogs"]}, config=config) == { + assert graph.invoke( + {"subjects": ["cats", "dogs"]}, config={**config, "callbacks": [tracer]} + ) == { "subjects": ["cats", "dogs"], "jokes": [], } + assert len(tracer.runs) == 1, "Should produce exactly 1 root run" + + # check state outer_state = graph.get_state(config) assert outer_state == StateSnapshot( values={"subjects": ["cats", "dogs"], "jokes": []}, diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index f0a81ceb0..f33532c0c 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -8467,15 +8467,19 @@ async def test_send_to_nested_graphs(checkpointer_name: str) -> None: async with awith_checkpointer(checkpointer_name) as checkpointer: graph = builder.compile(checkpointer=checkpointer) config = {"configurable": {"thread_id": "1"}} + tracer = FakeTracer() # invoke and pause at nested interrupt - assert await graph.ainvoke({"subjects": ["cats", "dogs"]}, config=config) == { + assert await graph.ainvoke( + {"subjects": ["cats", "dogs"]}, config={**config, "callbacks": [tracer]} + ) == { "subjects": ["cats", "dogs"], "jokes": [], } + assert len(tracer.runs) == 1, "Should produce exactly 1 root run" + # check state outer_state = await graph.aget_state(config) - assert outer_state == StateSnapshot( values={"subjects": ["cats", "dogs"], "jokes": []}, tasks=( From b8d8a879d8d214f3e851f3f238d5eab3199b45a3 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 14:31:20 -0700 Subject: [PATCH 258/287] Fix ids in debug task events --- libs/langgraph/langgraph/pregel/debug.py | 10 ++-------- libs/langgraph/poetry.lock | 6 +++--- libs/langgraph/tests/test_pregel.py | 20 ++++++++++---------- libs/langgraph/tests/test_pregel_async.py | 22 +++++++++++----------- 4 files changed, 26 insertions(+), 32 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index 9b20c3c3a..f24fad6e3 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -1,10 +1,9 @@ -import json from collections import defaultdict from dataclasses import asdict from datetime import datetime, timezone from pprint import pformat from typing import Any, Iterator, Literal, Mapping, Optional, Sequence, TypedDict, Union -from uuid import UUID, uuid5 +from uuid import UUID from langchain_core.runnables.config import RunnableConfig from langchain_core.utils.input import get_bolded_text, get_colored_text @@ -82,17 +81,12 @@ def map_debug_tasks( if task.config is not None and TAG_HIDDEN in task.config.get("tags", []): continue - metadata = task.config["metadata"].copy() - metadata.pop("checkpoint_id", None) - yield { "type": "task", "timestamp": ts, "step": step, "payload": { - "id": str( - uuid5(TASK_NAMESPACE, json.dumps((task.name, step, metadata))) - ), + "id": task.id, "name": task.name, "input": task.input, "triggers": task.triggers, diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index 2061c06b2..63859ebfc 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -1884,13 +1884,13 @@ url = "../checkpoint-sqlite" [[package]] name = "langsmith" -version = "0.1.111" +version = "0.1.112" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langsmith-0.1.111-py3-none-any.whl", hash = "sha256:e5c702764911193c9812fe55136ae01cd0b9ddf5dff0b068ce6fd60eeddbcb40"}, - {file = "langsmith-0.1.111.tar.gz", hash = "sha256:bab24fd6125685f588d682693c4a3253e163804242829b1ff902e1a3e984a94c"}, + {file = "langsmith-0.1.112-py3-none-any.whl", hash = "sha256:38ecc62df35e55e5aa9a05ecf1f1483d5a4db3275b9da4876ae27364714b3717"}, + {file = "langsmith-0.1.112.tar.gz", hash = "sha256:08c47a9497261c332bc4b15e2930d8e751310e63652ecdc78382a5dd07cb42e2"}, ] [package.dependencies] diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 74dfa2e85..690b2647c 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -1058,7 +1058,7 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", + "id": AnyStr(), "name": "one", "input": 2, "triggers": ["input"], @@ -1069,7 +1069,7 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", + "id": AnyStr(), "name": "two", "input": [12], "triggers": ["inbox"], @@ -1104,7 +1104,7 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", + "id": AnyStr(), "name": "two", "input": [3], "triggers": ["inbox"], @@ -6333,7 +6333,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", + "id": AnyStr(), "name": "rewrite_query", "input": {"query": "what is weather in sf", "docs": []}, "triggers": ["start:rewrite_query"], @@ -6364,7 +6364,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", + "id": AnyStr(), "name": "retriever_one", "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], @@ -6378,7 +6378,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", + "id": AnyStr(), "name": "retriever_two", "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], @@ -6437,7 +6437,7 @@ def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 3, "payload": { - "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", + "id": AnyStr(), "name": "qa", "input": { "query": "query: what is weather in sf", @@ -6861,7 +6861,7 @@ def test_branch_then( "timestamp": AnyStr(), "step": 1, "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "id": AnyStr(), "name": "prepare", "input": {"my_key": "value", "market": "DE"}, "triggers": ["start:prepare"], @@ -6914,7 +6914,7 @@ def test_branch_then( "timestamp": AnyStr(), "step": 2, "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "id": AnyStr(), "name": "tool_two_slow", "input": {"my_key": "value prepared", "market": "DE"}, "triggers": ["branch:prepare:condition:tool_two_slow"], @@ -6967,7 +6967,7 @@ def test_branch_then( "timestamp": AnyStr(), "step": 3, "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "id": AnyStr(), "name": "finish", "input": {"my_key": "value prepared slow", "market": "DE"}, "triggers": ["branch:prepare:condition::then"], diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index f33532c0c..a7e4b77e5 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -1306,7 +1306,7 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "2687f72c-e3a8-5f6f-9afa-047cbf24e923", + "id": AnyStr(), "name": "one", "input": 2, "triggers": ["input"], @@ -1317,7 +1317,7 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 0, "payload": { - "id": "18f52f6a-828d-58a1-a501-53cc0c7af33e", + "id": AnyStr(), "name": "two", "input": [12], "triggers": ["inbox"], @@ -1352,7 +1352,7 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "871d6e74-7bb3-565f-a4fe-cef4b8f19b62", + "id": AnyStr(), "name": "two", "input": [3], "triggers": ["inbox"], @@ -5019,7 +5019,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "592f3430-c17c-5d1c-831f-fecebb2c05bf", + "id": AnyStr(), "name": "rewrite_query", "input": {"query": "what is weather in sf", "docs": []}, "triggers": ["start:rewrite_query"], @@ -5050,7 +5050,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "7db5e9d8-e132-5079-ab99-ced15e67d48b", + "id": AnyStr(), "name": "retriever_one", "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], @@ -5064,7 +5064,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "96965ed0-2c10-52a1-86eb-081ba6de73b2", + "id": AnyStr(), "name": "retriever_two", "input": {"query": "query: what is weather in sf", "docs": []}, "triggers": ["rewrite_query"], @@ -5123,7 +5123,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None: "timestamp": AnyStr(), "step": 3, "payload": { - "id": "8959fb57-d0f5-5725-9ac4-ec1c554fb0a0", + "id": AnyStr(), "name": "qa", "input": { "query": "query: what is weather in sf", @@ -5482,7 +5482,7 @@ async def test_branch_then(checkpointer_name: str) -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "7b7b0713-e958-5d07-803c-c9910a7cc162", + "id": AnyStr(), "name": "prepare", "input": {"my_key": "value", "market": "DE"}, "triggers": ["start:prepare"], @@ -5537,7 +5537,7 @@ async def test_branch_then(checkpointer_name: str) -> None: "timestamp": AnyStr(), "step": 2, "payload": { - "id": "dd9f2fa5-ccfa-5d12-81ec-942563056a08", + "id": AnyStr(), "name": "tool_two_slow", "input": {"my_key": "value prepared", "market": "DE"}, "triggers": ["branch:prepare:condition:tool_two_slow"], @@ -5590,7 +5590,7 @@ async def test_branch_then(checkpointer_name: str) -> None: "timestamp": AnyStr(), "step": 3, "payload": { - "id": "9b590c54-15ef-54b1-83a7-140d27b0bc52", + "id": AnyStr(), "name": "finish", "input": {"my_key": "value prepared slow", "market": "DE"}, "triggers": ["branch:prepare:condition::then"], @@ -5719,7 +5719,7 @@ async def test_branch_then(checkpointer_name: str) -> None: "timestamp": AnyStr(), "step": 1, "payload": { - "id": "1a591be4-f85c-558f-8d00-1ccac0d1877f", + "id": AnyStr(), "name": "prepare", "input": {"my_key": "value", "market": "DE"}, "triggers": ["start:prepare"], From eb88dfefa3777906713bc5c08b9d8ad0b189a796 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 14:31:42 -0700 Subject: [PATCH 259/287] Speed improvements to local_read, emit output --- libs/langgraph/langgraph/pregel/algo.py | 29 +++++++++++++------ libs/langgraph/langgraph/pregel/debug.py | 36 +++++++++--------------- libs/langgraph/langgraph/pregel/io.py | 11 ++++---- libs/langgraph/langgraph/pregel/loop.py | 24 ++++++++-------- 4 files changed, 53 insertions(+), 47 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index c3dfd1b80..044ffbbf3 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -102,17 +102,25 @@ def local_read( ) -> Union[dict[str, Any], Any]: if isinstance(select, str): managed_keys = [] + for c, _ in task.writes: + if c == select: + updated = {c} + break + else: + updated = set() else: managed_keys = [k for k in select if k in managed] select = [k for k in select if k not in managed] - if fresh: - new_checkpoint = create_checkpoint(copy_checkpoint(checkpoint), channels, -1) - with ChannelsManager(channels, new_checkpoint, config, skip_context=True) as ( - channels, - _, - ): - apply_writes(new_checkpoint, channels, [task], None) - values = read_channels(channels, select) + updated = set(select).intersection(c for c, _ in task.writes) + if fresh and updated: + with ChannelsManager( + {k: v for k, v in channels.items() if k in updated}, + checkpoint, + config, + skip_context=True, + ) as (local_channels, _): + apply_writes(copy_checkpoint(checkpoint), local_channels, [task], None) + values = read_channels({**channels, **local_channels}, select) else: values = read_channels(channels, select) if managed_keys: @@ -171,7 +179,10 @@ def apply_writes( # Consume all channels that were read for chan in { - chan for task in tasks for chan in task.triggers if chan not in RESERVED + chan + for task in tasks + for chan in task.triggers + if chan not in RESERVED and chan in channels }: if channels[chan].consume(): if get_next_version is not None: diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index f24fad6e3..cb6e45e0d 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -96,33 +96,25 @@ def map_debug_tasks( def map_debug_task_results( step: int, - tasks: list[tuple[PregelExecutableTask, Sequence[tuple[str, Any]]]], + task_tup: tuple[PregelExecutableTask, Sequence[tuple[str, Any]]], stream_keys: Union[str, Sequence[str]], ) -> Iterator[DebugOutputTaskResult]: stream_channels_list = ( [stream_keys] if isinstance(stream_keys, str) else stream_keys ) - ts = datetime.now(timezone.utc) - for task, writes in tasks: - if task.config is not None and TAG_HIDDEN in task.config.get("tags", []): - continue - - metadata = task.config["metadata"].copy() - metadata.pop("checkpoint_id", None) - # TODO: make task IDs deterministic in tests and reuse task IDs for payload ID - - yield { - "type": "task_result", - "timestamp": ts.isoformat(), - "step": step, - "payload": { - "id": task.id, - "name": task.name, - "error": next((w[1] for w in writes if w[0] == ERROR), None), - "result": [w for w in writes if w[0] in stream_channels_list], - "interrupts": [asdict(w[1]) for w in writes if w[0] == INTERRUPT], - }, - } + task, writes = task_tup + yield { + "type": "task_result", + "timestamp": datetime.now(timezone.utc).isoformat(), + "step": step, + "payload": { + "id": task.id, + "name": task.name, + "error": next((w[1] for w in writes if w[0] == ERROR), None), + "result": [w for w in writes if w[0] in stream_channels_list], + "interrupts": [asdict(w[1]) for w in writes if w[0] == INTERRUPT], + }, + } def map_debug_checkpoint( diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/io.py index 8568e8781..286282f1d 100644 --- a/libs/langgraph/langgraph/pregel/io.py +++ b/libs/langgraph/langgraph/pregel/io.py @@ -107,26 +107,27 @@ def map_output_updates( (t, ww) for t, ww in tasks if (not t.config or TAG_HIDDEN not in t.config.get("tags")) - and all(k not in (ERROR, INTERRUPT) for k, _ in ww) + and ww[0][0] != ERROR + and ww[0][0] != INTERRUPT ] if not output_tasks: return if isinstance(output_channels, str): - updated = [ + updated = ( (task.name, value) for task, writes in output_tasks for chan, value in writes if chan == output_channels - ] + ) else: - updated = [ + updated = ( ( task.name, {chan: value for chan, value in task.writes if chan in output_channels}, ) for task, writes in output_tasks if any(chan in output_channels for chan, _ in writes) - ] + ) grouped = {t.name: [] for t, _ in output_tasks} for node, value in updated: grouped[node].append(value) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index d2052b17a..703882ab2 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -45,6 +45,7 @@ from langgraph.constants import ( ERROR, INPUT, INTERRUPT, + TAG_HIDDEN, ) from langgraph.errors import EmptyInputError, GraphInterrupt from langgraph.managed.base import ( @@ -437,14 +438,11 @@ class PregelLoop: if isinstance(self.stream_keys, str) else self.stream_keys, ) + # create new checkpoint + self.checkpoint = create_checkpoint(self.checkpoint, self.channels, self.step) # bail if no checkpointer if self._checkpointer_put_after_previous is not None: - # create new checkpoint self.checkpoint_metadata = metadata - self.checkpoint = create_checkpoint( - self.checkpoint, self.channels, self.step - ) - self.checkpoint_config = { **self.checkpoint_config, "configurable": { @@ -459,7 +457,6 @@ class PregelLoop: new_versions = get_new_channel_versions( self.checkpoint_previous_versions, channel_versions ) - self.checkpoint_previous_versions = channel_versions # save it, without blocking @@ -510,15 +507,20 @@ class PregelLoop: self, task_id: str, writes: Sequence[tuple[str, Any]], *, cached: bool = False ) -> None: if task := next((t for t in self.tasks if t.id == task_id), None): - self._emit( - (self.config["configurable"].get("checkpoint_ns", ""), "updates", v) - for v in map_output_updates(self.output_keys, [(task, writes)], cached) - ) + if task.config is not None and TAG_HIDDEN in task.config.get("tags"): + return + if writes[0][0] != ERROR and writes[0][0] != INTERRUPT: + self._emit( + (self.config["configurable"].get("checkpoint_ns", ""), "updates", v) + for v in map_output_updates( + self.output_keys, [(task, writes)], cached + ) + ) if not cached: self._emit( (self.config["configurable"].get("checkpoint_ns", ""), "debug", v) for v in map_debug_task_results( - self.step, [(task, writes)], self.stream_keys + self.step, (task, writes), self.stream_keys ) ) From f66493f6db3cff30ceab660fdcdf90418ae13295 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 4 Sep 2024 14:37:31 -0700 Subject: [PATCH 260/287] Lint --- libs/langgraph/langgraph/pregel/algo.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 044ffbbf3..459f67b1a 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -19,12 +19,7 @@ from langchain_core.callbacks.manager import AsyncParentRunManager, ParentRunMan from langchain_core.runnables.config import RunnableConfig from langgraph.channels.base import BaseChannel -from langgraph.checkpoint.base import ( - BaseCheckpointSaver, - Checkpoint, - copy_checkpoint, - create_checkpoint, -) +from langgraph.checkpoint.base import BaseCheckpointSaver, Checkpoint, copy_checkpoint from langgraph.constants import ( CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_CHECKPOINTER, From 82c989feeaa88e35ef5579d8720e20a6a6928e74 Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Wed, 4 Sep 2024 14:57:06 -0700 Subject: [PATCH 261/287] docs: subgraphs manage state (#1543) --- docs/_scripts/copy_notebooks.py | 1 + docs/docs/how-tos/index.md | 6 +- docs/mkdocs.yml | 4 +- examples/subgraphs-manage-state.ipynb | 868 ++++++++++++++++++++++++++ examples/time-travel.ipynb | 270 +++++++- 5 files changed, 1113 insertions(+), 36 deletions(-) create mode 100644 examples/subgraphs-manage-state.ipynb diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 1a3af4487..402ecfc01 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -30,6 +30,7 @@ _MANUAL = { "input_output_schema.ipynb", "pass_private_state.ipynb", "memory/manage-conversation-history.ipynb", + "subgraphs-manage-state.ipynb", "memory/delete-messages.ipynb", "memory/add-summary-conversation-history.ipynb", "persistence_postgres.ipynb", diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index bcd1b70a6..d57333b6d 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -12,7 +12,6 @@ Welcome to the LangGraph how-to guides! These guides provide practical, step-by- LangGraph is known for being a highly controllable agent framework. These how-to guides show how to achieve that controllability. -- [How to create subgraphs](subgraph.ipynb) - [How to create branches for parallel execution](branching.ipynb) - [How to create map-reduce branches for parallel execution](map-reduce.ipynb) - [How to control graph recursion limit](recursion-limit.ipynb) @@ -64,6 +63,11 @@ These guides show how to use different streaming modes. - [How to pass config to tools](pass-config-to-tools.ipynb) - [How to handle large numbers of tools](many-tools.ipynb) +## Subgraphs + +- [How to create subgraphs](subgraph.ipynb) +- [How to manage state in subgraphs](subgraphs-manage-state.ipynb) + ## State Management - [Use Pydantic model as state](state-model.ipynb) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 56a9435b2..f9c8138a8 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -126,7 +126,6 @@ nav: - "How-to Guides": - "how-tos/index.md" - Controllability: - - Create subgraphs: how-tos/subgraph.ipynb - Create branches for parallel execution: how-tos/branching.ipynb - Create map-reduce branches for parallel execution: how-tos/map-reduce.ipynb - Control graph recursion limit: how-tos/recursion-limit.ipynb @@ -161,6 +160,9 @@ nav: - Pass graph state to tools: how-tos/pass-run-time-values-to-tools.ipynb - Pass config to tools: how-tos/pass-config-to-tools.ipynb - Handle many tools: how-tos/many-tools.ipynb + - Subgraphs: + - Create subgraphs: how-tos/subgraph.ipynb + - Manage state in subgraphs: how-tos/subgraphs-manage-state.ipynb - State Management: - Use Pydantic model as state: how-tos/state-model.ipynb - Use a context object in state: how-tos/state-context-key.ipynb diff --git a/examples/subgraphs-manage-state.ipynb b/examples/subgraphs-manage-state.ipynb new file mode 100644 index 000000000..a018e6321 --- /dev/null +++ b/examples/subgraphs-manage-state.ipynb @@ -0,0 +1,868 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to manage state in subgraphs\n", + "\n", + "For more complex systems, sub-graphs are a useful design principle. Sub-graphs allow you to create and manage different states in different parts of your graph. This allows you build things like [multi-agent teams](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/), where each team can track its own separate state.\n", + "\n", + "In this how-to guide we will cover how to manage the persisted state in subgraphs. This will enable a lot of the human-in-the-loop interaction patterns.\n", + "\n", + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install -U langgraph" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we need to set API keys for OpenAI (the LLM we will use):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define SubGraph\n", + "\n", + "First, let's set up our subgraph. For this, we will create a simple graph that can get the weather for a specific city. We will compile this graph with a [breakpoint](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/) before the `weather_node`:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END, START, MessagesState\n", + "from langchain_core.tools import tool\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "\n", + "@tool\n", + "def get_weather(city: str):\n", + " \"\"\"Get the weather for a specific city\"\"\"\n", + " return f\"It's sunny in {city}!\"\n", + "\n", + "raw_model = ChatOpenAI()\n", + "model = raw_model.with_structured_output(get_weather)\n", + "\n", + "\n", + "class SubGraphState(MessagesState):\n", + " city: str\n", + "\n", + "\n", + "def model_node(state: SubGraphState):\n", + " result = model.invoke(state['messages'])\n", + " return {\"city\": result[\"city\"]}\n", + "\n", + "def weather_node(state: SubGraphState):\n", + " result = get_weather.invoke({\"city\": state['city']})\n", + " return {\"messages\": [{\"role\": \"assistant\", \"content\": result}]}\n", + "\n", + "\n", + "subgraph = StateGraph(SubGraphState)\n", + "subgraph.add_node(model_node)\n", + "subgraph.add_node(weather_node)\n", + "subgraph.add_edge(START, \"model_node\")\n", + "subgraph.add_edge(\"model_node\", \"weather_node\")\n", + "subgraph.add_edge(\"weather_node\", END)\n", + "subgraph = subgraph.compile(interrupt_before=[\"weather_node\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define Parent Graph\n", + "\n", + "We can now setup the overall graph. This graph will first route to the subgraph if it needs to get the weather, otherwise it will route to a normal LLM." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Literal\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "\n", + "memory = MemorySaver()\n", + "\n", + "\n", + "class RouterState(MessagesState):\n", + " route: Literal[\"weather\", \"other\"]\n", + "\n", + "\n", + "class Router(TypedDict):\n", + " route: Literal[\"weather\", \"other\"]\n", + "\n", + "router_model = raw_model.with_structured_output(Router)\n", + " \n", + "def router_node(state: RouterState):\n", + " system_message = \"Classify the incoming query as either about weather or not.\"\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + state['messages']\n", + " route = router_model.invoke(messages)\n", + " return {\"route\": route['route']}\n", + "\n", + "\n", + "def normal_llm_node(state: RouterState):\n", + " response = raw_model.invoke(state['messages'])\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "def route_after_prediction(state: RouterState) -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n", + " if state['route'] == \"weather\":\n", + " return \"weather_graph\"\n", + " else:\n", + " return \"normal_llm_node\"\n", + "\n", + "\n", + "graph = StateGraph(RouterState)\n", + "graph.add_node(router_node)\n", + "graph.add_node(normal_llm_node)\n", + "graph.add_node(\"weather_graph\", subgraph)\n", + "graph.add_edge(START, \"router_node\")\n", + "graph.add_conditional_edges(\"router_node\", route_after_prediction)\n", + "graph.add_edge(\"normal_llm_node\", END)\n", + "graph.add_edge(\"weather_graph\", END)\n", + "graph = graph.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "# Setting xray to 1 will show the internal structure of the nested graph\n", + "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's test this out with a normal query to make sure it works as intended!" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'router_node': {'route': 'other'}}\n", + "{'normal_llm_node': {'messages': [AIMessage(content='Hello! How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 9, 'prompt_tokens': 9, 'total_tokens': 18}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-9730e690-8cbd-4ba0-a962-3f8a4e848ef9-0', usage_metadata={'input_tokens': 9, 'output_tokens': 9, 'total_tokens': 18})]}}\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"hi!\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"updates\"):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Great! We didn't ask about the weather, so we got a normal response from the LLM.\n", + "\n", + "## Resuming from breakpoints\n", + "\n", + "Let's now look at what happens with breakpoints. Let's invoke it with a query that should get routed to the weather subgraph where we have the interrupt node." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'router_node': {'route': 'weather'}}\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"updates\"):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the graph stream doesn't include subgraph events. If we want to stream subgraph events, we can pass `subgraphs=True` and get back subgraph events like so:" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "((), {'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd')]})\n", + "((), {'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd')], 'route': 'weather'})\n", + "(('weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3',), {'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd')]})\n", + "(('weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3',), {'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd')], 'city': 'San Francisco'})\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"3\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"values\", subgraphs=True):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we get the state now, we can see that it's paused on `weather_graph`" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('weather_graph',)" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state = graph.get_state(config)\n", + "state.next" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we look at the pending tasks for our current state, we can see that we have one task named `weather_graph`, which corresponds to the subgraph task." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(PregelTask(id='99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3', name='weather_graph', error=None, interrupts=(), state={'configurable': {'thread_id': '3', 'checkpoint_ns': 'weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3'}}),)" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state.tasks" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However since we got the state using the config of the parent graph, we don't have access to the subgraph state. If you look at the `state` value of the `PregelTask` above you will note that it is simply the configuration of the parent graph. If we want to actually populate the subgraph state, we can pass in `subgraphs=True` to `get_state` like so:" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PregelTask(id='99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3', name='weather_graph', error=None, interrupts=(), state=StateSnapshot(values={'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd')], 'city': 'San Francisco'}, next=('weather_node',), config={'configurable': {'thread_id': '3', 'checkpoint_ns': 'weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3', 'checkpoint_id': '1ef6a48a-018f-638c-8001-a7af39dcd6ee', 'checkpoint_map': {'': '1ef6a489-fddc-6208-8001-5e02ff54dfba', 'weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3': '1ef6a48a-018f-638c-8001-a7af39dcd6ee'}}}, metadata={'source': 'loop', 'writes': {'model_node': {'city': 'San Francisco'}}, 'step': 1, 'parents': {'': '1ef6a489-fddc-6208-8001-5e02ff54dfba'}}, created_at='2024-09-03T23:02:42.795391+00:00', parent_config={'configurable': {'thread_id': '3', 'checkpoint_ns': 'weather_graph:99f49d5c-9d1a-5e00-b2fc-1f1ade30dec3', 'checkpoint_id': '1ef6a489-fded-6936-8000-c96152586915'}}, tasks=(PregelTask(id='c153ac13-b9a5-543a-8044-3b3c852fd0bc', name='weather_node', error=None, interrupts=(), state=None),)))" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state = graph.get_state(config, subgraphs=True)\n", + "state.tasks[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we have access to the subgraph state! If you look at the `state` value of the `PregelTask` you can see that it has all the information we need, like the next node (`weather_node`) and the current state values (e.g. `city`).\n", + "\n", + "To resume execution, we can just invoke the outer graph as normal:" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'weather_graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='ad42a2dc-57c5-4aae-b616-6a86ca6ee7bd'), AIMessage(content=\"It's sunny in San Francisco!\", id='07b513fa-30af-4ee4-83e4-2af8f6d133bd')]}}\n" + ] + } + ], + "source": [ + "for update in graph.stream(None, config=config, stream_mode=\"updates\"):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Modifying state\n", + "\n", + "### Update the state of a subgraph\n", + "\n", + "What if we want to modify the state of a subgraph? We can do this similarly to how we [update the state of normal graphs](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/time-travel/), just being careful to pass in the config of the subgraph to `update_state`." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'router_node': {'route': 'weather'}}\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"4\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"updates\"):\n", + " print(update)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[HumanMessage(content=\"what's the weather in sf\", id='35e331c6-eb47-483c-a63c-585877b12f5d')]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state = graph.get_state(config, subgraphs=True)\n", + "state.values['messages']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to update the state of the **inner** graph, we need to pass the config for the **inner** graph, which we can get by accessing calling `state.tasks[0].state.config` - since we interrupted inside the subgraph, the state of the task is just the state of the subgraph." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'configurable': {'thread_id': '4',\n", + " 'checkpoint_ns': 'weather_graph:9e512e8e-bac5-5412-babe-fe5c12a47cc2',\n", + " 'checkpoint_id': '1ef6a424-2bb2-6ee0-8002-6a6ca5dbc91f',\n", + " 'checkpoint_map': {'': '1ef6a40d-0fca-671c-8001-3064b486db01',\n", + " 'weather_graph:9e512e8e-bac5-5412-babe-fe5c12a47cc2': '1ef6a424-2bb2-6ee0-8002-6a6ca5dbc91f'}}}" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "graph.update_state(state.tasks[0].state.config, {\"city\": \"la\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now resume streaming the outer graph (which will resume the subgraph!) and check that we updated our search to use LA instead of SF." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(('weather_graph:9e512e8e-bac5-5412-babe-fe5c12a47cc2',), {'weather_node': {'messages': [{'role': 'assistant', 'content': \"It's sunny in la!\"}]}})\n", + "((), {'weather_graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='35e331c6-eb47-483c-a63c-585877b12f5d'), AIMessage(content=\"It's sunny in la!\", id='c3d6b224-9642-4b21-94d5-eef8dc3f2cc9')]}})\n" + ] + } + ], + "source": [ + "for update in graph.stream(None, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Fantastic! The AI responded with \"It's sunny in LA!\" as we expected.\n", + "\n", + "### Acting as a subgraph node\n", + "\n", + "Another way we could update the state is by acting as the `weather_node` ourselves instead of editing the state before `weather_node` is ran as we did above. We can do this by passing the subgraph config and also the `as_node` argument, which allows us to update the state as if we are the node we specify. Thus by setting an interrupt before the `weather_node` and then using the update state function as the `weather_node`, the graph itself never calls `weather_node` directly but instead we decide what the output of `weather_node` should be." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "((), {'router_node': {'route': 'weather'}})\n", + "(('weather_graph:bdb185a9-ff74-58dd-ae72-34e8665a33d7',), {'model_node': {'city': 'San Francisco'}})\n", + "interrupted!\n", + "((), {'weather_graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='5d721f30-278e-460f-a83c-fdb101731f3e'), AIMessage(content='rainy', id='43b30e0d-6ea0-4e9c-92de-3e411e6fa21d')]}})\n", + "[HumanMessage(content=\"what's the weather in sf\", id='5d721f30-278e-460f-a83c-fdb101731f3e'), AIMessage(content='rainy', id='43b30e0d-6ea0-4e9c-92de-3e411e6fa21d')]\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"14\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)\n", + "# Graph execution should stop before the weather node\n", + "print(\"interrupted!\")\n", + "state = graph.get_state(config, subgraphs=True)\n", + "# We update the state by passing in the message we want returned from the weather node, and make sure to use as_node\n", + "graph.update_state(state.tasks[0].state.config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_node\")\n", + "for update in graph.stream(None, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)\n", + "print(graph.get_state(config).values['messages'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perfect! The AI responded with the message we passed in ourselves.\n", + "\n", + "### Acting as the entire subgraph\n", + "\n", + "Lastly, we could also update the graph just acting as the **entire** subgraph. This is similar to the case above but instead of acting as just the `weather_node` we are acting as the entire subgraph. This is done by passing in the normal graph config as well as the `as_node` argument, where we specify the we are acting as the entire subgraph node." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "((), {'router_node': {'route': 'weather'}})\n", + "(('weather_graph:53ab3fb1-23e8-5de0-acc6-9fb904fd4dc4',), {'model_node': {'city': 'San Francisco'}})\n", + "interrupted!\n", + "[HumanMessage(content=\"what's the weather in sf\", id='64b1b683-778b-4623-b783-4a8f81322ec8'), AIMessage(content='rainy', id='c1d1a2f3-c117-41e9-8c1f-8fb0a02a3b70')]\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"8\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)\n", + "# Graph execution should stop before the weather node\n", + "print(\"interrupted!\")\n", + "# We update the state by passing in the message we want returned from the weather graph, making sure to use as_node\n", + "# Note that we don't need to pass in the subgraph config, since we aren't updating the state inside the subgraph\n", + "graph.update_state(config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_graph\")\n", + "for update in graph.stream(None, config=config, stream_mode=\"updates\"):\n", + " print(update)\n", + "print(graph.get_state(config).values['messages'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Again, the AI responded with \"rainy\" as we expected.\n", + "\n", + "## Double nested subgraphs\n", + "\n", + "This same functionality continues to work no matter the level of nesting. Here is an example of doing the same things with a double nested subgraph (although any level of nesting will work). We add another router on top of our already defined graphs." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "from typing import TypedDict, Literal\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "\n", + "memory = MemorySaver()\n", + "\n", + "\n", + "class RouterState(MessagesState):\n", + " route: Literal[\"weather\", \"other\"]\n", + "\n", + "\n", + "class Router(TypedDict):\n", + " route: Literal[\"weather\", \"other\"]\n", + "\n", + "router_model = raw_model.with_structured_output(Router)\n", + " \n", + "def router_node(state: RouterState):\n", + " system_message = \"Classify the incoming query as either about weather or not.\"\n", + " messages = [{\"role\": \"system\", \"content\": system_message}] + state['messages']\n", + " route = router_model.invoke(messages)\n", + " return {\"route\": route['route']}\n", + "\n", + "\n", + "def normal_llm_node(state: RouterState):\n", + " response = raw_model.invoke(state['messages'])\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "def route_after_prediction(state: RouterState) -> Literal[\"weather_graph\", \"normal_llm_node\"]:\n", + " if state['route'] == \"weather\":\n", + " return \"weather_graph\"\n", + " else:\n", + " return \"normal_llm_node\"\n", + "\n", + "\n", + "graph = StateGraph(RouterState)\n", + "graph.add_node(router_node)\n", + "graph.add_node(normal_llm_node)\n", + "graph.add_node(\"weather_graph\", subgraph)\n", + "graph.add_edge(START, \"router_node\")\n", + "graph.add_conditional_edges(\"router_node\", route_after_prediction)\n", + "graph.add_edge(\"normal_llm_node\", END)\n", + "graph.add_edge(\"weather_graph\", END)\n", + "graph = graph.compile()" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "memory = MemorySaver()\n", + "\n", + "class GrandfatherState(MessagesState):\n", + " to_continue: bool\n", + " \n", + "def router_node(state: GrandfatherState):\n", + " # Dummy logic that will always continue\n", + " return {\"to_continue\": True}\n", + "\n", + "def route_after_prediction(state: GrandfatherState) -> Literal[\"graph\", \"__end__\"]:\n", + " if state['to_continue']:\n", + " return \"graph\"\n", + " else:\n", + " return \"__end__\"\n", + "\n", + "grandparent_graph = StateGraph(GrandfatherState)\n", + "grandparent_graph.add_node(router_node)\n", + "grandparent_graph.add_node(\"graph\", graph)\n", + "grandparent_graph.add_edge(START, \"router_node\")\n", + "grandparent_graph.add_conditional_edges(\"router_node\", route_after_prediction)\n", + "grandparent_graph.add_edge(\"graph\", END)\n", + "grandparent_graph = grandparent_graph.compile(checkpointer=MemorySaver())" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAK0AdsDASIAAhEBAxEB/8QAHQABAAIDAQEBAQAAAAAAAAAAAAUGBAcIAwECCf/EAGAQAAAFAwAFAw8IBgcDCAkFAAABAgMFBAYRBxITFiEUMZQIFSI1NkFTVVZ0krKz0tMXUWFxc5W00SMyM1KBkTdCVHV3k9QkOKEJNENicnaCsRglJkRXg4SjwUZHY7XE/8QAGgEBAQEBAQEBAAAAAAAAAAAAAAIBAwQFBv/EADsRAQABAQMJBgQFBAIDAQAAAAABAgMRUQQSFCExQXKR0RM0UqGxwTNTYXEykqLC0gUiI0JD8BWB8eH/2gAMAwEAAhEDEQA/AP6pgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPGsrGI+keqqlxLNOyk1rcVzJIucxXERdbd6SqJN2rjoxZZaimVmy4tP7z60nrZPwaTIiLgrW5i60UXxnVTdH/AHY2ITlXNR8evUqq6mpl/uvPJQf/ABMY+9UJ44oOlI/MeNHY9ux7epTQMaynv6lI2Rn3+J44nnjkx77rQviig6Mj8hf+H6+RqfN6oTxxQdKR+Yb1Qnjig6Uj8x93WhfFFB0ZH5ButC+KKDoyPyD/AA/XybqfN6oTxxQdKR+Yb1Qnjig6Uj8x93WhfFFB0ZH5ButC+KKDoyPyD/D9fI1Pm9UJ44oOlI/MfU3TDKMiTL0BmfMRVKPzDdaF8UUHRkfkPh2rCqIyOHoDI+BkdKj8g/w/XyZqSTbqHkEttaVoVzKSeSMfsVtywothw34lCoCryR7aMw0lWOGFt41Fljh2ST+gyMiMZcLMVDtS7GSbaGpVhBOGpojJmpbM8E61kzMizwUgzM0GZEZmRoWqZopmM6ib/UuwTIAA4sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWLnxJ3Db8KoiUw6t2QfQeezQxqapf5rrKv/DjviziszCeSX5blYoj2b1NWR+SLJa69k8nJ97hTuCzD0Wn4KIjD3ls7IAFAV1Qei1CjSrSVaCVEeDI56lyX/3B9c6oHRcy4pC9JNoIWkzSpKp2lIyMucjLaDzsY9FpvjpbSNIWjGQFwSao6qTQ18zS0aFUFJUKaJ3ZuLNZLySVJyZINJGoiyKroZ09zl/U18vzNlzdE1BSciyy4xTsrJbdO4SU0pIQ+ta6rGckSdQzySVcxCCnbWuW49NsHdVlWv1kpHpCkfr7yoZ1ldDOxJNEaku0yVZcWZHqtrNB4IkqJZFwGMmxtJ8Lbel+z4OJVHvT0jKTELdbEkyhvNSonEsmjO1bcya0a+rgjwojAbEtzqg4iben6Svt64rYloaMVMuRc5Rtsv1FIWsRutarikKLWTq4NRGRmWSIUS+Oquri0UUV5WlY9xOUVdVxbdNWSVLToaeaqn0oXqJOoJZqIsoIzLV11tmRqQZqFWtXQnPx16zEtEaL0WXEyNk18CbBytNUVTlapTa0OPqSsyUS8Ggl66lZTleqRkL5dGiy55HqT7UtShoGjuuFoIN0416oQhLj9EunccY2pGaSM9ipJKzq5xxxxAbpt6WenYWkr6iLrYV59Osqgkdnt2eJlhezWtGeGexUfOJEa8p9N9tRFFTN3xKw2j6fdQbjkFNzlGVQ0jWUSFGaXDSZKJOSMjP5u8P3/wCkLor/APiXZ/39S/EAbAFYvzEfRUU2jCX4uqbcNXzsrWTbyfp7BRng+GshJ94jLOte87fvigcrrcnY24KJt02V1MXWN1LaXCIjNBqQZkSsKSeOfBl84wdI5be1H6FOTdkHWaJBEWcm44lJn9RJNSj+YkmPRk/xaY+vlv8AJsbVnAAHnYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACOn4VuejVUynDZcStDzL6Sypp1CiUhZfUZFku+WSPgYxIa4yqakoySJuhm0Jyql1uxeIudxkz/AF0f8U5wrBicGDLwlBPUvJpClaq2SPWSTicmlXeUk+dJ/SWDHamuLsyvZ6Nv3S9ettJ/ZWf8svyDrbSf2Vj/ACy/IQJWIhkjTSz05SN4wSCrjd1fqN0ln/xH53If8qZ7/Pa+EKzLPx+Ul0YrQlJISSUkSUkWCIuYh9FW3If8qZ7/AD2vhBuQ/wCVM9/ntfCDs7Px+UtujFaQHPvU5zNwaU4a86qaueVS7EXXIwtNyVbaCNhhSSRrZQeVdkeT4fUNs7kP+VM9/ntfCDs7Px+Ul0YrG7R076tZxhtxXNlaCMx+OtlH/ZGP8svyFf3If8qZ7/Pa+EPpWQ/37onlF823aL/ybDs7Px+Ul0Yp2pqKGEo3H33GKGlRxW4sybQXe4nzfMIaPYduSWp5ipYXT0NKSjjqd5CkOmpRGlT60njVM0maUpMsklSjVxVqp9aCyIuiq2qx1L8lWtGSm6iRqF1CmzIsZQSzMkHjPFJFzn85ifGTVRRExRrmd/8A3/v0ZqjYAADgwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHO/UV9zOk7/EGa9dsdEDnfqK+5nSd/iDNeu2OiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc79RX3M6Tv8QZr12x0QOd+or7mdJ3+IM167Y6IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGFMS1PBxztbUmrZN4LVQnWUtSjJKUpLvqNRkRF85kNiJqmIjaM0BSl3Bdjx67UXE06D4k2/WOKWRf9Y0t4z9BZL6TH56+Xh/YYPpT3wx69FrxjnDbl3AUjr5eH9hg+lPfDDr5eH9hg+lPfDDRa8Y5wXLuOSv+Ui0KPaTdCzVyxza3pa0FuVmySf69IsklUcObKSbbXnvJbV843918vD+wwfSnvhjzqZO662mdp6iNgXmHUG2405UPGlaTLBkZbPiRkGi14xzgufyN6jPQb8u2nGIjKun2tvxp9cpU1JyhTLaiw0f2ijSjHPg1GXMP7Wjmrqd9AVX1N9DcVPAU0TVLma86pb9TUO7RtkskzT5JvskoJSuJ8TNSj+gtvdfLw/sMH0p74YaLXjHOC5dwFI6+Xh/YYPpT3ww6+Xh/YYPpT3ww0WvGOcFy7gKR18vD+wwfSnvhh18vD+wwfSnvhhoteMc4Ll3AVyEuipfkExstSNUVa4hTjC6d03WX0pMtYiUaUmlZZI9Uy4keUmrVVq2Meeuiqzm6o2AAA5sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUdJZ4iYsu8ctRZL/5yRbhUdJfamK/vai9skenJvjUfdsbWWApmmq4ZC0tD17zkTUcklI2FrKulf1Er2bqGVqQrVURpPBkR4MjL5yGrpS4tIMND2BFnePKLivurZaVIORtOTEQ2ilcqH+TtkktdSiSSU7U18eP0D0TNzHQg+KUSEmpRklJFkzPmIcySele97QuersysuFMvWRl125TdeTomWnaqgkHTJxl1tKdRKy2biddBJMyUkywY/XVGXJOy1LptthMu7SRFBYzEiyy0w0Z661VRPEalIMzJxDSUHx4Fk06p8RmcOmUqJaSUkyUkyyRlzGPo5tu68bz0fW/o2s2Hlpi4Zy5kvvnKMUNAusp6Zhhtam2WlmywZ5WkiUvJkklnhR4xgS1/6YIW0qWlrTqYWQqrui4qNl5qhotvVUlQokuE+xTuONkaVZLWQaDUnGNU8hnDqEBE2vE18JDtUknN1Nw1iVLUqvq2WWlrI1GZFqtIQgiIjIuBZPHHJjXWledug9Kuj21IC4VW7RzlLKuV1Q3Rs1Dv6BNOps29okySotdZZMlJwo8pM9UyqZugbbAcyRelq+ZzrFYrU2zTXHV3XLQD90HQtqVyahQp43EsY2ZPLTqJ5jSXZHqj8yWmG+belJfRx14ppG6d5I6EobnqKJCCbp6ulXU7V1hGG1OtpadSRFqpUZoMyIs5nOgdIsTMfUylVGs11M7I0qEO1FGh5KnmUL1tRS0EeUkrVVgzLjqnjmMZg5BlrtuXQdeWmSUqJdd5XAiLt2lo6upo2WDNb79SyjXQ2aEHqms1f1CPBEZl+sNn6IpfShvxyO5aSdrLaeoXHHK64KKNpXaeqStGolsqN9eshSTcyS05SaE9keTCKr9Q2rKni7rNx35B4j+rkdQf/wCCF9FBlu66zP7xe/BVAvwnKv8AT7e8tncAADwsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUdJfamK/vai9skW4Ql3wj09DbGmUhNWw+1VMbQ8IUttZLJKjweCVg0meDxnODwO9hVFNrTVOy9sbVdvW1KS+7PnLbr3HmaGXonqB9ymUSXUtuoNCjQZkZErCjxkjLPeMQ136KIi87WiYWqqK6jVEOMvx0lQvE1V0jzSdVLqF4MtbVNRGRpNJkoyMhLLuwqfsamFnGHi4KaTFvPap/NrtJWg/rSoy+Yx+d86fxVPfclX8MfQ7Guf8AVubKko6nG2lW1MRtXITMhJS1bTyVVcNVVpOSOpYNJ07iXCQSU7PUSSUkgkkWSxxPPtFdT7BUlZdVVKS03ctRc8WiIlFy9She1YTtC7Em20E2Zk6osIwngRkRHkzlozTRa01PyMHHv11dMxuOWx9PHVDj9Nnm2iCRlGfpIhNb50/iqe+5Kv4YdhX4TNnBRnOpziaq14uJq7nuesrIeqKqiZx2ubKQjjJsm9RpxLREaDQWDStKiVk85EsrQvQVcDERslcE/MrjZunnkV0jVodfcfZUSkIUeoSSbykuwQlPfxgzyLHvnT+Kp77kq/hhvnT+Kp77kq/hh2FfhkzZwR9yzF80cqtqBteElY4kpNNTXTztI4ascSNtNI4REXz63H5iGFHWhX3VctvXZdNA1CT0AVZT0dHFyXLKZxqoQ0S1OKWw2rWy3wIiIi5zM84Kd3zp/FU99yVfww3zp/FU99yVfww7C08MszZU+u6nq3ayPrWUV8vRVr0+/clPKUlQhuqoat4sL2KtTGoaTNOotKiMjPOeGMdfU12tUWpXxFVWTNXIV0k3MPXG7Wf+tOWtkRNPpdJJEhSEkSUklJJIslq8TzeN86fxVPfclX8MN86fxVPfclX8MOwr8Lc2cFFoupsts6e7WpyUm7t3oo6ahkVzdUhajQwbhtKQbbaNRRG5nJcxpSZER5M7LYGjNVhv1Dzl23NcynGksIKfrkvpZQk8lqpQhBa3zrVlR98xK750/iqe+5Kv4Yb50/iqe+5Kv4YdhXH+smbOD0lu66zP7xe/BVAvw1Feejt7TXHIj3q2ftCOpl7dqTjXjopDblwSbRmk1ISRGeTMizrYIucxZq227zbv236uMuqnas2lpeTyMNV0JPVNS4RL1Xk1OtkjybZGRljsVHxM+HlyqYvpp3xHvM+7JXcBreNva+Yvf6suiy0lFQxuPwZwVTyurl2S2hkgmOdDuqlssGeDU5guBGY+OdUFaMRZluXJc71bZVPPVB0lJR3FSLp6pLxGstRxstbUPsDPJnjBlx4kPExskB5JqWV1C2EuoU+giUpslFrJI+YzLnIjwf8AIeoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADxqatulSo1Galk2pwmkFrLUlOM6qS4nzlzfOXzjUKblurT9o5jZSxq+T0ZbWUw8/PQ6VVT9EgzypppasETnYGSj72sA2FJ3zFUkjIw1HV00pc9JQLkCgKepb5W42nBJ7Az7ElKNKSUrBZUXEUVu2rn022jaEpcq5zRfIUch1xqoKIkW1qfQhZmy086lPEuDalJwXHWSZZwZX6hsO3Yy7JK56WFoWLikW0NVcohhPKHkIIkpSa+fBElPD6C+YhPAPFmkYp3X3WmW2nX1Et5aEESnFEkkkaj756qSLJ94iLvD2AAAAAAAAAAAAAAAAAAABiyMXRS9OTFfSMVrBKJeyqGkuJ1i5jwZGWS+cZQAKc5oitN3Sa1pCOJTve1SnRJkieczsTLGqaNbU/jq55+Ir9LoxuyzrLuiiti/JKVuCQqOUx1ddyyrUUOTSamiJKU5Rgl4LHDWL5htEAGuK+4tI9uUli0p2nQXbWVqm6e5JCPkE0TEco9mSn2m3SNbqCNTh6pYVhBfPgpCK0txUppLnrJ63y9FIxFKmscrqyhU1QvtGTZmpp4+CsG4RHzcUq5yLIu4/LjaXW1IWkloURpUlRZIyPnIyAQlo3zbt/Qrcvbc3QTkYtZtlVUFQl1GuREZoM0nwUWSyk+JZLgJ0UO7tBdjXpY1TZ9db1NS29UPpq10cXmhTtiMlE5+hNPZZIj48+Czkfa3RxKFddrSETeEnDwkNTFSvwLaG3aevQRYTtFLI1Eoux7IuPD6cgL2A11HVWkyFr77rJilg56HYbXU2zHxKnGa6oMiWZU763P0aVHhtJKLhlRmZ4IR1X1QFFaOjKHu+/LfmLOOvqjo3YxdKutfpVkpwiU5sUn2Bk3klY/rJ+cBtYBELu+CbuZFuLmY9FwLYKpRFKqUFVKaPW7Mms6xp7BXEix2JiXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABq2v0qV2kSzpWp0OvwtxTFFJ9annpdbzNJTOJxtVnhGs6SCUXBJkR8cKMywe0hr/AEIStbM2a+/X2Wiw3ykapsoptrZktJOGRP41U/tC7LOOOecwGXT6JoD5TE6QqimdcuzrcmO23KnVMtN5M1E22Z6paxmWTx/VI+B5zdQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAv2FbdVd1NdTsFHruWmaUw1L8mTypLZkZGjaY1jTgz4GeCyfzioUmg9i1rcvChs64Za3pK4XjqSkaioVX8ieM8mppDqsERmaspzjj9A2aADWtXT6UrbiLKoY2phLwqW3Es3HKyxKonXW8pI3mGmiNBHg1maT+YiLOTMSlBpCkajSjK2nUWfMUcZSUZVbFzuJScfU8G9ZslZylZG4Zaplx1FHzELsACjaKtNdm6bIurkLNlzlqakcJqoUqleYNtZ57EycQnJ8D5s/wDEheRSrQReiL/vg51NCm1FO0h29yYiJ407E+Um7jjnaYxnvC6gAD4Z4IefKWfCo9IhkzEbR6gPLlLPhUekQcpZ8Kj0iGZ0Yj1AeXKWfCo9Ig5Sz4VHpEGdGI9QHlylnwqPSIOUs+FR6RBnRiPUB5cpZ8Kj0iDlLPhUekQZ0Yj1AeXKWfCo9Ig5Sz4VHpEGdGI9QHlylnwqPSIOUs+FR6RBnRiPUB5cpZ8Kj0iDlLPhUekQZ0Yj1AeXKWfCo9Ig5Sz4VHpEGdGI9QHlylnwqPSIOUs+FR6RBnRiPUB5cpZ8Kj0iDlLPhUekQZ0Yj1AeXKWfCo9Ig5Sz4VHpEGdGI9QHlylnwqPSIOUs+FR6RBnRiPUUfQ/FXRD2m8xd1w0tzSp11S4itpMaiWDcM22+CU8UpwR8P4mLnylnwqPSIar6m5Fi0Oj2pbsOQqqyEOWrVLckMpcKoN09sRZSnsSVnHDm75jb4xG2QHlylnwqPSIOUs+FR6RDM6MR6gPLlLPhUekQcpZ8Kj0iDOjEeoDy5Sz4VHpEHKWfCo9IgzoxHqA8uUs+FR6RBylnwqPSIM6MR6gPLlLPhUekQcpZ8Kj0iDOjEeoDy5Sz4VHpEHKWfCo9IgzoxHqA8uUs+FR6RBylnwqPSIM6MR6gPLlLPhUekQcpZ8Kj0iDOjEeoDy5Sz4VHpEHKWfCo9IgzoxHqA8uUs+FR6RBylnwqPSIM6MR6gPLlLPhUekQcpZ8Kj0iDOjEeoDy5Sz4VHpEHKWfCo9IgzoxHqA8uUs+FR6RBylnwqPSIM6MR6gPLlLPhUekQ+lUNGeCdQZ/9ohudGI9AABo1lo8ioSj0vaUauPut6Zlqt2OORhXFGaIg005k2SS720TlZ/UNmjWWjyVhKzS9pRpI+1HoaWpHY4pGacSZIlzVTmbZpPv7NOUH9Y2aAjri7n5PzV31DFIhLPgXIagWuEjlrVTtmalUjZmZ6pcT4C73F3PyfmrvqGK/A9o47zZv1SHGiys7XKP76Yn+3fF+9V8xDE3Mt/xFGdDb/INzLf8AEUZ0Nv8AITID36Lk/wAuOUMvnFDbmW/4ijOht/kG5lv+Iozobf5CZANFyf5ccoL5xQ25lv8AiKM6G3+QbmW/4ijOht/kJkA0XJ/lxygvnFDbmW/4ijOht/kG5lv+Iozobf5CZANFyf5ccoL5xQ25lv8AiKM6G3+QbmW/4ijOht/kJkA0XJ/lxygvnFDbmW/4ijOht/kG5lv+Iozobf5CZANFyf5ccoL5xQ25lv8AiKM6G3+QbmW/4ijOht/kJkA0XJ/lxygvnFDbmW/4ijOht/kG5lv+Iozobf5CZANFyf5ccoL5xQ25lv8AiKM6G3+QbmW/4ijOht/kJkA0XJ/lxygvnFDbmW/4ijOht/kG5lv+Iozobf5CZANFyf5ccoL5xQ25lv8AiKM6G3+QbmW/4ijOht/kJkA0XJ/lxygvnFDbmW/4ijOht/kG5lv+Iozobf5CZHwy1iMvn+YNFyf5ccoL5xahjdKWiCZu2jtqPKPrpasqXaWnQxDOKaecbJRuajxNbNZI1D1jSoyT3zIVLQrpe0c3lakpWVVpxdtKj3K5+qZpoha6dphhzVU6t1LJISsywo2zPX4keDLiOfC0zU+gmv0dwl/wVz0lu2fVVlRCLYgDp6iuPDjSEuuPPIQZoQ8SlbHXSo1I7LnEPob6sxu29CStHtv0M9U6Q5CvfTG1VNQU7jSVVFTrZItprKcJK1mWUGWtguYhx7GwjVmU8oL5doQGkXRNc0LcErHpjl0sBTKrJNL8O4w/TMkhS9obLjSXDSaUKMjJJ5weMmPO39Iuiu8apVBCNRr0k5RrraVirh3KUqppKcmto3WkE6kslk0GeCGi5SHqKao0ozFNF3x1jrNGdfRplrwRUKdeqWycWtJ7Xsmi1XCNKTShJmTmoWOJ2uKrJPSzX6JGou1Z+LpLVonK2vlpWgVStq1o9TCWGFK/ba6nCMzRlOqnOTGxk9h8unlBfOKy6DdMWjzSlAWpTVtNBM3hLUBVLlDTxS26dbpI1nW2XFpNCzRxyglqUnB55jG5tzLf8RRnQ2/yHPlt2fOUuiTqa6ZUJIM18RL0S5Bk6RaXaJvkNUlZvJxltOspJGasFkyI+ch04KpyWwu12dPKC+cUNuZb/iKM6G3+QbmW/wCIozobf5CZAXouT/LjlBfOKG3Mt/xFGdDb/INzLf8AEUZ0Nv8AITIBouT/AC45QXzihtzLf8RRnQ2/yDcy3/EUZ0Nv8hMgGi5P8uOUF84obcy3/EUZ0Nv8g3Mt/wARRnQ2/wAhMgGi5P8ALjlBfOKG3Mt/xFGdDb/INzLf8RRnQ2/yEyAaLk/y45QXzihtzLf8RRnQ2/yDcy3/ABFGdDb/ACEyAaLk/wAuOUF84obcy3/EUZ0Nv8g3Mt/xFGdDb/ITIBouT/LjlBfOKG3Mt/xFGdDb/INzLf8AEUZ0Nv8AITIBouT/AC45QXzihtzLf8RRnQ2/yDcy3/EUZ0Nv8hMgGi5P8uOUF84obcy3/EUZ0Nv8g3Mt/wARRnQ2/wAhMgGi5P8ALjlBfOKG3Mt/xFGdDb/INzLf8RRnQ2/yEyAaLk/y45QXzihtzLf8RRnQ2/yEVc9rQtDEKfpoigp30PMGlxqmQlST2qOYyLJC3CFvHtA79sx7ZA8mV5NYRk9pMUR+Gd0YKpmb4X0AAUhS7S323/vbr/yLdPaUm7uwxttXZHyja44/tMYz3hdBrLR5FQlHpe0o1cfdb0zLVbsccjCuKM0RBppzJskl3tonKz+obNAR1xdz8n5q76hivwPaOO82b9UhYLi7n5PzV31DFfge0cd5s36pDLHvE8PurczgAB9RIAAAAAAAAAAAAAAAAAAAAAAAAAAACHueSfoKWlZpVk1VVtSilbdUklbPWyZqwfAzJKVGRH38c/MMQ7BjHMG9USrznfcVLVRGo/qS4RF9RERDrTRTdnVzdf8AS/3ht2KxgK38n0R+/Kfe9X8UPk+iP35T73q/iirrLxTyjq3UsgCt/J9Efvyn3vV/FD5Poj9+U+96v4oXWXinlHU1NCdUX1GNpXfovuDdWJeornYSquoElXVDjRukesttDK3DbSbhayexSXEyPvDn7/k1NCKpu7ZLSPJ0x8ihtaijdcuC6pacOLL59RtWPrdLHFI78+T6I/flPver+KMOK0TWxB0p00bSVcfTGtbps0slUtINajNSlYS4RZMzMzPvmeRymysc6Ks6eUdTUsM/BUN0QUlDSbHKY2RpnKSqY11I2jTiTQtOskyMspMyyRkZd4x7RsdTxEdS0FI3sqSlaQwy3rGrVQkiSksnkzwRFziE+T6I/flPver+KHyfRH78p971fxR0zbHxTyjqalkAVv5Poj9+U+96v4ofJ9Efvyn3vV/FG3WXinlHU1LIArfyfRH78p971fxR8pELtqeoo1FTUVVBXIcNtNW8p5xlxBEeCWozUpJkZ8Dzgy5+OAzKKvwTN/1i73kujcsoAA4JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABC3j2gd+2Y9sgTQhbx7QO/bMe2QPHlndrXhn0VTthfQABwS1lo8lYSs0vaUaSPtR6GlqR2OKRmnEmSJc1U5m2aT7+zTlB/WNmil2lvtv8A3t1/5FuntKTd3YY22rsj5Rtccf2mMZ7wugCOuLufk/NXfUMV+B7Rx3mzfqkLBcXc/J+au+oYr8D2jjvNm/VIZY94nh91bmcAAPqJAAAAAAAAAAAAAAAAAAAAAAAAAABW7y/51bX97N+ydExLzMfb8a/IyldTRsfTp1nquseS002WcZUtRkRFky5zEPeX/Ora/vZv2TooXVeoJzqbL8SfMqhSR4+1QOtpN1nTP39VTshfj0kWkmNkZE7phSj457k1bVnIM7Kld4fo3V62EK4l2KjI+IymL0t+qjY6QZnYx6gkXk09FVN1jamqp1WdVtpZHhajweCSZmeD+YaY02xEBZF4aJpGUj6ai0dQ9VWJrUlTa1HSVCqYkUbrqCIyJKdVxBLMsJNSeJcBq916lSzKXnE0y6PRq1pLi5alfRTqQwlhFOlqrq0JxwZN4865Fg+yMeaarkumdJWmK2dF1r3DMykjTuqg6dD9THsVDXKf0mSZTqKUWDcNKiTnGtqnjOBJ0+ku0qm1t5W7ohlW9nVVKlIMnSpVnBpN3W1MkfDGeccuX5KUelU+qWdtgznWXbWiE0zlO0pSag0JrFGbR4/SFwMiNOSMyMiyM/S3d1t3tKaL7ojblVQaOKJyup62ZjaBmpZj69TLRsqfbfZcQnsTcRrmjsDWfFOTGZw6bO+7aKHo5Y7iiiiqxzY01cda1sH14M9VC9bVUeEqPBHnsT+YZ0FcEXdMWzJwslRy8a8aiarKB9L7K9VRpVqrSZkeFEZHg+BkZDkyVsW0XbYsp+HnKm8YO4NJNBU1K62jZp6d11LLqF7Npplps0K2acmScLPJ5PJjr5inapWiaZaQy0nOENpJJFk8nwL6RUTMiIuC+LctIzKcuCLhjJrbn1wrW2MN6xJ1+zUXY6xkWebJkXfH6kb0t+IgG52vnYyihHEpWiSqKxtumUSv1TJwzJJkfe48Rq+4omileq2tgq2jYrCYs6QdaJ9tK9mvllKnWTkuB6qlFn5lGXfGkbMcirZb0ZS90tto0fws1dNHtKhrXo6CpOtWVKpwsGSSJCXkIUZYSZ85ZIZNUxI7DRdcI5b/AF+TMUCoPZm91zKqQdNqfvbXOrq/TnAp9k6aoW+rpvGPj6mgfhreZo3+vlNIIep6hL7bi1HlJaqSRszIz1jz9GBzO+ijKIO43414tDDmkdUmun5IvYKo+SkhNSbWP+anWFr41cH+tgYF7vRl4yWlqQshRvWoUpa9XKuQVElw3aJvanULQyps0vapklRkaVJUSDyRlkZnjtO2rygLzpXKm35yNnaZpWot6Mq26hCFfMZoMyI/oGHO92FsfXU+yGp9AMJZspfExddsX5WXjVojmo6qNNBS0tKSFOGtvPJ6ZlK3E6iy4mo0ksyMiyQ2xO92FsfXU+yHqsJvqn7T6S2FkAAHJgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIW8e0Dv2zHtkCaELePaB37Zj2yB48s7ta8M+iqdsL6AAOCWstHkVCUel7SjVx91vTMtVuxxyMK4ozREGmnMmySXe2icrP6hs0ay0eSsJWaXtKNJH2o9DS1I7HFIzTiTJEuaqczbNJ9/Zpyg/rGzQEdcXc/J+au+oYr8D2jjvNm/VIWC4u5+T81d9QxX4HtHHebN+qQyx7xPD7q3M4AAfUSAAAAAAAAAAAAAAAAAAAAAAAAAACuXiX+0W4fMSZVvJmf8A/G4X/mZCxio33dlvxtrz9VW1DlYiGbS/VsRP6esp1frIMkIyolcMln5jzwyKzF3BpHuSKteTgaeLRGSbe2rU3NQv0VfQoPVwRsoWonHOKsl2BdiXHCuHe+mumImq6Y+/tEq2w2oArfIbv8dQn3Q9/qg5Dd/jqE+6Hv8AVB2dPjjz6F31WQBW+Q3f46hPuh7/AFQchu/x1CfdD3+qDs6fHHn0Lvqsgp1xaLIu55Z2RqZS5aZ50kkbUdcdfRslgiIsNNPJQXNxwXE8mfEZvIbv8dQn3Q9/qg5Dd/jqE+6Hv9UM7Kmf948+jLoxZtsWzTWnGchpKmRq2tc3NpKSD9a7k8cNo8tSscObOCEuK3yG7/HUJ90Pf6oOQ3f46hPuh7/VB2dPjjz6F0YrIArfIbv8dQn3Q9/qg5Dd/jqE+6Hv9UN7Onxx59G3fVZBXJws3hbOOcuUmZd/GzIs/wDEv5iFmy0m0sjGIinbWkKB14kVz1TT1FO7Tt5Ls20E4snDIs8DUjvcfmj7Mv6ilLzuyhl0ydPK24jD9fIxyqGP5OriblOpSlJNBmg8qUrJ6n7pENpzbOZnOidUxv3xdvg2NmgPKkq2K+maqaZ5uop3Uktt1pRKQtJ8xkZcDL6R6jzpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABC3j2gd+2Y9sgTQhbx7QO/bMe2QPHlndrXhn0VTthfQABwSpdpb7b/AN7df+Rbp7Sk3d2GNtq7I+UbXHH9pjGe8LoNZaPIqEo9L2lGrj7remZardjjkYVxRmiINNOZNkku9tE5Wf1DZoCOuLufk/NXfUMV+B7Rx3mzfqkLBcXc/J+au+oYr8D2jjvNm/VIZY94nh91bmcAAPqJAAAAAAAAAAAAAAAAAABEVd2xNLX1kcVexUS1JSqrXYyncSurJov62yI9bBmZEXDiZkAlwGpnr0vnSno0o5bR9Gps+Vqq021IvehdbcbpS1v0qWkHxUrsDTngZGf1i0L0XRlRpNp76frJRyXYouRNUh1y+RNJPOupLPNrKyWTPh2KTxksib79ggntOFHdVjTs3oxo0aRpCLrCjzj6SpKmSt89TP6VwiTqpJwlGoskZEeD4GJJ62ruuC6bSn3Llet2No6XXkrWYp2nkVNQtBkZLfznVRrGWCyRmklFjAutFQU0bTkxR0zVKwRmZNMIJCSMzyZ4LhxMe4XYis2ho1tiwa6brLfhaaLq5qqVWSLzJHr1LpqUrWUZmffWsyIuBax4IhZgAbsAAAaAAAAAAAAAAAAAAMSXiaKeiqyNkaVqtj6xlbFRTPpJSHW1EZKSoj5yMjMhlgA1pJaHaiFtq2oLR1Pr0exsNXHUnS0lImqaqWlLUpxlROHzKNazzk8GZHjgQl2LwuRnSLLxMjah0VoU1EVXTXTy5tTbqiJG0aWz+sgyNSsGfAyQf8boAy7AVrR7pItvSrbTM/asq1LxTqjbJ9tKkmlZYM0qSoiUlRZLgZEfEhZRStIeiSD0j2ucHVLrYZgqtNe3UQdSdG+3UFnDhKRznlRnxI+OD5yIfp2ivmn0kxy6Osh3LAKh2VVS1KXVSKagtfVcQ5+qojygj1jM+xMy4nkNYuYCg2jpji7hg5qVl46SsmliK06GpcudkqJBqNSSQtKlHg0K10YPhxURC9svN1DSHWlpcaWklJWg8koj4kZH3yC+8fsAAaAAAAAAAAAAAAAAAAAAAAAhbx7QO/bMe2QJoQt49oHftmPbIHjyzu1rwz6Kp2wvoAA4Jay0eSsJWaXtKNJH2o9DS1I7HFIzTiTJEuaqczbNJ9/Zpyg/rGzRS7S323/vbr/yLdPaUm7uwxttXZHyja44/tMYz3hdAEdcXc/J+au+oYr8D2jjvNm/VIWC4u5+T81d9QxX4HtHHebN+qQyx7xPD7q3M4AAfUSAAAAAAAAAAAAAA8ax5ynpH3WmjfdQhSkNEeDWZFkk5+nmHsMeRQ45H1SGnip3VNKJDx8yDweFfw5wGsIyLvTS1Ztly0xJSejOSZqTrJSEjdRS3yQ4ezZW4tJmlJklJqIiwolmRkfDF5jrDt2IuqTuWjhaKnuCTSlFZJoZTyh5KUpSSTXz6uEI4c3YkfOI3RFHS0To4hKScuJq7ZVppRPzLCtZFUeuoyUR/QRkX8BcBMQAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEbcdsxN4Q1TETkbSy8XUp1XqSsaS62siPJZSZYyRkRkfeMiMhV6zRpU08/aNVb9y19tQsCzyRdv0jba6KrpySRJQpKiyk06qSJRGZkRGRYMzMXoBlwqOj+5bhuGouduft84JEfMP0cc5tDUVdSJ1TbqCyXDWyZY+gW4UfRlFzMbWXiqXuhm5W6mcqH6JtpRGcdTmSdWlV8xowfD6ReAjYAAA0AAAAAAAAAAAAAAAAAELePaB37Zj2yBNCFvHtA79sx7ZA8eWd2teGfRVO2F9AAHBLWWjyKhKPS9pRq4+63pmWq3Y45GFcUZoiDTTmTZJLvbROVn9Q2aNZaPJWErNL2lGkj7UehpakdjikZpxJkiXNVOZtmk+/s05Qf1jZoCOuLufk/NXfUMV+B7Rx3mzfqkLBcXc/J+au+oYr8D2jjvNm/VIZY94nh91bmcAAPqJAAAAAAAAAAAAAAYcxsOtFdyrW5LsF7XV59TVPWx9OMjMGPIrcbj6pbTJVDqWlGhk+ZZ4PCf48wCh9T1up8jVsbj8q3V2C+QctzttTarzrZ451tYbEFP0RSMtLaOISrnLdatKVdaUb8MwnVRSnrqIkkX0kRH/ABFwGRsAAAaAAAAAAAAAAAAAAACvM3a9VbVVLASdUyh5xknm1U5JUaFmhRkSnSPGUnzkQ4WltRZTEVbZwiZ9L2xF6wgILeOu8mZb06X44bx13kzLenS/HHPSrPCfy1dG3J0BBbx13kzLenS/HDeOu8mZb06X44aVZ4T+WroXJ0BBbx13kzLenS/HDeOu8mZb06X44aVZ4T+WroXJ0BBbx13kzLenS/HDeOu8mZb06X44aVZ4T+WroXJ0BBbx13kzLenS/HDeOu8mZb06X44aVZ4T+WroXJ0BBbx13kzLenS/HDeOu8mZb06X44aVZ4T+WroXKZoS3N646Rt0eWbfemr69crzjrhhG12ef6mNXGBtAax0eXdclVV3WUpo6qoNDUy+3SLpeTpOtYIk6tQ5l0srVxyZZLhzi47x13kzLenS/HDSbOnVMT+Wroy5OgILeOu8mZb06X44bx13kzLenS/HDSrPCfy1dG3J0BBbx13kzLenS/HDeOu8mZb06X44aVZ4T+WroXJ0BBbx13kzLenS/HDeOu8mZb06X44aVZ4T+WroXJ0BBbx13kzLenS/HDeOu8mZb06X44aVZ4T+WroXJ0BBbx13kzLenS/HDeOu8mZb06X44aVZ4T+WroXJ0BBbx13kzLenS/HGVDThS7lW0qiqaB+mUlLjVTszPinWIyNClFzH842nKbOqqKYvvnGJj1hl0pMQt49oHftmPbIE0IW8e0Dv2zHtkCcs7ta8M+jadsL6AAOCVLtLfbf+9uv/ACLdPaUm7uwxttXZHyja44/tMYz3hdBrLR5FQlHpe0o1cfdb0zLVbsccjCuKM0RBppzJskl3tonKz+obNAR1xdz8n5q76hivwPaOO82b9UhYLi7n5PzV31DFfge0cd5s36pDLHvE8PurczgAB9RIAAAAAAAAAAAAADHkUOOR9Uhp4qd1TSiQ8fMg8HhX8OcZAw5jYdaK7lWtyXYL2urz6mqetj6cZAVvRFHS0To4hKScuJq7ZVppRPzLCtZFUeuoyUR/QRkX8BcBrvqet1PkatjcflW6uwXyDludtqbVedbPHOtrDYgyNgAADQAAAAAAAAAAAAABBWZ2ld8+rfxTonRBWZ2ld8+rfxTo8dXeaOGr1pVuToAA9iQAAAFP0g6QisCttQn6DlNBNTDUO9Vk9qckW6hexWadU9YlOJQ3zpwbhHx5hcBTNMdgr0naNJ63KeoTRV9UyS6GrUZkVPVNqJxhzJEZkSXEIM8ccEYydmoUhzqn4isg7qqoaOVJyUPcLNuUsep/ZdcH3XUMtrQvVVhBrN3jhXBhZiu3j1ZsHbE3PtU9JE1sTA1TlJXOPXHS00g4ts8Pcmol9m6STyRZUg1mkySR8DOeoupjjIi+NG8xQ1RooLVj+S1NKsz/ANvebQtNM+suY1oVUVS9Y+OXPrH5gNFl96PbhnKS2nLVkbUlph2XJc0h8q2hN9ZLfaQlCTS6nW1jQZqSZa3HOBz/ALh80s9Uueiesaq6yDj6i2Fss1JVy7gp2K19pZEal09Eotd3VI+JayTPB4Ix70l/3zUdU1M2zTx1DV2nTw9BVEa5HZqZQ448S6hKSYM1rM0auzNZERNkoldkZFWdJPU73bc9dpNpop62VUN6No/9bSqHl19CSGENlToSlOqbeUZJWsWrtFHqrxxuirAvWK0rR14RSoF1quhaOInKKsfeSbOxdW4blMtLZ6/B1xJEskcyTzxMi3XeNe6N9ONzWrCV9ZPQFZLWqV410S9cdRKpW9TE5IrZZ1WFEajZQpTbf6yTT/VSZFxl766sSHtK4rho6SkiK+ht55dNILq7kpaKtccQRG6mlpHOye1c6vE0ayiNKc4EhVaCJ9/QvN2imsjSkq66FTbbpuubEmDlk1mqZ6mdfZpMsERlrcM44jTXVP29pY6nmJuO8NH1wULVoVsy7LV9OuPZfrKd6qNBLybrS0m0TickaTSZG8ZGSiIjKZmqmBs/Q1pTu6d0k35B0LTd5w7cxTSLUvUSJUzUfH1bSHG6dCNmtS3G06x6nAskZGpPDPRo/jtoS056XD0i19HaFyLoZa7q/lcm81E09Sbiy11uPE1sVHhKTcVqNp4kWCIzwQ/qQzp1tZ95DaWLl1lqJJa1pyqSyfzmdNgvrMKK4mBSG+qVlK6vgayissjs2buIrdop6olCStbhPLaU4dOltRkg1NOkjKsmZJ1iQR5E/YmmO5NIFbcK6CyENxENJSMUurel0pcqXqZa0I2LZtYMlmlJGalJJJqP9bVMxztb0zR2tpoZj0FG3Yy1drztHbEdKV6HIx119aFVaKBymJtOzSta1KN00ZNakGWSx0DF6F5dvRHpCtB+VZoa25JCZqaatolLUTDdW84tvWySTyRLIlEX0kRnzjYmZEfY/VMlclxz1vScLHUUzHRL0w23Ez7Mm0420okuNOLbSRtOkakdiaT4GZkZ4HpaXVDTEmxYUlcVlJgLfvQmGo6Rp5ZNYbdQ8ybrTbzeyQaSWRGSVEauONYk5wVbp9EN4QUkzcclSWlEx0VachBLjIA3sJQpCFpdSpTadYzU0RahknVIzPWWZ4GDoY0f3tpB0f6FnLgqIGisy36KOmKVqOU85W1jrdKRU6XddJIbJOvrK1TVrGnhguZfUPugzTlc0XYtj7zQFZXQkzLPQyLnqZVL9Quocqnks67JkatnrETZKNeSwXY4wOnhoiK0ET9Doi0f2q5WRpyFv3NTTVU6l1zZLZbr11CkoPUya9RRERGRFnPHHEb3FU33awAAFgAAACChu6S4ftGPZEJ0QUN3SXD9ox7Ih5Lf4llxftqVGyU6IW8e0Dv2zHtkCaELePaB37Zj2yBmWd2teGfQp2wvoAA4Jay0eSsJWaXtKNJH2o9DS1I7HFIzTiTJEuaqczbNJ9/Zpyg/rGzRS7S323/vbr/yLdPaUm7uwxttXZHyja44/tMYz3hdAEdcXc/J+au+oYr8D2jjvNm/VIWC4u5+T81d9QxX4HtHHebN+qQyx7xPD7q3M4AAfUSAAAAAAAAAAAAAAx5FbjcfVLaZKodS0o0MnzLPB4T/AB5hkDHkUOOR9Uhp4qd1TSiQ8fMg8HhX8OcBV9EUjLS2jiEq5y3WrSlXWlG/DMJ1UUp66iJJF9JER/xFwFP0RR0tE6OISknLiau2VaaUT8ywrWRVHrqMlEf0EZF/AXAZGwAABoAAAAAAAAAAAAAAgrM7Su+fVv4p0TogrM7Su+fVv4p0eOrvNHDV60q3J0AAexIAAAAAr9/PLZtOt1FKTtDbZUaTweqtxKFY/goxdFOfVFOLYi+bnx6/IhpxaEKraskqNJuUUdU1DeS5y1221JP+Y/HygRX9nmfuOt+CLCyy3TtIaaQlppCSShCCwlJFwIiLvEP2LvsvDPOOhqVv5QIr+zzP3HW/BD5QIr+zzP3HW/BFkAbfZeGecdG6lb+UCK/s8z9x1vwRGXROW1eNuScFK0MxUxslTOUlQ0cJWlrNrSaVYPY8DwfA+8Yu4DL7LwzzjoanDnUZdT+nQffV3XFc1FKOVLTq46DcKGql7Sm1sqqcJbPUNZEhJEeFEWuRlgyz1/8AKBFf2eZ+4634IsgDKexpi6KZ5x0NSt/KBFf2eZ+4634IfKBFf2eZ+4634IsgCr7Lwzzjoalb+UCK/s8z9x1vwQ+UCK/s8z9x1vwRZAC+y8M846GpgRU7QzW1KkeNTjJkTjTramnEZzjWQsiURHg8GZccH8wzxXLh1aa57XfQnVefqnqRay4GbZ07rhpP5y1mkHj5yFjEV0xF007J/wDnsyQAAc2AAAAIKG7pLh+0Y9kQnRBQ3dJcP2jHsiHkt/iWXF+2pUbJTohbx7QO/bMe2QJoQt49oHftmPbIGZZ3a14Z9CnbC+gADglrLR5FQlHpe0o1cfdb0zLVbsccjCuKM0RBppzJskl3tonKz+obNGstHkrCVml7SjSR9qPQ0tSOxxSM04kyRLmqnM2zSff2acoP6xs0BHXF3PyfmrvqGK/A9o47zZv1SFguLufk/NXfUMV+B7Rx3mzfqkMse8Tw+6tzOAAH1EgAAAAAAAAAAAAAMOY2HWiu5Vrcl2C9rq8+pqnrY+nGRmDHkVuNx9Utpkqh1LSjQyfMs8HhP8eYBQ+p63U+Rq2Nx+Vbq7BfIOW522ptV51s8c62sNiCn6IpGWltHEJVzlutWlKutKN+GYTqopT11ESSL6SIj/iLgMjYAAA0AAAAAAAAAAAAAAQVmdpXfPq38U6J0QVmdpXfPq38U6PHV3mjhq9aVbk6AAPYkAAABW9IncnU/bU/t2xZBW9IncnU/bU/t2x3sPjUfePVVO2Gdd09utak1NbDlXW2ierNhr6m02bal6utg8Z1cZwePmGnKLqiLtkJG0KNrRs2ld30C6+FU5PoIjShtDiyqcMnsuwcIy1NoZ5IsFxxt6+YN+5rJuCHpVtt1MhHVFI0t4zJCVuNqSk1GRGeMmWcEf1DX8TojmKCY0N1blTQm3ZsPUR8gSXF5dccpWWkm12HFOs0oz1tU8GXDvF5Zv3JYLPVHqr7ZhFUNrP1N5ysxVwLdtqrEIJqqpTXyk11GqZE0hLZq1ySZmRp7HJ8Py91SfW2HlaaTtapp75oZeng0WzT1aHuVVVQ3tKc26jCU7JTess1qSWqTa8p4cYprQJdEMpmcia+IK6Iy8Ji4I9uqU6dI/SVynCUw6okayF6i0nrJJRJUn+sXEYsh1PV3zJyN31MxD0+kh6fo52mbZQ6uMZTSsKp26VSjInFJU247rLJJHrKIySWOM/3DCjNOExZV86WZ3SBQvQdFCxEMtqFZlSrGCcdXUoI2VqJCEm4o20mZpT+rlR4IjGBc/VUJuqxb+hGjjYm4E2pJSkdW2zcrMoSDaZPJKcZJKmXUmpKi4YPCtVR6om5bqfrx0iu6Qqu66+Dh6+4KKJRHuQi3qhNHUUTzrqFrJ1CNdJqUjmxkjUWCwRnblWLfd82Rdtu3ii1IpEtDvxjD9vJfdWTjrakKdXtEownsiMkFn/tGH9w2NZ77lVaUI884p15yhYWtxajUpSjbSZmZnzmZipaR9KEnZ942rbENbabgk7hZrXGTcrypGmDpyZM9oo0KPVMnT4pIzI0kWqesZpwoHSI3o8t2IgLxbrXLhoqRtqpXb0DJVtGrBYSpDqKdRZNJJM05yRmZfSPwVB8p2kqx72hlusxFvNSlHVtSlDVUNStb7dPqG2080k1JLUPKjwXHhkyMiq/VqEQz1SDlZARbdJar1Re1fN1dvotzlqEoRVU2sqoUqp1cbJKE6+uSDMyUnCeIPdUn1th5Wmk7Wqae+aGXp4NFs09Wh7lVVUN7SnNuowlOyU3rLNaklqk2vKeHGK+QS6IqvXcURXxBXNQ3hJ3BHNVanTpXqSsb2S2HlJRrIXqnnWSSiI0l+tnhiyHU9XfMnI3fUzEPT6SHp+jnaZtlDq4xlNKwqnbpVKMicUlTbjusskkesojJJY4z/cMKM04TFlXzpZndIFC9B0ULEQy2oVmVKsYJx1dSgjZWokISbijbSZmlP6uVHgiMW3RV1S1FpCvdFq1dND08lUUbldSuQVw08wypLakktDimiSbTha6TIjI0qIlYUeqK9LdT9eOkV3SFV3XXwcPX3BRRKI9yEW9UJo6iieddQtZOoRrpNSkc2MkaiwWCM9oaOqK+2qqocvGktSlbSylDJW9t1rW5nslqU6lOqkyxhBErH7xjYzhL3T3QWd/ebv4KpFkFbunugs7+83fwVSLIPVafho+3vKp2QAADikAAABBQ3dJcP2jHsiE6IKG7pLh+0Y9kQ8lv8Sy4v21KjZKdELePaB37Zj2yBNCFvHtA79sx7ZAzLO7WvDPoU7YX0AAcEqXaW+2/wDe3X/kW6e0pN3dhjbauyPlG1xx/aYxnvC6DWWjyKhKPS9pRq4+63pmWq3Y45GFcUZoiDTTmTZJLvbROVn9Q2aAjri7n5PzV31DFfge0cd5s36pCwXF3PyfmrvqGK/A9o47zZv1SGWPeJ4fdW5nAAD6iQAAAAAAAAAAAAAGPIoccj6pDTxU7qmlEh4+ZB4PCv4c4yBhzGw60V3KtbkuwXtdXn1NU9bH04yAreiKOlonRxCUk5cTV2yrTSifmWFayKo9dRkoj+gjIv4C4DXfU9bqfI1bG4/Kt1dgvkHLc7bU2q862eOdbWGxBkbAAAGgAAAAAAAAAAAAACCsztK759W/inROiCsztK759W/inR46u80cNXrSrcnQAB7EgAAAIK+KN6utesbYaU86k23ibQWVK1HErMiLvmZJPBCdAVRVmVRVG5sapveFFWsSVI1VUryH6d1OshxB5JRD3EFW2JbsjVOVNTCULtQ6o1uOmwnWWr51Hjif0mPD5N7W8Q0H+SQ6XWWM8o6mpZAFb+Te1vENB/kkHyb2t4hoP8kgusvFPKOrdSyAK38m9reIaD/JIPk3tbxDQf5JBdZeKeUdTUsgDm/qbLejbkmNLzcpRMyCI6966io01CdcqdhKW9VtGeZJZPBF843Z8m9reIaD/JIZEWUxfnTyjqalkAVv5N7W8Q0H+SQfJva3iGg/ySG3WXinlHU1LIArfyb2t4hoP8kg+Te1vENB/kkF1l4p5R1NT7NmmQuu3qZk9o9QvOVz5JP9k2bDrSTV82spzgR4M9VRlnVVixjDjIehhac2I+jYomTUazQw2SCUo+czxzmfzjME11RN0Rsj/wCskAAHNgAAACChu6S4ftGPZEJ0QUN3SXD9ox7Ih5Lf4llxftqVGyU6IW8e0Dv2zHtkCaELePaB37Zj2yBmWd2teGfQp2wvoAA4Jay0eSsJWaXtKNJH2o9DS1I7HFIzTiTJEuaqczbNJ9/Zpyg/rGzRS7S323/vbr/yLdPaUm7uwxttXZHyja44/tMYz3hdAEdcXc/J+au+oYr8D2jjvNm/VIWC4u5+T81d9QxX4HtHHebN+qQyx7xPD7q3M4AAfUSAAAAAAAAAAAAAAx5FbjcfVLaZKodS0o0MnzLPB4T/AB5hkDHkUOOR9Uhp4qd1TSiQ8fMg8HhX8OcBV9EUjLS2jiEq5y3WrSlXWlG/DMJ1UUp66iJJF9JER/xFwFP0RR0tE6OISknLiau2VaaUT8ywrWRVHrqMlEf0EZF/AXAZGwAABoAAAAAAAAAAAAAAgrM7Su+fVv4p0TogrM7Su+fVv4p0eOrvNHDV60q3J0AAexIAAAAAAAAAAAAAAAANAdSj2803/wCIEj6rQ3+NAdSj2803/wCIEj6rQ3+Ip2AAALAAAAAAAAAAAAAAEFDd0lw/aMeyITogobukuH7Rj2RDyW/xLLi/bUqNkp0Qt49oHftmPbIE0IW8e0Dv2zHtkDMs7ta8M+hTthfQABwS1lo8ioSj0vaUauPut6Zlqt2OORhXFGaIg005k2SS720TlZ/UNmjWWjyVhKzS9pRpI+1HoaWpHY4pGacSZIlzVTmbZpPv7NOUH9Y2aAjri7n5PzV31DFfge0cd5s36pCwXF3PyfmrvqGK/A9o47zZv1SGWPeJ4fdW5nAAD6iQAAAAAAAAAAAAAGHMbDrRXcq1uS7Be11efU1T1sfTjIzBjyK3G4+qW0yVQ6lpRoZPmWeDwn+PMAofU9bqfI1bG4/Kt1dgvkHLc7bU2q862eOdbWGxBT9EUjLS2jiEq5y3WrSlXWlG/DMJ1UUp66iJJF9JER/xFwGRsAAAaAAAAAAAAAAAAAAIKzO0rvn1b+KdE6IKzO0rvn1b+KdHjq7zRw1etKtydAAHsSAAAAAAAAAAAAAAAKFpU07WNoT61nek4UKUnteSGdI++Tmz1Nf9khWMbRHPjOeGcHjJm7aNe9Sj2803/wCIEj6rQ3+OI+p86rPRRZEppVem7sTRNTN41spQHyCqc21MtLZIcwlo8Z1T4KwfDmHbgiiYmNQAADoAAAAAAAAAAAAAAIKG7pLh+0Y9kQnRBQ3dJcP2jHsiHkt/iWXF+2pUbJTohbx7QO/bMe2QJoQt49oHftmPbIGZZ3a14Z9CnbC+gADglS7S323/AL26/wDIt09pSbu7DG21dkfKNrjj+0xjPeF0GstHkVCUel7SjVx91vTMtVuxxyMK4ozREGmnMmySXe2icrP6hs0BHXF3PyfmrvqGK/A9o47zZv1SFguLufk/NXfUMV+B7Rx3mzfqkMse8Tw+6tzOAAH1EgAAAAAAAAAAAAAMeRQ45H1SGnip3VNKJDx8yDweFfw5xkDDmNh1oruVa3Jdgva6vPqap62PpxkBW9EUdLROjiEpJy4mrtlWmlE/MsK1kVR66jJRH9BGRfwFwGu+p63U+Rq2Nx+Vbq7BfIOW522ptV51s8c62sNiDI2AAANAAAAAAAAAAAAAAEFZnaV3z6t/FOidEFZnaV3z6t/FOjx1d5o4avWlW5OgAD2JAAQ83JVKKyjjKDURW1aVubZ1JqQy0g0kteO+rK0kRZLieeYsHVNM1TdDdqYAVw7cmlHneutSffJNJTY/4tmPm7c15W1/Rab4Y6dnT448+jbvqsgCt7tzXlbX9Fpvhhu3NeVtf0Wm+GHZ0+OPPoXfVZAFb3bmvK2v6LTfDDdua8ra/otN8MOzp8cefQu+qyDSvVeaFS036FZeLpmScnY8uuMWZJyo3myPLZf9tJqRjmyaT7w2Nu3NeVtf0Wm+GG7c15W1/Rab4YybKiYumuPPoXfV/KnqJNCB6Y9NdEqvpttb0AaZCRJwuxWaT/RMn8+usuJHzpSsf16GsrI0FUGjipnKi25Wpi3pusVXyC26dhRvPKyZn2SD1S4nhCcJLJ4IsmLTu3NeVtf0Wm+GJosaaIuz48+hd9VkAVvdua8ra/otN8MN25rytr+i03wxfZ0+OPPoXfVZAFb3bmvK2v6LTfDDdua8ra/otN8MOzp8cefQu+qyAK3u3NeVtf0Wm+GG7c15W1/Rab4YdnT448+hd9VkAVasbmbYo3pJyXXMUtMg3aimqKZtK1NkRmo21NknCiLiRGRkeMcM6xWdtxLzaXEKJSFESkmXfIxNVGbF8TfH/cWXP0AAObAQUN3SXD9ox7IhOiChu6S4ftGPZEPJb/EsuL9tSo2SnRC3j2gd+2Y9sgTQhbx7QO/bMe2QMyzu1rwz6FO2F9AAHBLWWjyVhKzS9pRpI+1HoaWpHY4pGacSZIlzVTmbZpPv7NOUH9Y2aKXaW+2/97df+Rbp7Sk3d2GNtq7I+UbXHH9pjGe8LoAjri7n5PzV31DFfge0cd5s36pCwXF3PyfmrvqGK/A9o47zZv1SGWPeJ4fdW5nAAD6iQAEXLz7cW63TtUr8jXuJNaKOk1dpqlzqM1KSlKc8MqMsnwLJiqaZqm6BKAK3vNMeSMl0mk+MG80x5IyXSaT4w69jVjHOOqrlkAVveaY8kZLpNJ8YN5pjyRkuk0nxg7GrGOcdS5ZAFb3mmPJGS6TSfGDeaY8kZLpNJ8YOxqxjnHUuWQcO/wDKOlpBs523ryte7p+Ht91rrVXUUXIvU7SHtZbiHVJQoiM1pUpJqPwaS75Dr7eaY8kZLpNJ8YVLSzb1Rpb0dT1pSdoSJU8nTKaS6b9Io2XS4tukW250LJKv4CKrCqqLr45x1LnCnUGr0jaVNLVGipva6DtC3EFXVlL13qOTuK1j2LBo19UyWsjUaTLCkoWR84/p4OeepX0TSnU8aMW4J616utm6p9dXI1lNU02zccPglKNZ0j1UoJJcSLjrHgsjcW80x5IyXSaT4wyjJ66Y1zHOOpdKyAK3vNMeSMl0mk+MG80x5IyXSaT4w6djVjHOOpcsgCt7zTHkjJdJpPjBvNMeSMl0mk+MHY1YxzjqXLIAre80x5IyXSaT4wbzTHkjJdJpPjB2NWMc46lyyAIaguTb1jVJXR1VEVL2dgmqNtSXjItYySttSk6xFk9UzIzJKjIjJJmUyOVVM0TdLNgAAJYCCsztK759W/inROiCsztK759W/inR46u80cNXrSrcnQAB7EgrdZ/SPEf3TW+2pRZBW6z+keI/umt9tSjtY/in7T6SqFkABjScbTTMbVx9Y0l+jq2VsPNK5loUk0qSf1kZkOKWSA4hgLpvO0qOjl3k1i4bQopcJK06U8ZZlbqmlrT8+yo00jxZxxUY/D9oyz8po3tu6a6EjaW4omvuWrbuameeoayZqKhDrjS0Nvs6zjTThEglKMiJKsJM8GXPP+g7hENdV4RVl0dHVS1QdOzV1tPHMGltSzW++4ltpOCI8ZUouJ8C745elLXpIqy7QsKskLZvqnuCdrqmLrq1x9ELFMstZWwSCqFre1VbRKGlO4IzPiWzIVBuPiJ3qeaSmnXoyfhbc0mtUDdXxcpGKHl7aVJQpxazJk0OGktZauwURZMgzx2Kd+R5aRk2XsanroqKOYJ7VTsNiTxNaudbW19Y841cY7/eFkHNUnovse9OqJiIF2Joq61qOxl8loKZw00hatcSCwlBkk9XKsEecHxLiRGWqKCplLwt7Q/bE3MxrNtPtTbCF3Q29U0VZU0tapmnZeJD7OspLCD1CWsyMyPsTPBkzpgd2AOPpbRkzRW/o0gau56G54Gv0ia7KIQ3GqWkZ5FUpco2zN91RN66HCNOvw11J4FwHWFvW7F2nDU0TDUFPFxlMRkzSUrZIbbIzNRklJcC4mZ/xFRN4kQGh9IFnQ199VBbsVP0LcpF7oVz6qJ8zNlxaaymSk1ozhZFrmZEojIjwfORGWpbRRRz9PortO8Ks3rCRJ3LRcnr6hRMVT1LVKRRMPqM+yJDW01EKPB7MuB6oyatY7SFbgL8j7ju26bdpmalFbbrlM1VuOpSTazeZJ5GzMlGZkSVER5IuPNnnHIzFfHv1dJZ9TJuNaFndIFXGlUnWKTTrZRQk63RbbW/5udXrpxrYPV1c4IYl2N0NjNaX4+yKqkhrVXc1vUchVUzjjtNR0TjCCqDUbbiVE3rHqrJC0mSVKIjT3pzx3QA0LoC0aN2Xe0vVxd1WvUxbkc23UW9alM4zTodUvWaqlpXVPaqjSlxOSJOsXPnVG+hcTfAibv7k5rzJ/2ahkwfaWP83b9UhjXf3JzXmT/s1DJg+0sf5u36pD0f8Uff2buZoAA4sBBQ3dJcP2jHsiE6IKG7pLh+0Y9kQ8lv8Sy4v21KjZKdELePaB37Zj2yBNCFvHtA79sx7ZAzLO7WvDPoU7YX0AAcEtZaPIqEo9L2lGrj7remZardjjkYVxRmiINNOZNkku9tE5Wf1DZo1lo8lYSs0vaUaSPtR6GlqR2OKRmnEmSJc1U5m2aT7+zTlB/WNmgI64u5+T81d9QxX4HtHHebN+qQsFxdz8n5q76hivwPaOO82b9Uhlj3ieH3VuZwAA+okFchVGu8rlM+JpTSoI/o1FHj+aj/AJixitwXdlc//wBL7Mx3s/w1/b3hsb1kAaqvm+rwb0txNkWsmEY5bB1Es5XS7LzuxNp9pvBIbWjXI9qRYynHPk8ap1GG0/3XecZZUNBxkRSXpOPSbVa7XbVygok0DxsvuEhKkrXrr1NROsWNY8nw4+bOhjoMBzlXdUDezVPGQjETB75JvMrTkScN3kSkqpF1CKlrCtdJGnZq1Vax8Fp5zJRTspfOlBGkJyyY960lyVNbbc2/JVNDVJZW4qpfa2aWifMySaUN8TWZpMlHhWsRJzOgbwAc96O9O16Tx6MJedoIJq3772jDNNHk9yqheKnceQpTi1GlxKiaURkSEmnJcVYyfhB9UbN/K3D29W1duTsJLydRFIfgKStI6N1Dbi0a1U4RsPn+jNKkoMjSZ98iMM6B0WA1B1MvcveH/fOd/HOB1Vv9Dj/98w3/APZ0w2/VeNvgNC1U4/bOmzTRMUqG3KmPs+Nq2kPEZoUtsq5SSURGR4yRZwZfWI6N0taVpGW0eUeys9lN8xbshSL5PVKONNtlp5ROFtS2+UuEREnZ4PPEyLjmcOiwFD0MX9IaQbTq6mYpaakmoyUrYeuRRGo2FPUz6mlLb1uyJKtUjIjMzLOMnzjF0037PWMxZ7Vu00dU189Pswx9c9psm0uMPr1+wPOUqbSeOOSyXAzJRbfqvGxgHP1fp+um12LogJSNiZO9Y+cjYOOXRE7T0NWuuQlTDi0qUtbZII165EpWdTgZZySf6oG5dFb10RF6xsXLTtDF00pFLgScYZr9vU8lSypLqlm2onlNkatZRGlecFjAzOgdAgOW5C67rsHTk3cmkU4R8oqwZWQ1LdbeQWoiopVLbMnVHrK4ERKIyI8/qpxxndGWn68bpu+3KSShmaiLm9faJoYGVpVxX6FTiDcqKlpLL6cpJs1J1OK0mRGQZ0bBuS+lbOLoHCLs0ytARH82tVNpP/goy/iLGK3f3aai/vaO/GMiyD1VfCp+8+zdwAAODAQVmdpXfPq38U6J0QVmdpXfPq38U6PHV3mjhq9aVbk6AAPYkFbrP6R4j+6a321KLIK3Wf0jxH901vtqUdrH8U/afSVQsgDAkpRyPeZbajqyvNxKlGdMSNVGNX9Y1qTxPW4F9B/MMXeCq8n5P+dP8UeeaohLNdho9+nr2HKGmcYr9bljamUmmpygkHtCxheUJSk854ERcxDHuK1IS8I7rfPQ9BN0GsS+SyNKioa1i5j1VkZZ+keW8FV5Pyf86f4obwVXk/J/zp/ijM6keNdo+taUgKeDrLaiKuEp1EpmNfoGl0zRlnBpbNOqRlk+Yu+Y9Ssm3Ux9fQFAxhUMhjllKVG3sqnCSSW0Tq4XhKUp454JIu8Pu8FV5Pyf86f4obwVXk/J/wA6f4oZ1IQtlW9bblO5EwMZFrp6c6RlVFRtsm0wa9obSTSRYQaz1tUuGeOMjyqrAtevt8oGptuIqIMnFOlGO0LSqbXUo1qVsjTq5NSlKM8cTUZ85j13gqvJ+T/nT/FDeCq8n5P+dP8AFDOpH2ns236SjjaRiCjWaWMd5RQsN0jaUUjmFJ12kkWEKwtRZTg8KP5zEJcFj3BLy9RV0WkSdg6VzV1KCjo45bTWEkR6qnaVazyZGfFR8TPGCwRTW8FV5Pyf86f4obwVXk/J/wA6f4oX0jwt20ERCmKuSrV3FOstuU6ZuvpaZurJlaiUbRKZaQRIylJ4IiyaSM8mQ/VXYNsSEG5C1VuRNTDuPKqFxz1C0unU6pRrUs2zTqmo1KUozxkzMz5zHrvBVeT8n/On+KG8FV5Pyf8AOn+KGdSD1l29UW4VvOwUY7AEgmyil0bZ0pJI8knZGWrjPHGB5xFhWzb9HWUkXbsTG0lagm6lijoWmkPpJOqSVpSkiURJ7EiPPDgPTeCq8n5P+dP8UN4Kryfk/wCdP8UM6kfLZsq3rKp3WLegYyBYeVruNRlG3TJWr5zJCSIz+sTQht4Kryfk/wCdP8UfUz1UpREcDJJIzxkzYwX/AN0M6kfq7+5Oa8yf9moZMH2lj/N2/VIY139yc15k/wCzUMmD7Sx/m7fqkPT/AMUff2buZoAA4sBBQ3dJcP2jHsiE6IKG7pLh+0Y9kQ8lv8Sy4v21KjZKdELePaB37Zj2yBNCFvHtA79sx7ZAzLO7WvDPoU7YX0AAcEqXaW+2/wDe3X/kW6e0pN3dhjbauyPlG1xx/aYxnvC6DWWjyKhKPS9pRq4+63pmWq3Y45GFcUZoiDTTmTZJLvbROVn9Q2aAjri7n5PzV31DFfge0cd5s36pCwXF3PyfmrvqGK/A9o47zZv1SGWPeJ4fdW5nAAD6iQVuC7srn/8ApfZmLIKtUVjFq3JI10isqaOr22TKsXwbbcQSiNK1f1clqmRnw5yznBH3stcVUxtmPeGw8anR9ynS3H3vy/V5JCVEPyDY519q+y7tNprcMbHGrq8dbOSxg6DTdTlVwsfCPwl2HG3PDScpXUkqqOJ1lTVc+p16ndYNwtdPFBEolpPLZGWM4GyvlKtHyqhPvFn3g+Uq0fKqE+8WfeE6Pa+CeUl04NexPU6FQHBVlTcjshN010ndUnIPUiU8vfOncY2aUEoiZQSVoJJZVgkd/ORc1aOs6V629OuH/OYFqE5Dsf1dR913a6+txztMaur3s544Gf8AKVaPlVCfeLPvB8pVo+VUJ94s+8Gj2kf6TykunBRbf0AlBWxoqhzneUFYr+2N7kmry7/ZnWMY2h7P9rrZyr9XHfyUFb/Uzy8C1ZUem+zegrOk010TQdZ20qNvs0qbfcJzLi9m6tJLSSCI1aykrMbX+Uq0fKqE+8WfeD5SrR8qoT7xZ94Zo1p4J5SXTgpkFbkloTeuJUdRTN6xc7LvyrEbHMUjbka48pTjxKcefbJxClqynhlODLjzj83bQ1una05W06+2rksYnUs1dPL15ULiW32X23WsJaqXDUesgjMjIiMiUWSPAuvylWj5VQn3iz7wfKVaPlVCfeLPvDdHtdmbPIunBS5bQlIStwy8xvSbT85bRQEw11vSbdS4lDpN1KC18tmk3lmaMmRkZFksZGdG6HOt8povrOu+03JjH47U5Njlu0p2mdfOv+jxstbHZZ1sZ4ZOzfKVaPlVCfeLPvB8pVo+VUJ94s+8Gj2ngnlJdOCmQEfIaF25iio7enb1RMzUhOKqIlujZRTHUvqc2KifqkGo062NYiwfPguYespEV2mKptt+thJmy92Z2nmUJl2qV3l2q0+2baNhUr1cbQjNSvowR5PFu+Uq0fKqE+8WfeD5SrR8qoT7xZ94NHtfDPIunBRbp6nxi6Ji9JNc69RVk5WRklQP09OW0i6qhQRNOEalGTmVFkyMi4GZd/Ijq/qal3lS3TUXtdT09PzVCxHMyNDRJoURzLD23a2LRLX2RPEThmpR5NJFwIbL+Uq0fKqE+8WfeD5SrR8qoT7xZ94NGtPBPKS6cGuKTqfpicuV2Uvy827uZet6stx2lZh00JLZqFtKUvWS4rCsNmR8McSMtXHGwaNNG932K7QUclpBXcVvR1MdLSULsS0y+pBESWzefJRm4pKSxlKUZ5zyLP8AKVaPlVCfeLPvB8pVo+VUJ94s+8GjWngnlJdOD5f3aai/vaO/GMiyCoycxQ3kqhoIioak0JrKeqfqaZZOMsoacS6WVkeNZRpSRJLJnkzxgjMrcLriaaKaatU6/Zs6ouAAB50ggrM7Su+fVv4p0TogrM7Su+fVv4p0eOrvNHDV60q3J0AAexIK3Wf0jxH901vtqUWQVus/pHiP7prfbUo7WP4p+0+kqhZAABxSAKzMaRIWDvCOtqpqUFJVlI/XqTtmiKmp2sZddSpZKSgzyRKJJllJ5MsDzLStZjlFLVTN1wtSzEscprzp5BpzkzeMkpwkqPVI+9nGRl8C1AKVBaZrMnrCobxTccZQwFWlB8rra5lpDK1JJWycVr6qXCzg0GeSPJCTrtI1pxjFE9WXPDUrNazyilcfkGkJqG8pTrtmauyTlaCyWSyoi75BfAsQCtvaS7Qp6mup3bqhGqihbcdq2lyLJKp0Nq1HFOFrZQSVGSVGeMGeD4jNlbwgYKocYkpuOj32223VtVVW20pKHF7NtRkoyMiUsjSR99RYLiF4lwFFuPTdZNt2LWXeu446vgqVwmDqY+saeSt4zIiaSol6pr482SwXE8EQuEVK0U5HU8hG1lPIUFSgnGaqldS606k+ZSVJMyMvpILxlAAoUbpig6rSXP2ZV1kfHSEc5TM0yKivQl6uddZJ1SW2TwZ6iVN8xqzr97HFfcL6AhKq+LcobiYgKm4Iunnqgssxbta2mqcLGcpaNWsf8CFRtjTfEXdd900EfUxBwNtnsq+WclkEsnSSlSjSySTLYp1jSbqlp7NCkkk8GZL4GyQFdY0j2lVQSJtm6IV2GW7sESKJBlVOpz9wnCVqmr6M5H4q9Jtnx9NGVFVdcHTU8pjkDr0iyhFXk8FsjNWF8f3chfAz7v7k5rzJ/wBmoZMH2lj/ADdv1SGNd/cnNeZP+zUMmD7Sx/m7fqkO/wDxR9/Zu5mgADiwEFDd0lw/aMeyITogobukuH7Rj2RDyW/xLLi/bUqNkp0Qt49oHftmPbIE0IW8e0Dv2zHtkDMs7ta8M+hTthfQABwS1lo8lYSs0vaUaSPtR6GlqR2OKRmnEmSJc1U5m2aT7+zTlB/WNmil2lvtv/e3X/kW6e0pN3dhjbauyPlG1xx/aYxnvC6AI64u5+T81d9QxX4HtHHebN+qQsFxdz8n5q76hivwPaOO82b9Uhlj3ieH3VuZwAA+okAAAMBgAAMBgAAMBgAAMCMucv8A2alvNHvUMSYjLn7mpbzR71DAal6ir/ddsHzR38Q6N24Gkuoq/wB12wfNHfxDo3aJp/DAYDAAKDAYAADAYAAAAAAAAABBWZ2ld8+rfxTonRBWZ2ld8+rfxTo8dXeaOGr1pVuToAA9iQVus/pHiP7prfbUosgrkgWz0hQjij1ULjq1lJn317SmUSfr1UqP/wAJ/MO1j+KftPpKoZFyXvblm8n6/wA/FwfKdbYdcq1un2urjW1ddRa2NZOcc2S+cQvy3aOvL61/vmm98XUBw1paI0iwszpEkbjuOzk0s9RLsish4Sto65hbL9bUvGTpJVr4LU5OzlR4LiZEZmRkUbf+hif1LopLUiWWaZjR+VsxKkOtM8odddXtkFx7AyQ21hSiItZzn4HjokBObEjnnSFY923BdNjXJGQk/G0UZFVNGiJjKmKOsjn1LbJKz5TtGDJTaDTrNqNaeGMkaiFrsTRA1aOk7lTEZsrfhbcpoiIqH3UuuLdcffdq1c+sRnhjJmRZyeOGcbbAbmxfeOfajRRcCepxkodEGhd1TEqcvK0CH2Uu1G1kk1D7e0NWzNewy2RmrV7FJZwKrpEjrkk9IRSkxZlUs7gnoajjoRVVSOVFRRR7NTXuGf6XZJPa5I0qXg8FxMjIz6sGLURdFWVtJWP0jD9XRmo6Z9xpKnGDUnVUaFGWU5LgeOcuAyaRz3N6N7vrKeYuUrYzUzV1xMrVWvS1dOTyaKiJGrrOKWllTynGkrUWvq4Ik6xmQ3jc8RWXTaVTQ09QqGrappOHFreywrJGZGdO80vJYMuwdT9ZlwOdAbEXCk6MrElbHYkESk717VUqQptWvXq2ZER5L/a62pPjkv1DRzcSPhjUFXoxu656i6YeqtMo05+725p65qirpllT0VO6zsCbShandsbdMkiSaSSnamZqzlI6VAM2Ng5wsLQ9JRN3zNReaJc41ifqrlOQXVx6Yt9xLyl0zqjSkqslNt6hajitmnZcDNOCLFt61Zq7tDltztHEuTSJG8X7skYYnG2l11GupqFsITtVJQeCOkcJK1ERk0XHmHSzzLdQytp1CXWnEmlaFllKiPgZGR85D9kRERERYIu8QzNgc+0miOfn74jpyZhGaSPkrsO4q+N2zThUTdNHHTUiXMKMluqdJDqjRrERkRZPVyfjcmhmauHTPd1RKsTdXa1yopKZT0ZUx6KdFG2ylLlM/tk8pQRubVf+znhW144MjMdEAGbAibv7k5rzJ/2ahkwfaWP83b9Uhh3o8hiz5xxZ6qSoXvUPh9Yz4pldPF0bThaq0MoSovmMkkRj0z8KPv7Q3cygABxYCChu6S4ftGPZEJ0QUN3SXD9ox7Ih5Lf4llxftqVGyU6IW8e0Dv2zHtkCaELePaB37Zj2yBmWd2teGfQp2wvoAA4Jay0eRUJR6XtKNXH3W9My1W7HHIwrijNEQaacybJJd7aJys/qGzRrLR5KwlZpe0o0kfaj0NLUjscUjNOJMkS5qpzNs0n39mnKD+sbNAR1xdz8n5q76hivwPaOO82b9UhYLi7n5PzV31DFfge0cd5s36pDLHvE8PurczgAB9RIAAAAAAAAAAAAACMufualvNHvUMSYjLn7mpbzR71DGDUvUVf7rtg+aO/iHRu0aS6ir/ddsHzR38Q6N2jKfwwAAAoAAAAAAAAAAAAAAQVmdpXfPq38U6J0QVmdpXfPq38U6PHV3mjhq9aVbk6AAPYkGJJxdNL02wqUGpJKJaFIUaVoUXMpKi4kZfOQywGxMxN8CuHZq+9cE0kuYi5Sk/8AzRkfNzXPKKb6Qj3BZAHXtq8fKG3yre5rnlFN9IR7gbmueUU30hHuCyAN7avHygvlW9zXPKKb6Qj3A3Nc8opvpCPcFkAO2rx8oL5Vvc1zyim+kI9wNzXPKKb6Qj3BZADtq8fKC+WhdBMvOaQ5LSWxLXHJmi37srIai2DiEHydskGnW7Hirsj4ja25rnlFN9IR7g1H1KHbvTf/AIgyPqtDoATTb2l23ygvlW9zXPKKb6Qj3A3Nc8opvpCPcFkAV21ePlBfKt7mueUU30hHuBua55RTfSEe4LIAdtXj5QXyre5rnlFN9IR7gbmueUU30hHuCyAHbV4+UF8oGltBhqoaeqq6QkzaUS20Vr+s2lRcSVqkREZkfEjMjweDLiRCeABzqrqr/FJfeAACGAgobukuH7Rj2RCdEFDd0lw/aMeyIeS3+JZcX7alRslOiFvHtA79sx7ZAmhC3j2gd+2Y9sgZlndrXhn0KdsL6AAOCVLtLfbf+9uv/It09pSbu7DG21dkfKNrjj+0xjPeF0GstHkVCUel7SjVx91vTMtVuxxyMK4ozREGmnMmySXe2icrP6hs0BHXF3PyfmrvqGK/A9o47zZv1SFguLufk/NXfUMV+B7Rx3mzfqkMse8Tw+6tzOAAH1EgAAAAAAAAAAAAAIy5+5qW80e9QxJiMufualvNHvUMYNS9RV/uu2D5o7+IdG7RpLqKv912wfNHfxDo3aMp/DAAACgAAAAAAAAAAAAABBWZ2ld8+rfxTonRXKW3ZSPS61STSGqdb7ryULoyUado4pZlnW48VGPFa59NrTaU0zVERMart804zGCo2XLGAgutc749a6CXvB1rnfHrXQS94V29p8qr9P8AIujFOgILrXO+PWugl7wda53x610EveDt7T5VX6f5F0Yp0BBda53x610EveDrXO+PWugl7wdvafKq/T/IujFOgILrXO+PWugl7wda53x610EveDt7T5VX6f5F0Yp0BBda53x610EveDrXO+PWugl7wdvafKq/T/IujFOgILrXO+PWugl7wda53x610EveDt7T5VX6f5F0YtOdSh2703/4gyPqtDoAc1dS1QSzs1pp2Es2wab9kEuGdIStdWq3lX63D6hvjrXO+PWugl7wyLauNUWUz+X+TLvqnQEF1rnfHrXQS94Otc749a6CXvDe3tPlVfp/k26MU6Agutc749a6CXvB1rnfHrXQS94O3tPlVfp/kXRinQEF1rnfHrXQS94Otc749a6CXvB29p8qr9P8i6MU6Agutc749a6CXvB1rnfHrXQS94O3tPlVfp/kXRinQEF1rnfHrXQS94Otc749a6CXvB29p8qr9P8AIujFOiChu6S4ftGPZEHWud8etdBL3hkQkM/Gv11RU1nLH6paVKUTRNkWqkkkRFk/mHKZtLW0s/8AHMRE3683wzG6ZxNUROtKiFvHtA79sx7ZAmhC3j2gd+2Y9sgdMs7ta8M+hTthfQABwS1lo8lYSs0vaUaSPtR6GlqR2OKRmnEmSJc1U5m2aT7+zTlB/WNmil2lvtv/AHt1/wCRbp7Sk3d2GNtq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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "# Setting xray to 1 will show the internal structure of the nested graph\n", + "display(Image(grandparent_graph.get_graph(xray=2).draw_mermaid_png()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we run until the interrupt, we can now see that there are snapshots of the state of all three graphs" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "((), {'router_node': {'to_continue': True}})\n", + "(('graph:e18ecd45-5dfb-53b0-bcb7-db793924e9a8',), {'router_node': {'route': 'weather'}})\n", + "(('graph:e18ecd45-5dfb-53b0-bcb7-db793924e9a8', 'weather_graph:12bd3069-de24-5bc6-b4f1-f39527605781'), {'model_node': {'city': 'San Francisco'}})\n" + ] + } + ], + "source": [ + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "inputs = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf\"}]}\n", + "for update in grandparent_graph.stream(inputs, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Grandparent State:\n", + "{'messages': [HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848')], 'to_continue': True}\n", + "---------------\n", + "Parent Graph State:\n", + "{'messages': [HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848')], 'route': 'weather'}\n", + "---------------\n", + "Subgraph State:\n", + "{'messages': [HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848')], 'city': 'San Francisco'}\n" + ] + } + ], + "source": [ + "state = grandparent_graph.get_state(config, subgraphs=True)\n", + "print(\"Grandparent State:\")\n", + "print(state.values)\n", + "print(\"---------------\")\n", + "print(\"Parent Graph State:\")\n", + "print(state.tasks[0].state.values)\n", + "print(\"---------------\")\n", + "print(\"Subgraph State:\")\n", + "print(state.tasks[0].state.tasks[0].state.values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now continue, acting as the node three levels down" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(('graph:e18ecd45-5dfb-53b0-bcb7-db793924e9a8',), {'weather_graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848'), AIMessage(content='rainy', id='be926b59-c647-4355-88fd-a429b9e2b420')]}})\n", + "((), {'graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848'), AIMessage(content='rainy', id='be926b59-c647-4355-88fd-a429b9e2b420')]}})\n", + "[HumanMessage(content=\"what's the weather in sf\", id='3bb28060-3d30-49a7-9f84-c90b6ada7848'), AIMessage(content='rainy', id='be926b59-c647-4355-88fd-a429b9e2b420')]\n" + ] + } + ], + "source": [ + "grandparent_graph_state = state\n", + "parent_graph_state = grandparent_graph_state.tasks[0].state\n", + "subgraph_state = parent_graph_state.tasks[0].state\n", + "grandparent_graph.update_state(subgraph_state.config, {\"messages\": [{\"role\": \"assistant\", \"content\": \"rainy\"}]}, as_node=\"weather_node\")\n", + "for update in grandparent_graph.stream(None, config=config, stream_mode=\"updates\", subgraphs=True):\n", + " print(update)\n", + "print(grandparent_graph.get_state(config).values['messages'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As in the cases above, we can see that the AI responds with \"rainy\" as we expect.\n", + "\n", + "We can explore the state history to see how the state of the grandparent graph was updated at each step." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "StateSnapshot(values={'messages': [HumanMessage(content=\"what's the weather in sf\", id='5ff89e4d-8255-4d23-8b55-01633c112720'), AIMessage(content='rainy', id='7c80f847-248d-4b8f-8238-633ed757b353')], 'to_continue': True}, next=(), config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f40-7a2c-6f9e-8002-a37a61b26709'}}, metadata={'source': 'loop', 'writes': {'graph': {'messages': [HumanMessage(content=\"what's the weather in sf\", id='5ff89e4d-8255-4d23-8b55-01633c112720'), AIMessage(content='rainy', id='7c80f847-248d-4b8f-8238-633ed757b353')]}}, 'step': 2, 'parents': {}}, created_at='2024-08-30T17:19:35.793847+00:00', parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f312-6338-8001-766acddc781e'}}, tasks=())\n", + "-----\n", + "StateSnapshot(values={'messages': [HumanMessage(content=\"what's the weather in sf\", id='5ff89e4d-8255-4d23-8b55-01633c112720')], 'to_continue': True}, next=('graph',), config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f312-6338-8001-766acddc781e'}}, metadata={'source': 'loop', 'writes': {'router_node': {'to_continue': True}}, 'step': 1, 'parents': {}}, created_at='2024-08-30T17:19:21.627097+00:00', parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f303-61d0-8000-1945c8a74e9e'}}, tasks=(PregelTask(id='b59fe96f-fdce-5afe-aa58-bd2876a0d592', name='graph', error=None, interrupts=(), state={'configurable': {'thread_id': '2', 'checkpoint_ns': 'graph:b59fe96f-fdce-5afe-aa58-bd2876a0d592'}}),))\n", + "-----\n", + "StateSnapshot(values={'messages': [HumanMessage(content=\"what's the weather in sf\", id='5ff89e4d-8255-4d23-8b55-01633c112720')]}, next=('router_node',), config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f303-61d0-8000-1945c8a74e9e'}}, metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}}, created_at='2024-08-30T17:19:21.620923+00:00', parent_config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f2f9-6d6a-bfff-c8b76e5b2462'}}, tasks=(PregelTask(id='e3d4a97a-f4ca-5260-801e-e65b02907825', name='router_node', error=None, interrupts=(), state=None),))\n", + "-----\n", + "StateSnapshot(values={'messages': []}, next=('__start__',), config={'configurable': {'thread_id': '2', 'checkpoint_ns': '', 'checkpoint_id': '1ef66f3f-f2f9-6d6a-bfff-c8b76e5b2462'}}, metadata={'source': 'input', 'writes': {'messages': [{'role': 'user', 'content': \"what's the weather in sf\"}]}, 'step': -1, 'parents': {}}, created_at='2024-08-30T17:19:21.617127+00:00', parent_config=None, tasks=(PregelTask(id='f0538638-b794-58fc-a406-980d2fea28a1', name='__start__', error=None, interrupts=(), state=None),))\n", + "-----\n" + ] + } + ], + "source": [ + "for state in grandparent_graph.get_state_history(config):\n", + " print(state)\n", + " print(\"-----\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/time-travel.ipynb b/examples/time-travel.ipynb index dcdd035c0..2dd46e42b 100644 --- a/examples/time-travel.ipynb +++ b/examples/time-travel.ipynb @@ -46,7 +46,10 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain_openai"] + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain_openai" + ] }, { "cell_type": "markdown", @@ -62,7 +65,18 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\n\ndef _set_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_set_env(\"ANTHROPIC_API_KEY\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"OPENAI_API_KEY\")" + ] }, { "cell_type": "markdown", @@ -78,7 +92,10 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n_set_env(\"LANGCHAIN_API_KEY\")"] + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] }, { "cell_type": "markdown", @@ -96,7 +113,22 @@ "id": "f5319e01", "metadata": {}, "outputs": [], - "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import add_messages\n\n# `add_messages`` essentially does this\n# (with more robust handling)\n# def add_messages(left: list, right: list):\n# return left + right\n\n\nclass State(TypedDict):\n messages: Annotated[list, add_messages]"] + "source": [ + "from typing import Annotated\n", + "\n", + "from typing_extensions import TypedDict\n", + "\n", + "from langgraph.graph.message import add_messages\n", + "\n", + "# `add_messages`` essentially does this\n", + "# (with more robust handling)\n", + "# def add_messages(left: list, right: list):\n", + "# return left + right\n", + "\n", + "\n", + "class State(TypedDict):\n", + " messages: Annotated[list, add_messages]" + ] }, { "cell_type": "markdown", @@ -116,7 +148,19 @@ "id": "d7ef57dd-5d6e-4ad3-9377-a92201c1310e", "metadata": {}, "outputs": [], - "source": ["from langchain_core.tools import tool\n\n\n@tool\ndef search(query: str):\n \"\"\"Call to surf the web.\"\"\"\n # This is a placeholder for the actual implementation\n return [\"The weather is cloudy with a chance of meatballs.\"]\n\n\ntools = [search]"] + "source": [ + "from langchain_core.tools import tool\n", + "\n", + "\n", + "@tool\n", + "def search(query: str):\n", + " \"\"\"Call to surf the web.\"\"\"\n", + " # This is a placeholder for the actual implementation\n", + " return [\"The weather is cloudy with a chance of meatballs.\"]\n", + "\n", + "\n", + "tools = [search]" + ] }, { "cell_type": "markdown", @@ -133,7 +177,11 @@ "id": "5cf3331e-ccb3-41c8-aeb9-a840a94d41e7", "metadata": {}, "outputs": [], - "source": ["from langgraph.prebuilt import ToolNode\n\ntool_node = ToolNode(tools)"] + "source": [ + "from langgraph.prebuilt import ToolNode\n", + "\n", + "tool_node = ToolNode(tools)" + ] }, { "cell_type": "markdown", @@ -157,7 +205,11 @@ "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", "metadata": {}, "outputs": [], - "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0)"] + "source": [ + "from langchain_openai import ChatOpenAI\n", + "\n", + "model = ChatOpenAI(temperature=0)" + ] }, { "cell_type": "markdown", @@ -175,7 +227,9 @@ "id": "cd3cbae5-d92c-4559-a4aa-44721b80d107", "metadata": {}, "outputs": [], - "source": ["model = model.bind_tools(tools)"] + "source": [ + "model = model.bind_tools(tools)" + ] }, { "cell_type": "markdown", @@ -210,7 +264,20 @@ "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": ["from typing import Literal\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state: State) -> Literal[\"continue\", \"end\"]:\n last_message = state[\"messages\"][-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\""] + "source": [ + "from typing import Literal\n", + "\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state: State) -> Literal[\"continue\", \"end\"]:\n", + " last_message = state[\"messages\"][-1]\n", + " # If there is no function call, then we finish\n", + " if not last_message.tool_calls:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"" + ] }, { "cell_type": "markdown", @@ -228,7 +295,50 @@ "id": "812b4e70-4956-4415-8880-db48b3dcbad2", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(State)\n\n\n# Define the two nodes we will cycle between\ndef call_model(state: State) -> State:\n return {\"messages\": model.invoke(state[\"messages\"])}\n\n\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", tool_node)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")"] + "source": [ + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(State)\n", + "\n", + "\n", + "# Define the two nodes we will cycle between\n", + "def call_model(state: State) -> State:\n", + " return {\"messages\": model.invoke(state[\"messages\"])}\n", + "\n", + "\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", tool_node)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.add_edge(START, \"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END,\n", + " },\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")" + ] }, { "cell_type": "markdown", @@ -246,7 +356,11 @@ "id": "6845ed6a-d155-4105-9160-28849877248b", "metadata": {}, "outputs": [], - "source": ["from langgraph.checkpoint.memory import MemorySaver\n\nmemory = MemorySaver()"] + "source": [ + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "memory = MemorySaver()" + ] }, { "cell_type": "code", @@ -254,7 +368,12 @@ "id": "79d29875-8aa8-434c-9f20-1c58346a6249", "metadata": {}, "outputs": [], - "source": ["# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile(checkpointer=memory)"] + "source": [ + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile(checkpointer=memory)" + ] }, { "cell_type": "markdown", @@ -281,7 +400,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(app.get_graph().draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "markdown", @@ -312,7 +439,14 @@ ] } ], - "source": ["from langchain_core.messages import HumanMessage\n\nconfig = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"hi! I'm bob\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "input_message = HumanMessage(content=\"hi! I'm bob\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] }, { "cell_type": "markdown", @@ -350,7 +484,9 @@ "output_type": "execute_result" } ], - "source": ["app.get_state(config).values"] + "source": [ + "app.get_state(config).values" + ] }, { "cell_type": "markdown", @@ -379,7 +515,9 @@ "output_type": "execute_result" } ], - "source": ["app.get_state(config).next"] + "source": [ + "app.get_state(config).next" + ] }, { "cell_type": "markdown", @@ -426,7 +564,12 @@ ] } ], - "source": ["config = {\"configurable\": {\"thread_id\": \"2\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n event[\"messages\"][-1].pretty_print()"] + "source": [ + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "input_message = HumanMessage(content=\"what is the weather in sf currently\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] }, { "cell_type": "markdown", @@ -466,7 +609,9 @@ "id": "5a68afc0-606f-4294-a872-b2b563be0d69", "metadata": {}, "outputs": [], - "source": ["app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])"] + "source": [ + "app_w_interrupt = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])" + ] }, { "cell_type": "code", @@ -490,7 +635,14 @@ ] } ], - "source": ["config = {\"configurable\": {\"thread_id\": \"4\"}}\ninput_message = HumanMessage(content=\"what is the weather in sf currently\")\nfor event in app_w_interrupt.stream(\n {\"messages\": [input_message]}, config, stream_mode=\"values\"\n):\n event[\"messages\"][-1].pretty_print()"] + "source": [ + "config = {\"configurable\": {\"thread_id\": \"4\"}}\n", + "input_message = HumanMessage(content=\"what is the weather in sf currently\")\n", + "for event in app_w_interrupt.stream(\n", + " {\"messages\": [input_message]}, config, stream_mode=\"values\"\n", + "):\n", + " event[\"messages\"][-1].pretty_print()" + ] }, { "cell_type": "markdown", @@ -526,7 +678,10 @@ "output_type": "execute_result" } ], - "source": ["current_values = app_w_interrupt.get_state(config)\ncurrent_values.next"] + "source": [ + "current_values = app_w_interrupt.get_state(config)\n", + "current_values.next" + ] }, { "cell_type": "markdown", @@ -555,7 +710,9 @@ "output_type": "execute_result" } ], - "source": ["current_values.values[\"messages\"][-1].tool_calls"] + "source": [ + "current_values.values[\"messages\"][-1].tool_calls" + ] }, { "cell_type": "markdown", @@ -571,7 +728,11 @@ "id": "060e2e33-1f6a-40ef-850e-161b308986fb", "metadata": {}, "outputs": [], - "source": ["current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n \"query\"\n] = \"weather in San Francisco today\""] + "source": [ + "current_values.values[\"messages\"][-1].tool_calls[0][\"args\"][\n", + " \"query\"\n", + "] = \"weather in San Francisco today\"" + ] }, { "cell_type": "code", @@ -591,7 +752,9 @@ "output_type": "execute_result" } ], - "source": ["app_w_interrupt.update_state(config, current_values.values)"] + "source": [ + "app_w_interrupt.update_state(config, current_values.values)" + ] }, { "cell_type": "markdown", @@ -629,7 +792,9 @@ "output_type": "execute_result" } ], - "source": ["app_w_interrupt.get_state(config).values"] + "source": [ + "app_w_interrupt.get_state(config).values" + ] }, { "cell_type": "code", @@ -648,7 +813,9 @@ "output_type": "execute_result" } ], - "source": ["app_w_interrupt.get_state(config).next"] + "source": [ + "app_w_interrupt.get_state(config).next" + ] }, { "cell_type": "markdown", @@ -679,7 +846,11 @@ ] } ], - "source": ["for event in app_w_interrupt.stream(None, config):\n for v in event.values():\n print(v)"] + "source": [ + "for event in app_w_interrupt.stream(None, config):\n", + " for v in event.values():\n", + " print(v)" + ] }, { "cell_type": "markdown", @@ -726,7 +897,13 @@ ] } ], - "source": ["for state in app_w_interrupt.get_state_history(config):\n print(state)\n print(\"--\")\n if len(state.values[\"messages\"]) == 2:\n to_replay = state"] + "source": [ + "for state in app_w_interrupt.get_state_history(config):\n", + " print(state)\n", + " print(\"--\")\n", + " if len(state.values[\"messages\"]) == 2:\n", + " to_replay = state" + ] }, { "cell_type": "markdown", @@ -754,7 +931,9 @@ "output_type": "execute_result" } ], - "source": ["to_replay.values"] + "source": [ + "to_replay.values" + ] }, { "cell_type": "code", @@ -773,7 +952,9 @@ "output_type": "execute_result" } ], - "source": ["to_replay.next"] + "source": [ + "to_replay.next" + ] }, { "cell_type": "markdown", @@ -806,7 +987,11 @@ ] } ], - "source": ["for event in app_w_interrupt.stream(None, to_replay.config):\n for v in event.values():\n print(v)"] + "source": [ + "for event in app_w_interrupt.stream(None, to_replay.config):\n", + " for v in event.values():\n", + " print(v)" + ] }, { "cell_type": "markdown", @@ -834,7 +1019,18 @@ "id": "b084f141-5800-487b-b115-d2e58421b963", "metadata": {}, "outputs": [], - "source": ["from langchain_core.messages import AIMessage\n\nbranch_config = app_w_interrupt.update_state(\n to_replay.config,\n {\n \"messages\": [\n AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n ]\n },\n)"] + "source": [ + "from langchain_core.messages import AIMessage\n", + "\n", + "branch_config = app_w_interrupt.update_state(\n", + " to_replay.config,\n", + " {\n", + " \"messages\": [\n", + " AIMessage(content=\"All done here!\", id=to_replay.values[\"messages\"][-1].id)\n", + " ]\n", + " },\n", + ")" + ] }, { "cell_type": "code", @@ -842,7 +1038,9 @@ "id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641", "metadata": {}, "outputs": [], - "source": ["branch_state = app_w_interrupt.get_state(branch_config)"] + "source": [ + "branch_state = app_w_interrupt.get_state(branch_config)" + ] }, { "cell_type": "code", @@ -862,7 +1060,9 @@ "output_type": "execute_result" } ], - "source": ["branch_state.values"] + "source": [ + "branch_state.values" + ] }, { "cell_type": "code", @@ -881,7 +1081,9 @@ "output_type": "execute_result" } ], - "source": ["branch_state.next"] + "source": [ + "branch_state.next" + ] }, { "cell_type": "markdown", From 1774dbb860e3eefe65faca7f0c7ec4032e2561fa Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Wed, 4 Sep 2024 15:13:19 -0700 Subject: [PATCH 262/287] [docs]: adding js/curl tabs (#1558) * jsifying * x * z * x * fix background run output * reformatting * z * z * z * z * z * add tabs to stream debug * default urls --- docs/docs/cloud/concepts/api.md | 6 +- docs/docs/cloud/deployment/test_locally.md | 8 +- docs/docs/cloud/how-tos/background_run.md | 443 ++++++++++++++++++ .../docs/cloud/how-tos/check_thread_status.md | 12 +- .../docs/cloud/how-tos/configuration_cloud.md | 264 +++++++++++ docs/docs/cloud/how-tos/copy_threads.md | 24 +- docs/docs/cloud/how-tos/cron_jobs.md | 184 ++++++++ docs/docs/cloud/how-tos/enqueue_concurrent.md | 108 ++++- .../how-tos/human_in_the_loop_breakpoint.md | 9 +- .../how-tos/human_in_the_loop_edit_state.md | 16 +- .../human_in_the_loop_review_tool_calls.md | 412 ++++++++++++++-- .../how-tos/human_in_the_loop_time_travel.md | 13 +- .../how-tos/human_in_the_loop_user_input.md | 46 +- docs/docs/cloud/how-tos/index.md | 12 +- .../cloud/how-tos/interrupt_concurrent.md | 108 ++++- docs/docs/cloud/how-tos/reject_concurrent.md | 102 +++- .../docs/cloud/how-tos/rollback_concurrent.md | 100 +++- docs/docs/cloud/how-tos/same-thread.md | 310 ++++++++++++ docs/docs/cloud/how-tos/stateless_runs.md | 180 +++++++ docs/docs/cloud/how-tos/stream_debug.md | 88 +++- docs/docs/cloud/how-tos/stream_events.md | 50 +- docs/docs/cloud/how-tos/stream_messages.md | 40 +- docs/docs/cloud/how-tos/stream_multiple.md | 40 +- docs/docs/cloud/how-tos/stream_updates.md | 34 +- docs/docs/cloud/how-tos/stream_values.md | 25 +- docs/docs/cloud/how-tos/webhooks.md | 125 +++++ docs/mkdocs.yml | 12 +- examples/cloud_examples/background_run.ipynb | 388 --------------- .../cloud_examples/configuration_cloud.ipynb | 198 -------- examples/cloud_examples/cron_jobs.ipynb | 132 ------ .../cloud_examples/img/webhook_results.png | Bin 442633 -> 0 bytes examples/cloud_examples/same-thread.ipynb | 192 -------- examples/cloud_examples/stateless_runs.ipynb | 152 ------ examples/cloud_examples/webhooks.ipynb | 72 --- 34 files changed, 2489 insertions(+), 1416 deletions(-) create mode 100644 docs/docs/cloud/how-tos/background_run.md create mode 100644 docs/docs/cloud/how-tos/configuration_cloud.md create mode 100644 docs/docs/cloud/how-tos/cron_jobs.md create mode 100644 docs/docs/cloud/how-tos/same-thread.md create mode 100644 docs/docs/cloud/how-tos/stateless_runs.md create mode 100644 docs/docs/cloud/how-tos/webhooks.md delete mode 100644 examples/cloud_examples/background_run.ipynb delete mode 100644 examples/cloud_examples/configuration_cloud.ipynb delete mode 100644 examples/cloud_examples/cron_jobs.ipynb delete mode 100644 examples/cloud_examples/img/webhook_results.png delete mode 100644 examples/cloud_examples/same-thread.ipynb delete mode 100644 examples/cloud_examples/stateless_runs.ipynb delete mode 100644 examples/cloud_examples/webhooks.ipynb diff --git a/docs/docs/cloud/concepts/api.md b/docs/docs/cloud/concepts/api.md index e81c672f3..99bd6c9ab 100644 --- a/docs/docs/cloud/concepts/api.md +++ b/docs/docs/cloud/concepts/api.md @@ -39,7 +39,7 @@ It's often useful to run graphs on some schedule. LangGraph Cloud supports cron - Create a new thread with the specified assistant - Send the specified input to that thread -Note that this sends the same input to the thread every time. See the [how-to guide](../how-tos/cloud_examples/cron_jobs.ipynb) for creating cron jobs. +Note that this sends the same input to the thread every time. See the [how-to guide](../how-tos/cron_jobs.md) for creating cron jobs. The LangGraph Cloud API provides several endpoints for creating and managing cron jobs. See the [API reference](../reference/api/api_ref.html#tag/runscreate/POST/threads/{thread_id}/runs/crons) for more details. @@ -182,13 +182,13 @@ The only difference is in stateless background runs, if the task worker dies hal - whereas a stateful background run would retry from the last successful checkpoint - a stateless background run would retry from the beginning -See the [how-to guide](../how-tos/cloud_examples/stateless_runs.ipynb) for creating stateless runs. +See the [how-to guide](../how-tos/stateless_runs.md) for creating stateless runs. ### Webhooks For all types of runs, langgraph cloud supports completion webhooks. When you create the run you can pass a webhook URL to be called when the completes (successfully or not). This is especially useful for background runs and cron jobs, as the webhook can give you an indication the run has completed and you can perform further actions for your appilcation. -See this [how-to guide](../how-tos/cloud_examples/webhooks.ipynb) to learn about how to use webhooks with LangGraph Cloud. +See this [how-to guide](../how-tos/webhooks.md) to learn about how to use webhooks with LangGraph Cloud. ## Deployment diff --git a/docs/docs/cloud/deployment/test_locally.md b/docs/docs/cloud/deployment/test_locally.md index a5a06624a..2822b4052 100644 --- a/docs/docs/cloud/deployment/test_locally.md +++ b/docs/docs/cloud/deployment/test_locally.md @@ -49,6 +49,7 @@ You can either initialize by passing authentication or by setting an environment # only pass the url argument to get_client() if you changed the default port when calling langgraph up client = get_client(url=,api_key=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -60,7 +61,8 @@ You can either initialize by passing authentication or by setting an environment // only set the apiUrl if you changed the default port when calling langgraph up const client = new Client({ apiUrl: , apiKey: }); - const assistantId = "agent" + // Using the graph deployed with the name "agent" + const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -85,6 +87,7 @@ If you have a `LANGCHAIN_API_KEY` set in your environment, you do not need to ex # only pass the url argument to get_client() if you changed the default port when calling langgraph up client = get_client() + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -96,7 +99,8 @@ If you have a `LANGCHAIN_API_KEY` set in your environment, you do not need to ex // only set the apiUrl if you changed the default port when calling langgraph up const client = new Client(); - const assistantId = "agent" + // Using the graph deployed with the name "agent" + const assistantId = "agent"; const thread = await client.threads.create(); ``` diff --git a/docs/docs/cloud/how-tos/background_run.md b/docs/docs/cloud/how-tos/background_run.md new file mode 100644 index 000000000..d648e25b0 --- /dev/null +++ b/docs/docs/cloud/how-tos/background_run.md @@ -0,0 +1,443 @@ +# How to kick off background runs + +This guide covers how to kick off background runs for your agent. +This can be useful for long running jobs. + +First let's set up our client and thread: + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" + # create thread + thread = await client.threads.create() + print(thread) + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; + // create thread + const thread = await client.threads.create(); + console.log(thread); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' + ``` + +Output: + + { + 'thread_id': '5cb1e8a1-34b3-4a61-a34e-71a9799bd00d', + 'created_at': '2024-08-30T20:35:52.062934+00:00', + 'updated_at': '2024-08-30T20:35:52.062934+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } + +If we list the current runs on this thread, we will see that it's empty: + +=== "Python" + + ```python + runs = await client.runs.list(thread["thread_id"]) + print(runs) + ``` + +=== "Javascript" + + ```js + let runs = await client.runs.list(thread['thread_id']); + console.log(runs); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//runs + ``` + +Output: + + [] + +Now let's kick off a run: + +=== "Python" + + ```python + input = {"messages": [{"role": "human", "content": "what's the weather in sf"}]} + run = await client.runs.create(thread["thread_id"], assistant_id, input=input) + ``` + +=== "Javascript" + + ```js + let input = {"messages": [{"role": "human", "content": "what's the weather in sf"}]}; + let run = await client.runs.create(thread["thread_id"], assistantID, { input }); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": + }' + ``` + +The first time we poll it, we can see `status=pending`: + +=== "Python" + + ```python + print(await client.runs.get(thread["thread_id"], run["run_id"])) + ``` + +=== "Javascript" + + ```js + console.log(await client.runs.get(thread["thread_id"], run["run_id"])); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//runs/ + ``` + +Output: + + { + "run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b", + "thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a", + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca", + "created_at": "2024-09-04T01:46:47.244887+00:00", + "updated_at": "2024-09-04T01:46:47.244887+00:00", + "metadata": {}, + "status": "pending", + "kwargs": { + "input": { + "messages": [ + { + "role": "human", + "content": "what's the weather in sf" + } + ] + }, + "config": { + "metadata": { + "created_by": "system" + }, + "configurable": { + "run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b", + "user_id": "", + "graph_id": "agent", + "thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a", + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca", + "checkpoint_id": null + } + }, + "webhook": null, + "temporary": false, + "stream_mode": [ + "values" + ], + "feedback_keys": null, + "interrupt_after": null, + "interrupt_before": null + }, + "multitask_strategy": "reject" + } + + + +Now we can join the run, wait for it to finish and check that status again: + +=== "Python" + + ```python + await client.runs.join(thread["thread_id"], run["run_id"]) + print(await client.runs.get(thread["thread_id"], run["run_id"])) + ``` + +=== "Javascript" + + ```js + await client.runs.join(thread["thread_id"], run["run_id"]); + console.log(await client.runs.get(thread["thread_id"], run["run_id"])); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//runs//join && + curl --request GET \ + --url /threads//runs/ + ``` + +Output: + + { + "run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b", + "thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a", + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca", + "created_at": "2024-09-04T01:46:47.244887+00:00", + "updated_at": "2024-09-04T01:46:47.244887+00:00", + "metadata": {}, + "status": "success", + "kwargs": { + "input": { + "messages": [ + { + "role": "human", + "content": "what's the weather in sf" + } + ] + }, + "config": { + "metadata": { + "created_by": "system" + }, + "configurable": { + "run_id": "1ef6a5f8-bd86-6763-bbd6-bff042db7b1b", + "user_id": "", + "graph_id": "agent", + "thread_id": "7885f0cf-94ad-4040-91d7-73f7ba007c8a", + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca", + "checkpoint_id": null + } + }, + "webhook": null, + "temporary": false, + "stream_mode": [ + "values" + ], + "feedback_keys": null, + "interrupt_after": null, + "interrupt_before": null + }, + "multitask_strategy": "reject" + } + + +Perfect! The run succeeded as we would expect. We can double check that the run worked as expected by printing out the final state: + +=== "Python" + + ```python + final_result = await client.threads.get_state(thread["thread_id"]) + print(final_result) + ``` + +=== "Javascript" + + ```js + let finalResult = await client.threads.getState(thread["thread_id"]); + console.log(finalResult); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//state + ``` + +Output: + + { + "values": { + "messages": [ + { + "content": "what's the weather in sf", + "additional_kwargs": {}, + "response_metadata": {}, + "type": "human", + "name": null, + "id": "beba31bf-320d-4125-9c37-cadf526ac47a", + "example": false + }, + { + "content": [ + { + "id": "toolu_01AaNPSPzqia21v7aAKwbKYm", + "input": {}, + "name": "tavily_search_results_json", + "type": "tool_use", + "index": 0, + "partial_json": "{\"query\": \"weather in san francisco\"}" + } + ], + "additional_kwargs": {}, + "response_metadata": { + "stop_reason": "tool_use", + "stop_sequence": null + }, + "type": "ai", + "name": null, + "id": "run-f220faf8-1d27-4f73-ad91-6bb3f47e8639", + "example": false, + "tool_calls": [ + { + "name": "tavily_search_results_json", + "args": { + "query": "weather in san francisco" + }, + "id": "toolu_01AaNPSPzqia21v7aAKwbKYm", + "type": "tool_call" + } + ], + "invalid_tool_calls": [], + "usage_metadata": { + "input_tokens": 273, + "output_tokens": 61, + "total_tokens": 334 + } + }, + { + "content": "[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1725052131, 'localtime': '2024-08-30 14:08'}, 'current': {'last_updated_epoch': 1725051600, 'last_updated': '2024-08-30 14:00', 'temp_c': 21.1, 'temp_f': 70.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 11.9, 'wind_kph': 19.1, 'wind_degree': 290, 'wind_dir': 'WNW', 'pressure_mb': 1018.0, 'pressure_in': 30.07, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 59, 'cloud': 25, 'feelslike_c': 21.1, 'feelslike_f': 70.0, 'windchill_c': 18.6, 'windchill_f': 65.5, 'heatindex_c': 18.6, 'heatindex_f': 65.5, 'dewpoint_c': 12.2, 'dewpoint_f': 54.0, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 15.0, 'gust_kph': 24.2}}\"}]", + "additional_kwargs": {}, + "response_metadata": {}, + "type": "tool", + "name": "tavily_search_results_json", + "id": "686b2487-f332-4e58-9508-89b3a814cd81", + "tool_call_id": "toolu_01AaNPSPzqia21v7aAKwbKYm", + "artifact": { + "query": "weather in san francisco", + "follow_up_questions": null, + "answer": null, + "images": [], + "results": [ + { + "title": "Weather in San Francisco", + "url": "https://www.weatherapi.com/", + "content": "{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.78, 'lon': -122.42, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1725052131, 'localtime': '2024-08-30 14:08'}, 'current': {'last_updated_epoch': 1725051600, 'last_updated': '2024-08-30 14:00', 'temp_c': 21.1, 'temp_f': 70.0, 'is_day': 1, 'condition': {'text': 'Partly cloudy', 'icon': '//cdn.weatherapi.com/weather/64x64/day/116.png', 'code': 1003}, 'wind_mph': 11.9, 'wind_kph': 19.1, 'wind_degree': 290, 'wind_dir': 'WNW', 'pressure_mb': 1018.0, 'pressure_in': 30.07, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 59, 'cloud': 25, 'feelslike_c': 21.1, 'feelslike_f': 70.0, 'windchill_c': 18.6, 'windchill_f': 65.5, 'heatindex_c': 18.6, 'heatindex_f': 65.5, 'dewpoint_c': 12.2, 'dewpoint_f': 54.0, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 5.0, 'gust_mph': 15.0, 'gust_kph': 24.2}}", + "score": 0.976148, + "raw_content": null + } + ], + "response_time": 3.07 + }, + "status": "success" + }, + { + "content": [ + { + "text": "\n\nThe search results provide the current weather conditions in San Francisco. According to the data, as of 2:00 PM on August 30, 2024, the temperature in San Francisco is 70\u00b0F (21.1\u00b0C) with partly cloudy skies. The wind is blowing from the west-northwest at around 12 mph (19 km/h). The humidity is 59% and visibility is 9 miles (16 km). Overall, it looks like a nice late summer day in San Francisco with comfortable temperatures and partly sunny conditions.", + "type": "text", + "index": 0 + } + ], + "additional_kwargs": {}, + "response_metadata": { + "stop_reason": "end_turn", + "stop_sequence": null + }, + "type": "ai", + "name": null, + "id": "run-8fecc61d-3d9f-4e16-8e8a-92f702be498a", + "example": false, + "tool_calls": [], + "invalid_tool_calls": [], + "usage_metadata": { + "input_tokens": 837, + "output_tokens": 124, + "total_tokens": 961 + } + } + ] + }, + "next": [], + "tasks": [], + "metadata": { + "step": 3, + "run_id": "1ef67140-eb23-684b-8253-91d4c90bb05e", + "source": "loop", + "writes": { + "agent": { + "messages": [ + { + "id": "run-8fecc61d-3d9f-4e16-8e8a-92f702be498a", + "name": null, + "type": "ai", + "content": [ + { + "text": "\n\nThe search results provide the current weather conditions in San Francisco. According to the data, as of 2:00 PM on August 30, 2024, the temperature in San Francisco is 70\u00b0F (21.1\u00b0C) with partly cloudy skies. The wind is blowing from the west-northwest at around 12 mph (19 km/h). The humidity is 59% and visibility is 9 miles (16 km). Overall, it looks like a nice late summer day in San Francisco with comfortable temperatures and partly sunny conditions.", + "type": "text", + "index": 0 + } + ], + "example": false, + "tool_calls": [], + "usage_metadata": { + "input_tokens": 837, + "total_tokens": 961, + "output_tokens": 124 + }, + "additional_kwargs": {}, + "response_metadata": { + "stop_reason": "end_turn", + "stop_sequence": null + }, + "invalid_tool_calls": [] + } + ] + } + }, + "user_id": "", + "graph_id": "agent", + "thread_id": "5cb1e8a1-34b3-4a61-a34e-71a9799bd00d", + "created_by": "system", + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca" + }, + "created_at": "2024-08-30T21:09:00.079909+00:00", + "checkpoint_id": "1ef67141-3ca2-6fae-8003-fe96832e57d6", + "parent_checkpoint_id": "1ef67141-2129-6b37-8002-61fc3bf69cb5" + } + +We can also just print the content of the last AIMessage: + +=== "Python" + + ```python + print(final_result['values']['messages'][-1]['content'][0]['text']) + ``` + +=== "Javascript" + + ```js + console.log(finalResult['values']['messages'][finalResult['values']['messages'].length-1]['content'][0]['text']); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//state | jq -r '.values.messages[-1].content.[0].text' + ``` + +Output: + + The search results provide the current weather conditions in San Francisco. According to the data, as of 2:00 PM on August 30, 2024, the temperature in San Francisco is 70°F (21.1°C) with partly cloudy skies. The wind is blowing from the west-northwest at around 12 mph (19 km/h). The humidity is 59% and visibility is 9 miles (16 km). Overall, it looks like a nice late summer day in San Francisco with comfortable temperatures and partly sunny conditions. \ No newline at end of file diff --git a/docs/docs/cloud/how-tos/check_thread_status.md b/docs/docs/cloud/how-tos/check_thread_status.md index 655b918c1..5fb26c403 100644 --- a/docs/docs/cloud/how-tos/check_thread_status.md +++ b/docs/docs/cloud/how-tos/check_thread_status.md @@ -13,6 +13,7 @@ First, we need to setup our client so that we can communicate with our hosted gr ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -23,7 +24,8 @@ First, we need to setup our client so that we can communicate with our hosted gr import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); - const assistantId = agent; + // Using the graph deployed with the name "agent" + const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -48,7 +50,7 @@ We can use the following commands to find threads that are idle, which means tha === "Javascript" ```js - console.log(await client.threads.search({status: "idle",limit:1})); + console.log(await client.threads.search({ status: "idle", limit: 1 })); ``` === "CURL" @@ -83,7 +85,7 @@ We can use the following commands to find threads that have been interrupted in === "Javascript" ```js - console.log(await client.threads.search({status: "interrupted",limit:1})); + console.log(await client.threads.search({ status: "interrupted", limit: 1 })); ``` === "CURL" @@ -117,7 +119,7 @@ We can use the following commands to find threads that are busy, meaning they ar === "Javascript" ```js - console.log(await client.threads.search({status: "busy",limit: 1})); + console.log(await client.threads.search({ status: "busy", limit: 1 })); ``` === "CURL" @@ -183,7 +185,7 @@ The search endpoint for threads also allows you to filter on metadata, which can === "Javascript" ```js - console.log((await client.threads.search({metadata: {"foo":"bar"},limit: 1}))[0].status); + console.log((await client.threads.search({ metadata: { "foo": "bar" }, limit: 1 }))[0].status); ``` === "CURL" diff --git a/docs/docs/cloud/how-tos/configuration_cloud.md b/docs/docs/cloud/how-tos/configuration_cloud.md new file mode 100644 index 000000000..becdf802d --- /dev/null +++ b/docs/docs/cloud/how-tos/configuration_cloud.md @@ -0,0 +1,264 @@ +# How to create agents with configuration + +One of the benefits of LangGraph API is that it lets you create agents with different configurations. +This is useful when you want to: + +- Define a cognitive architecture once as a LangGraph +- Let that LangGraph be configurable across some attributes (for example, system message or LLM to use) +- Let users create agents with arbitrary configurations, save them, and then use them in the future + +In this guide we will show how to do that for the default agent we have built in. + +If you look at the agent we defined, you can see that inside the `call_model` node we have created the model based on some configuration. That node looks like: + +=== "Python" + + ```python + def call_model(state, config): + messages = state["messages"] + model_name = config.get('configurable', {}).get("model_name", "anthropic") + model = _get_model(model_name) + response = model.invoke(messages) + # We return a list, because this will get added to the existing list + return {"messages": [response]} + ``` + +=== "Javascript" + + ```js + function callModel(state: State, config: RunnableConfig) { + const messages = state.messages; + const modelName = config.configurable?.model_name ?? "anthropic"; + const model = _getModel(modelName); + const response = model.invoke(messages); + // We return a list, because this will get added to the existing list + return { messages: [response] }; + } + ``` + +We are looking inside the config for a `model_name` parameter (which defaults to `anthropic` if none is found). That means that by default we are using Anthropic as our model provider. In this example we will see an example of how to create an example agent that is configured to use OpenAI. + +First let's set up our client and thread: + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + # Select an assistant that is not configured + assistants = await client.assistants.search() + assistant = [a for a in assistants if not a["config"]][0] + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // Select an assistant that is not configured + const assistants = await client.assistants.search(); + const assistant = assistants.find(a => !a.config); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0]' + ``` + +We can now call `.get_schemas` to get schemas associated with this graph: + +=== "Python" + + ```python + schemas = await client.assistants.get_schemas( + assistant_id=assistant["assistant_id"] + ) + # There are multiple types of schemas + # We can get the `config_schema` to look at the the configurable parameters + print(schemas["config_schema"]) + ``` + +=== "Javascript" + + ```js + const schemas = await client.assistants.getSchemas( + assistant["assistant_id"] + ); + // There are multiple types of schemas + // We can get the `config_schema` to look at the the configurable parameters + console.log(schemas.config_schema); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /assistants//schemas | jq -r '.config_schema' + ``` + +Output: + + { + 'model_name': + { + 'title': 'Model Name', + 'enum': ['anthropic', 'openai'], + 'type': 'string' + } + } + +Now we can initialize an assistant with config: + +=== "Python" + + ```python + openai_assistant = await client.assistants.create( + # "agent" is the name of a graph we deployed + "agent", config={"configurable": {"model_name": "openai"}} + ) + + print(openai_assistant) + ``` + +=== "Javascript" + + ```js + let openAIAssistant = await client.assistants.create( + // "agent" is the name of a graph we deployed + "agent", { "configurable": { "model_name": "openai" } } + ); + + console.log(openAIAssistant); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants \ + --header 'Content-Type: application/json' \ + --data '{"graph_id":"agent","config":{"configurable":{"model_name":"open_ai"}}}' + ``` + +Output: + + { + "assistant_id": "62e209ca-9154-432a-b9e9-2d75c7a9219b", + "graph_id": "agent", + "created_at": "2024-08-31T03:09:10.230718+00:00", + "updated_at": "2024-08-31T03:09:10.230718+00:00", + "config": { + "configurable": { + "model_name": "open_ai" + } + }, + "metadata": {} + } + +We can verify the config is indeed taking effect: + +=== "Python" + + ```python + thread = await client.threads.create() + input = {"messages": [{"role": "user", "content": "who made you?"}]} + async for event in client.runs.stream( + thread["thread_id"], + openai_assistant["assistant_id"], + input=input, + stream_mode="updates", + ): + print(f"Receiving event of type: {event.event}") + print(event.data) + print("\n\n") + ``` + +=== "Javascript" + + ```js + const thread = await client.threads.create(); + let input = { "messages": [{ "role": "user", "content": "who made you?" }] }; + + const streamResponse = client.runs.stream( + thread["thread_id"], + openAIAssistant["assistant_id"], + { + input, + streamMode: "updates" + } + ); + + for await (const event of streamResponse) { + console.log(`Receiving event of type: ${event.event}`); + console.log(event.data); + console.log("\n\n"); + } + ``` + +=== "CURL" + + ```bash + thread_id=$(curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' | jq -r '.thread_id') && \ + curl --request POST \ + --url "/threads/${thread_id}/runs/stream" \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + "input": { + "messages": [ + { + "role": "human", + "content": "who made you?" + } + ] + }, + "stream_mode": [ + "updates" + ] + }' | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n\n" + } + } + ' + ``` + +Output: + + Receiving event of type: metadata + {'run_id': '1ef6746e-5893-67b1-978a-0f1cd4060e16'} + + + + Receiving event of type: updates + {'agent': {'messages': [{'content': 'I was created by OpenAI, a research organization focused on developing and advancing artificial intelligence technology.', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-e1a6b25c-8416-41f2-9981-f9cfe043f414', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}} + + + diff --git a/docs/docs/cloud/how-tos/copy_threads.md b/docs/docs/cloud/how-tos/copy_threads.md index 9a27282b2..22586ed4d 100644 --- a/docs/docs/cloud/how-tos/copy_threads.md +++ b/docs/docs/cloud/how-tos/copy_threads.md @@ -24,8 +24,8 @@ First, we need to setup our client so that we can communicate with our hosted gr ```js import { Client } from "@langchain/langgraph-sdk"; - const client = new Client({ apiUrl:"" }); - const assistantId = agent; + const client = new Client({ apiUrl: "" }); + const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -92,21 +92,21 @@ We can verify that the history from the prior thread did indeed copy over correc ```js function removeThreadId(d) { - if (d.metadata && d.metadata.thread_id) { - delete d.metadata.thread_id; - } - return d; + if (d.metadata && d.metadata.thread_id) { + delete d.metadata.thread_id; + } + return d; } // Assuming `client.threads.getHistory(threadId)` is an async function that returns a list of dicts async function compareThreadHistories(threadId, copiedThreadId) { - const originalThreadHistory = (await client.threads.getHistory(threadId)).map(removeThreadId); - const copiedThreadHistory = (await client.threads.getHistory(copiedThreadId)).map(removeThreadId); + const originalThreadHistory = (await client.threads.getHistory(threadId)).map(removeThreadId); + const copiedThreadHistory = (await client.threads.getHistory(copiedThreadId)).map(removeThreadId); - // Compare the two histories - console.assert(JSON.stringify(originalThreadHistory) === JSON.stringify(copiedThreadHistory)) - // if we made it here the assertion passed! - console.log("The histories are the same."); + // Compare the two histories + console.assert(JSON.stringify(originalThreadHistory) === JSON.stringify(copiedThreadHistory)); + // if we made it here the assertion passed! + console.log("The histories are the same."); } // Example usage diff --git a/docs/docs/cloud/how-tos/cron_jobs.md b/docs/docs/cloud/how-tos/cron_jobs.md new file mode 100644 index 000000000..f787c8b9c --- /dev/null +++ b/docs/docs/cloud/how-tos/cron_jobs.md @@ -0,0 +1,184 @@ +# Cron Jobs + +Sometimes you don't want to run your graph based on user interaction, but rather you would like to schedule your graph to run on a schedule - for example if you wish for your graph to compose and send out a weekly email of to-dos for your team. LangGraph Cloud allows you to do this without having to write your own script by using the `Crons` client. To schedule a graph job, you need to pass a [cron expression](https://crontab.cronhub.io/) to inform the client when you want to run the graph. `Cron` jobs are run in the background and do not interfere with normal invocations of the graph. + +## Setup + +First, let's setup our SDK client, assistant, and thread: + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" + # create thread + thread = await client.threads.create() + print(thread) + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantId = "agent"; + // create thread + const thread = await client.threads.create(); + console.log(thread); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0].graph_id' && \ + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + +Output: + + { + 'thread_id': '9dde5490-2b67-47c8-aa14-4bfec88af217', + 'created_at': '2024-08-30T23:07:38.242730+00:00', + 'updated_at': '2024-08-30T23:07:38.242730+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } + +## Cron job on a thread + +To create a cron job associated with a specific thread, you can write: + + +=== "Python" + + ```python + # This schedules a job to run at 15:27 (3:27PM) every day + cron_job = await client.crons.create_for_thread( + thread["thread_id"], + assistant_id, + schedule="27 15 * * *", + input={"messages": [{"role": "user", "content": "What time is it?"}]}, + ) + ``` + +=== "Javascript" + + ```js + // This schedules a job to run at 15:27 (3:27PM) every day + const cronJob = await client.crons.create_for_thread( + thread["thread_id"], + assistantId, + { + schedule: "27 15 * * *", + input: { messages: [{ role: "user", content: "What time is it?" }] } + } + ); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/crons \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + }' + ``` + +Note that it is **very** important to delete `Cron` jobs that are no longer useful. Otherwise you could rack up unwanted API charges to the LLM! You can delete a `Cron` job using the following code: + +=== "Python" + + ```python + await client.crons.delete(cron_job["cron_id"]) + ``` + +=== "Javascript" + + ```js + await client.crons.delete(cronJob["cron_id"]); + ``` + +=== "CURL" + + ```bash + curl --request DELETE \ + --url /runs/crons/ + ``` + +## Cron job stateless + +You can also create stateless cron jobs by using the following code: + +=== "Python" + + ```python + # This schedules a job to run at 15:27 (3:27PM) every day + cron_job_stateless = await client.crons.create( + assistant_id, + schedule="27 15 * * *", + input={"messages": [{"role": "user", "content": "What time is it?"}]}, + ) + ``` + +=== "Javascript" + + ```js + // This schedules a job to run at 15:27 (3:27PM) every day + const cronJobStateless = await client.crons.create( + assistantId, + { + schedule: "27 15 * * *", + input: { messages: [{ role: "user", content: "What time is it?" }] } + } + ); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /runs/crons \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + }' + ``` + +Again, remember to delete your job once you are done with it! + +=== "Python" + + ```python + await client.crons.delete(cron_job_stateless["cron_id"]) + ``` + +=== "Javascript" + + ```js + await client.crons.delete(cronJobStateless["cron_id"]); + ``` + +=== "CURL" + + ```bash + curl --request DELETE \ + --url /runs/crons/ + ``` diff --git a/docs/docs/cloud/how-tos/enqueue_concurrent.md b/docs/docs/cloud/how-tos/enqueue_concurrent.md index 17c4ea0f5..5172b8ac6 100644 --- a/docs/docs/cloud/how-tos/enqueue_concurrent.md +++ b/docs/docs/cloud/how-tos/enqueue_concurrent.md @@ -5,20 +5,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou The guide covers the `enqueue` option for double texting, which adds the interruptions to a queue and executes them in the order they are received by the client. Below is a quick example of using the `enqueue` option. -First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python): +First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python): -```js -function prettyPrint(m) { - const padded = " " + m['type'] + " "; - const sepLen = Math.floor((80 - padded.length) / 2); - const sep = "=".repeat(sepLen); - const secondSep = sep + (padded.length % 2 ? "=" : ""); - - console.log(`${sep}${padded}${secondSep}`); - console.log("\n\n"); - console.log(m.content); -} -``` +=== "Javascript" + + ```js + function prettyPrint(m) { + const padded = " " + m['type'] + " "; + const sepLen = Math.floor((80 - padded.length) / 2); + const sep = "=".repeat(sepLen); + const secondSep = sep + (padded.length % 2 ? "=" : ""); + + console.log(`${sep}${padded}${secondSep}`); + console.log("\n\n"); + console.log(m.content); + } + ``` + +=== "CURL" + + ```bash + # PLACE THIS IN A FILE CALLED pretty_print.sh + pretty_print() { + local type="$1" + local content="$2" + local padded=" $type " + local total_width=80 + local sep_len=$(( (total_width - ${#padded}) / 2 )) + local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}")) + local second_sep=$sep + if (( (total_width - ${#padded}) % 2 )); then + second_sep="${second_sep}=" + fi + + echo "${sep}${padded}${second_sep}" + echo + echo "$content" + } + ``` Then, let's import our required packages and instantiate our client, assistant, and thread. @@ -32,6 +56,7 @@ Then, let's import our required packages and instantiate our client, assistant, from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -43,9 +68,18 @@ Then, let's import our required packages and instantiate our client, assistant, const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' + ``` Now let's start two runs, with the second interrupting the first one with a multitask strategy of "enqueue": @@ -82,6 +116,25 @@ Now let's start two runs, with the second interrupting the first one with a mult ) ``` +=== "CURL" + + ```bash + curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]}, + }" && curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]}, + \"multitask_strategy\": \"enqueue\" + }" + ``` + Verify that the thread has data from both runs: === "Python" @@ -108,12 +161,25 @@ Verify that the thread has data from both runs: } ``` +=== "CURL" + + ```bash + source pretty_print.sh && curl --request GET \ + --url /threads//runs//join && \ + curl --request GET --url /threads//state | \ + jq -c '.values.messages[]' | while read -r element; do + type=$(echo "$element" | jq -r '.type') + content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end') + pretty_print "$type" "$content" + done + ``` + Output: - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in sf? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'id': 'toolu_01Dez1sJre4oA2Y7NsKJV6VT', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -121,11 +187,11 @@ Output: Call ID: toolu_01Dez1sJre4oA2Y7NsKJV6VT Args: query: weather in san francisco - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.accuweather.com/en/us/san-francisco/94103/weather-forecast/347629", "content": "Get the current and future weather conditions for San Francisco, CA, including temperature, precipitation, wind, air quality and more. See the hourly and 10-day outlook, radar maps, alerts and allergy information."}] - ================================== Ai Message ================================== + ================================== Ai Message ================================== According to AccuWeather, the current weather conditions in San Francisco are: @@ -145,10 +211,10 @@ Output: Sunday: Partly sunny, high of 61°F (16°C) So in summary, expect seasonable spring weather in San Francisco over the next several days, with a mix of sun and clouds and temperatures ranging from the upper 40s at night to the low 60s during the days. Typical dry conditions with no rain in the forecast. - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in nyc? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'text': 'Here are the current weather conditions and forecast for New York City:', 'type': 'text'}, {'id': 'toolu_01FFft5Sx9oS6AdVJuRWWcGp', 'input': {'query': 'weather in new york city'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -156,11 +222,11 @@ Output: Call ID: toolu_01FFft5Sx9oS6AdVJuRWWcGp Args: query: weather in new york city - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.weatherapi.com/", "content": "{'location': {'name': 'New York', 'region': 'New York', 'country': 'United States of America', 'lat': 40.71, 'lon': -74.01, 'tz_id': 'America/New_York', 'localtime_epoch': 1718734479, 'localtime': '2024-06-18 14:14'}, 'current': {'last_updated_epoch': 1718733600, 'last_updated': '2024-06-18 14:00', 'temp_c': 29.4, 'temp_f': 84.9, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 2.2, 'wind_kph': 3.6, 'wind_degree': 158, 'wind_dir': 'SSE', 'pressure_mb': 1025.0, 'pressure_in': 30.26, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 63, 'cloud': 0, 'feelslike_c': 31.3, 'feelslike_f': 88.3, 'windchill_c': 28.3, 'windchill_f': 82.9, 'heatindex_c': 29.6, 'heatindex_f': 85.3, 'dewpoint_c': 18.4, 'dewpoint_f': 65.2, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 7.0, 'gust_mph': 16.5, 'gust_kph': 26.5}}"}] - ================================== Ai Message ================================== + ================================== Ai Message ================================== According to the weather data from WeatherAPI: diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md b/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md index 56c1931e2..08eaa0f3d 100644 --- a/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md +++ b/docs/docs/cloud/how-tos/human_in_the_loop_breakpoint.md @@ -22,6 +22,7 @@ In this how-to we use a simple ReAct style hosted graph (you can see the full co ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -32,7 +33,8 @@ In this how-to we use a simple ReAct style hosted graph (you can see the full co import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); - const assistantId = "agent" + // Using the graph deployed with the name "agent" + const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -73,7 +75,7 @@ And, now let's compile it with a breakpoint before the tool node: === "Javascript" ```js - const input = { "messages": [{ "role": "human", "content": "what's the weather in sf"}] } + const input = { messages: [{ role: "human", content: "what's the weather in sf" }] }; const streamResponse = client.runs.stream( thread["thread_id"], @@ -81,9 +83,10 @@ And, now let's compile it with a breakpoint before the tool node: { input: input, streamMode: "updates", - interruptBefore: ["action"], + interruptBefore: ["action"] } ); + for await (const chunk of streamResponse) { console.log(`Receiving new event of type: ${chunk.event}...`); console.log(chunk.data); diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_edit_state.md b/docs/docs/cloud/how-tos/human_in_the_loop_edit_state.md index 668d826a8..927df7164 100644 --- a/docs/docs/cloud/how-tos/human_in_the_loop_edit_state.md +++ b/docs/docs/cloud/how-tos/human_in_the_loop_edit_state.md @@ -18,6 +18,7 @@ First, we need to setup our client so that we can communicate with our hosted gr ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -28,6 +29,7 @@ First, we need to setup our client so that we can communicate with our hosted gr import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -65,7 +67,7 @@ Now let's invoke our graph, making sure to interrupt before the `action` node. === "Javascript" ```js - const input = {"messages": [{ "role": "human", "content": "search for weather in SF"}] } + const input = { messages: [{ role: "human", content: "search for weather in SF" }] }; const streamResponse = client.runs.stream( thread["thread_id"], @@ -76,6 +78,7 @@ Now let's invoke our graph, making sure to interrupt before the `action` node. interruptBefore: ["action"], } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -154,15 +157,15 @@ Now, let's assume we actually meant to search for the weather in Sidi Frej (anot === "Javascript" ```js - // First, lets get the current state - const currentState = await client.threads.getState(thread['thread_id']); + // First, let's get the current state + const currentState = await client.threads.getState(thread["thread_id"]); // Let's now get the last message in the state // This is the one with the tool calls that we want to update - let lastMessage = currentState['values']['messages'][-1]; + let lastMessage = currentState.values.messages.slice(-1)[0]; // Let's now update the args for that tool call - lastMessage['tool_calls'][0]['args'] = {'query': 'current weather in Sidi Frej'}; + lastMessage.tool_calls[0].args = { query: "current weather in Sidi Frej" }; // Let's now call `update_state` to pass in this message in the `messages` key // This will get treated as any other update to the state @@ -170,7 +173,7 @@ Now, let's assume we actually meant to search for the weather in Sidi Frej (anot // That reducer function will use the ID of the message to update it // It's important that it has the right ID! Otherwise it would get appended // as a new message - await client.threads.updateState(thread['thread_id'], {values:{"messages": lastMessage}}); + await client.threads.updateState(thread["thread_id"], { values: { messages: lastMessage } }); ``` === "CURL" @@ -220,6 +223,7 @@ Now we can resume our graph run but with the updated state: streamMode: "updates", } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md b/docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md index e9a698181..69f247807 100644 --- a/docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md +++ b/docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md @@ -29,6 +29,7 @@ First, we need to setup our client so that we can communicate with our hosted gr ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -39,10 +40,19 @@ First, we need to setup our client so that we can communicate with our hosted gr import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' + ``` + ## Example with no review Let's look at an example when no review is required (because no tools are called) @@ -66,7 +76,7 @@ Let's look at an example when no review is required (because no tools are called === "Javascript" ```js - const input = {"messages": [{ "role": "human", "content": "hi!"}] } + const input = { "messages": [{ "role": "human", "content": "hi!" }] }; const streamResponse = client.runs.stream( thread["thread_id"], @@ -77,6 +87,7 @@ Let's look at an example when no review is required (because no tools are called interruptBefore: ["action"], } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -84,6 +95,42 @@ Let's look at an example when no review is required (because no tools are called } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"hi!\"}]}, + \"stream_mode\": [ + \"updates\" + ], + \"interrupt_before\": [\"action\"] + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': 'hi!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '39c51f14-2d5c-4690-883a-d940854b1845', 'example': False}]} @@ -108,6 +155,13 @@ If we check the state, we can see that it is finished console.log(state.next); ``` +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//state | jq -c '.next' + ``` + Output: [] @@ -125,7 +179,6 @@ Let's now look at what it looks like to approve a tool call. Note that we don't thread["thread_id"], "agent", input=input, - stream_mode="values", ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -134,16 +187,16 @@ Let's now look at what it looks like to approve a tool call. Note that we don't === "Javascript" ```js - const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]} + const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] }; const streamResponse = client.runs.stream( thread["thread_id"], assistantId, { input: input, - streamMode: "values", } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -151,6 +204,38 @@ Let's now look at what it looks like to approve a tool call. Note that we don't } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]} + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}]} @@ -175,6 +260,13 @@ If we now check, we can see that it is waiting on human review: console.log(state.next); ``` +=== "CURL" + + ```bash + curl --request GET \ + --url /threads//state | jq -c '.next' + ``` + Output: ['human_review_node'] @@ -201,10 +293,11 @@ To approve the tool call, we can just continue the thread with no edits. To do t thread["thread_id"], assistantId, { - input: undefined, + input: null, streamMode: "values", } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -212,6 +305,37 @@ To approve the tool call, we can just continue the thread with no edits. To do t } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\" + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '54e19d6e-89fa-44fb-b92c-12e7dd4ddf08', 'example': False}, {'content': [{'text': "Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search function. Let me do that for you right away.", 'type': 'text', 'index': 0}, {'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-45a6b6c3-ac69-42a4-8957-d982203d6392', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 90, 'total_tokens': 450}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '826cd0f2-9cc6-46f0-b7df-daa6a05d13d2', 'tool_call_id': 'toolu_015yrR3GMDXe6X8m2p9CsEDN', 'artifact': None, 'status': 'success'}]} @@ -239,7 +363,7 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters === "Javascript" ```js - const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]} + const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] }; const streamResponse = client.runs.stream( thread["thread_id"], @@ -249,6 +373,7 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters streamMode: "values", } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -256,6 +381,38 @@ Let's now say we want to edit the tool call. E.g. change some of the parameters } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]} + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'cec11391-84da-464b-bd2a-bd4f0d93b9ee', 'example': False}]} @@ -310,7 +467,6 @@ To do this, we first need to update the state. We can do this by passing a messa thread["thread_id"], "agent", input=None, - stream_mode="values", ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -332,43 +488,93 @@ To do this, we first need to update the state. We can do this by passing a messa // Construct a replacement tool call const newMessage = { - role: "assistant", - content: currentContent, - tool_calls: [ - { - id: toolCallId, - name: "weather_search", - args: { city: "San Francisco, USA" } - } - ], - // Ensure the ID is the same as the message you're replacing - id: currentId + role: "assistant", + content: currentContent, + tool_calls: [ + { + id: toolCallId, + name: "weather_search", + args: { city: "San Francisco, USA" } + } + ], + // Ensure the ID is the same as the message you're replacing + id: currentId }; await client.threads.updateState( - thread.thread_id, // Thread ID - { + thread.thread_id, // Thread ID + { values: { "messages": [newMessage] }, // Updated message asNode: "human_review_node" - } // Acting as human_review_node + } // Acting as human_review_node ); console.log("\nResuming Execution"); // Continue executing from here const streamResponseResumed = client.runs.stream( - thread["thread_id"], - assistantId, - { - input: undefined, - streamMode: "values", - interruptBefore: ["action"], - } + thread["thread_id"], + assistantId, + { + input: null, + } ); + for await (const chunk of streamResponseResumed) { - if (chunk.data && chunk.event !== "metadata") { + if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); + } } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//state \ + --header 'Content-Type: application/json' \ + --data "{ + \"values\": { \"messages\": [$(curl --request GET \ + --url /threads//state | + jq -c '{ + role: "assistant", + content: .values.messages[-1].content, + tool_calls: [ + { + id: .values.messages[-1].tool_calls[0].id, + name: "weather_search", + args: { city: "San Francisco, USA" } + } + ], + id: .values.messages[-1].id + }') + ]}, + \"as_node\": \"human_review_node\" + }" && echo "Resuming Execution" && curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": "agent" + }' | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' ``` Output: @@ -404,7 +610,6 @@ For this example we will just add a single tool call representing the feedback. thread["thread_id"], "agent", input=input, - stream_mode="values", ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -413,16 +618,16 @@ For this example we will just add a single tool call representing the feedback. === "Javascript" ```js - const input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]} + const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] }; const streamResponse = client.runs.stream( thread["thread_id"], assistantId, { input: input, - streamMode: "values", } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -430,6 +635,38 @@ For this example we will just add a single tool call representing the feedback. } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]} + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'c80f13d0-674d-4233-b6a0-3940509d3cf3', 'example': False}]} @@ -493,38 +730,85 @@ To do this, we first need to update the state. We can do this by passing a messa // Construct a replacement tool call const newMessage = { - role: "tool", - content: "User requested changes: pass in the country as well", - name: "weather_search", - tool_call_id: toolCallId, + role: "tool", + content: "User requested changes: pass in the country as well", + name: "weather_search", + tool_call_id: toolCallId, }; await client.threads.updateState( - thread.thread_id, // Thread ID - { + thread.thread_id, // Thread ID + { values: { "messages": [newMessage] }, // Updated message asNode: "human_review_node" - } // Acting as human_review_node + } // Acting as human_review_node ); console.log("\nResuming Execution"); // Continue executing from here const streamResponseEdited = client.runs.stream( - thread["thread_id"], - assistantId, - { - input: undefined, + thread["thread_id"], + assistantId, + { + input: null, streamMode: "values", interruptBefore: ["action"], - } + } ); + for await (const chunk of streamResponseEdited) { - if (chunk.data && chunk.event !== "metadata") { + if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); - } + } } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//state \ + --header 'Content-Type: application/json' \ + --data "{ + \"values\": { \"messages\": [$(curl --request GET \ + --url /threads//state | + jq -c '{ + role: "tool", + content: "User requested changes: pass in the country as well", + name: "get_weather", + tool_call_id: .values.messages[-1].id.tool_calls[0].id + }') + ]}, + \"as_node\": \"human_review_node\" + }" && echo "Resuming Execution" && curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": "agent" + }' | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + + Output: Current State: @@ -545,7 +829,6 @@ We can see that we now get to another breakpoint - because it went back to the m thread["thread_id"], "agent", input=None, - stream_mode="values", ): if chunk.data and chunk.event != "metadata": print(chunk.data) @@ -558,10 +841,10 @@ We can see that we now get to another breakpoint - because it went back to the m thread["thread_id"], assistantId, { - input: undefined, - streamMode: "values", + input: null, } ); + for await (const chunk of streamResponseResumed) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -569,6 +852,37 @@ We can see that we now get to another breakpoint - because it went back to the m } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\" + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + sub(/^event: /, "", $0) + event_type = $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "" && event_type != "metadata") { + print data_content "\n" + } + } + ' + ``` + Output: {'messages': [{'content': "what's the weather in sf?", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '3b2bbc38-d11b-49eb-80c0-c24a40dab5a8', 'example': False}, {'content': [{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text', 'index': 0}, {'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-c5a50900-abf5-4885-9cdb-da2bf0d892ac', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}}, {'content': 'User requested changes: pass in the country as well', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '787288be-213c-4fd3-8503-4a009bdb1b00', 'tool_call_id': 'toolu_01NNw18j57GEGPZvsa9f1wvX', 'artifact': None, 'status': 'success'}, {'content': [{'text': '\n\nI apologize for the oversight. It seems the function requires additional information. Let me try again with a more specific request.', 'type': 'text', 'index': 0}, {'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'input': {}, 'name': 'weather_search', 'type': 'tool_use', 'index': 1, 'partial_json': '{"city": "San Francisco, USA"}'}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'tool_use', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-5c355a56-cfe3-4046-b49f-f5b09fc397ef', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 461, 'output_tokens': 83, 'total_tokens': 544}}, {'content': 'Sunny!', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'weather_search', 'id': '3b857482-bca2-4a73-a9ab-1f35a3e43e5f', 'tool_call_id': 'toolu_01YAbLBoKozJyRQnB8LUMpXC', 'artifact': None, 'status': 'success'}]} diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md b/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md index 046bb95a8..69b418784 100644 --- a/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md +++ b/docs/docs/cloud/how-tos/human_in_the_loop_time_travel.md @@ -15,6 +15,7 @@ First, we need to setup our client so that we can communicate with our hosted gr ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -25,7 +26,8 @@ First, we need to setup our client so that we can communicate with our hosted gr import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); - const assistantId = agent; + // Using the graph deployed with the name "agent" + const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -34,8 +36,7 @@ First, we need to setup our client so that we can communicate with our hosted gr ```bash curl --request POST \ --url /threads \ - --header 'Content-Type: application/json' \ - --data {} + --header 'Content-Type: application/json' ``` ## Replay a state @@ -51,7 +52,7 @@ Before replaying a state - we need to create states to replay from! In order to async for chunk in client.runs.stream( thread["thread_id"], - assistant_id, # graph_id + assistant_id, input=input, stream_mode="updates", ): @@ -308,7 +309,7 @@ Now we can rerun our graph with this new config, starting from the `new_state`, ```python async for chunk in client.runs.stream( thread["thread_id"], - assistant["assistant_id"], # graph_id + assistant_id, input=None, stream_mode="updates", checkpoint_id=config['checkpoint_id'] @@ -322,7 +323,7 @@ Now we can rerun our graph with this new config, starting from the `new_state`, ```js const streamResponse = client.runs.stream( thread["thread_id"], - assistant["assistant_id"], + assistantId, { input: null, streamMode: "updates", diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_user_input.md b/docs/docs/cloud/how-tos/human_in_the_loop_user_input.md index 9b9d1392e..435b49158 100644 --- a/docs/docs/cloud/how-tos/human_in_the_loop_user_input.md +++ b/docs/docs/cloud/how-tos/human_in_the_loop_user_input.md @@ -25,6 +25,7 @@ First, we need to setup our client so that we can communicate with our hosted gr ```python from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -35,6 +36,7 @@ First, we need to setup our client so that we can communicate with our hosted gr import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` @@ -56,7 +58,14 @@ Now, let's invoke our graph by interrupting before `ask_human` node: === "Python" ```python - input = { 'messages':[{ "role":"user", "content":"Use the search tool to ask the user where they are, then look up the weather there" }] } + input = { + "messages": [ + { + "role": "human", + "content": "Use the search tool to ask the user where they are, then look up the weather there", + } + ] + } async for chunk in client.runs.stream( thread["thread_id"], @@ -71,7 +80,14 @@ Now, let's invoke our graph by interrupting before `ask_human` node: === "Javascript" ```js - const input = { "messages":[{ "role":"human", "content": "Use the search tool to ask the user where they are, then look up the weather there"}] } + const input = { + messages: [ + { + role: "human", + content: "Use the search tool to ask the user where they are, then look up the weather there" + } + ] + }; const streamResponse = client.runs.stream( thread["thread_id"], @@ -79,9 +95,10 @@ Now, let's invoke our graph by interrupting before `ask_human` node: { input: input, streamMode: "updates", - interruptBefore: ["ask_human"], + interruptBefore: ["ask_human"] } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); @@ -152,13 +169,23 @@ Because we are treating this as a tool call, we will need to update the state as === "Javascript" ```js - const state = await client.threads.getState(thread['thread_id']); - const toolCallId = state['values']['messages'][-1]['tool_calls'][0]['id']; + const state = await client.threads.getState(thread["thread_id"]); + const toolCallId = state.values.messages[state.values.messages.length - 1].tool_calls[0].id; - # We now create the tool call with the id and the response we want - const toolMessage = [{"tool_call_id": toolCallId, "type": "tool", "content": "san francisco"}]; + // We now create the tool call with the id and the response we want + const toolMessage = [ + { + tool_call_id: toolCallId, + type: "tool", + content: "san francisco" + } + ]; - await client.threads.updateState(thread['thread_id'], {values: {"messages": toolMessage}, asNode:"ask_human"}) + await client.threads.updateState( + thread["thread_id"], + { values: { messages: toolMessage } }, + { asNode: "ask_human" } + ); ``` === "CURL" @@ -212,9 +239,10 @@ We can now tell the agent to continue. We can just pass in None as the input to assistantId, { input: null, - streamMode: "updates", + streamMode: "updates" } ); + for await (const chunk of streamResponse) { if (chunk.data && chunk.event !== "metadata") { console.log(chunk.data); diff --git a/docs/docs/cloud/how-tos/index.md b/docs/docs/cloud/how-tos/index.md index 90b13c35e..25954dd3b 100644 --- a/docs/docs/cloud/how-tos/index.md +++ b/docs/docs/cloud/how-tos/index.md @@ -62,17 +62,17 @@ LangGraph Studio is a built-in UI for visualizing, testing, and debugging your a LangGraph Cloud supports multiple types of runs besides streaming runs. -- [How to run an agent in the background](cloud_examples/background_run.ipynb) -- [How to run multiple agents in the same thread](cloud_examples/same-thread.ipynb) -- [How to create cron jobs](cloud_examples/cron_jobs.ipynb) -- [How to create stateless runs](cloud_examples/stateless_runs.ipynb) +- [How to run an agent in the background](./background_run.md) +- [How to run multiple agents in the same thread](./same-thread.md) +- [How to create cron jobs](./cron_jobs.md) +- [How to create stateless runs](./stateless_runs.md) ## Other Other guides that may prove helpful! -- [How to configure agents](cloud_examples/configuration_cloud.ipynb) +- [How to configure agents](./configuration_cloud.md) - [How to convert LangGraph calls to LangGraph cloud calls](cloud_examples/langgraph_to_langgraph_cloud.ipynb) -- [How to integrate webhooks](cloud_examples/webhooks.ipynb) +- [How to integrate webhooks](./webhooks.md) - [How to copy threads](./copy_threads.md) - [How to check status of your threads](./check_thread_status.md) diff --git a/docs/docs/cloud/how-tos/interrupt_concurrent.md b/docs/docs/cloud/how-tos/interrupt_concurrent.md index b09f908ef..5ac9934b5 100644 --- a/docs/docs/cloud/how-tos/interrupt_concurrent.md +++ b/docs/docs/cloud/how-tos/interrupt_concurrent.md @@ -4,20 +4,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou The guide covers the `interrupt` option for double texting, which interrupts the prior run of the graph and starts a new one with the double-text. This option does not delete the first run, but rather keeps it in the database but sets its status to `interrupted`. Below is a quick example of using the `interrupt` option. -First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python): +First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python): -```js -function prettyPrint(m) { - const padded = " " + m['type'] + " "; - const sepLen = Math.floor((80 - padded.length) / 2); - const sep = "=".repeat(sepLen); - const secondSep = sep + (padded.length % 2 ? "=" : ""); - - console.log(`${sep}${padded}${secondSep}`); - console.log("\n\n"); - console.log(m.content); -} -``` +=== "Javascript" + + ```js + function prettyPrint(m) { + const padded = " " + m['type'] + " "; + const sepLen = Math.floor((80 - padded.length) / 2); + const sep = "=".repeat(sepLen); + const secondSep = sep + (padded.length % 2 ? "=" : ""); + + console.log(`${sep}${padded}${secondSep}`); + console.log("\n\n"); + console.log(m.content); + } + ``` + +=== "CURL" + + ```bash + # PLACE THIS IN A FILE CALLED pretty_print.sh + pretty_print() { + local type="$1" + local content="$2" + local padded=" $type " + local total_width=80 + local sep_len=$(( (total_width - ${#padded}) / 2 )) + local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}")) + local second_sep=$sep + if (( (total_width - ${#padded}) % 2 )); then + second_sep="${second_sep}=" + fi + + echo "${sep}${padded}${second_sep}" + echo + echo "$content" + } + ``` Now, let's import our required packages and instantiate our client, assistant, and thread. @@ -30,6 +54,7 @@ Now, let's import our required packages and instantiate our client, assistant, a from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -40,11 +65,20 @@ Now, let's import our required packages and instantiate our client, assistant, a import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` -Now we can start our two runs and join the second one until it has completed: +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' + ``` + +Now we can start our two runs and join the second on euntil it has completed: === "Python" @@ -90,6 +124,26 @@ Now we can start our two runs and join the second one until it has completed: await client.runs.join(thread["thread_id"], run["run_id"]); ``` +=== "CURL" + + ```bash + curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]}, + }" && sleep 2 && curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]}, + \"multitask_strategy\": \"interrupt\" + }" && curl --request GET \ + --url /threads//runs//join + ``` + We can see that the thread has partial data from the first run + data from the second run @@ -112,12 +166,24 @@ We can see that the thread has partial data from the first run + data from the s } ``` +=== "CURL" + + ```bash + source pretty_print.sh && curl --request GET \ + --url /threads//state | \ + jq -c '.values.messages[]' | while read -r element; do + type=$(echo "$element" | jq -r '.type') + content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end') + pretty_print "$type" "$content" + done + ``` + Output: - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in sf? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'id': 'toolu_01MjNtVJwEcpujRGrf3x6Pih', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -125,14 +191,14 @@ Output: Call ID: toolu_01MjNtVJwEcpujRGrf3x6Pih Args: query: weather in san francisco - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.wunderground.com/hourly/us/ca/san-francisco/KCASANFR2002/date/2024-6-18", "content": "High 64F. Winds W at 10 to 20 mph. A few clouds from time to time. Low 49F. Winds W at 10 to 20 mph. Temp. San Francisco Weather Forecasts. Weather Underground provides local & long-range weather ..."}] - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in nyc? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'id': 'toolu_01KtE1m1ifPLQAx4fQLyZL9Q', 'input': {'query': 'weather in new york city'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -140,11 +206,11 @@ Output: Call ID: toolu_01KtE1m1ifPLQAx4fQLyZL9Q Args: query: weather in new york city - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.accuweather.com/en/us/new-york/10021/june-weather/349727", "content": "Get the monthly weather forecast for New York, NY, including daily high/low, historical averages, to help you plan ahead."}] - ================================== Ai Message ================================== + ================================== Ai Message ================================== The search results provide weather forecasts and information for New York City. Based on the top result from AccuWeather, here are some key details about the weather in NYC: diff --git a/docs/docs/cloud/how-tos/reject_concurrent.md b/docs/docs/cloud/how-tos/reject_concurrent.md index 22d0ae30c..d94bb27a6 100644 --- a/docs/docs/cloud/how-tos/reject_concurrent.md +++ b/docs/docs/cloud/how-tos/reject_concurrent.md @@ -4,20 +4,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou The guide covers the `reject` option for double texting, which rejects the new run of the graph by throwing an error and continues with the original run until completion. Below is a quick example of using the `reject` option. -First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python): +First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python): -```js -function prettyPrint(m) { - const padded = " " + m['type'] + " "; - const sepLen = Math.floor((80 - padded.length) / 2); - const sep = "=".repeat(sepLen); - const secondSep = sep + (padded.length % 2 ? "=" : ""); - - console.log(`${sep}${padded}${secondSep}`); - console.log("\n\n"); - console.log(m.content); -} -``` +=== "Javascript" + + ```js + function prettyPrint(m) { + const padded = " " + m['type'] + " "; + const sepLen = Math.floor((80 - padded.length) / 2); + const sep = "=".repeat(sepLen); + const secondSep = sep + (padded.length % 2 ? "=" : ""); + + console.log(`${sep}${padded}${secondSep}`); + console.log("\n\n"); + console.log(m.content); + } + ``` + +=== "CURL" + + ```bash + # PLACE THIS IN A FILE CALLED pretty_print.sh + pretty_print() { + local type="$1" + local content="$2" + local padded=" $type " + local total_width=80 + local sep_len=$(( (total_width - ${#padded}) / 2 )) + local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}")) + local second_sep=$sep + if (( (total_width - ${#padded}) % 2 )); then + second_sep="${second_sep}=" + fi + + echo "${sep}${padded}${second_sep}" + echo + echo "$content" + } + ``` Now, let's import our required packages and instantiate our client, assistant, and thread. @@ -29,6 +53,7 @@ Now, let's import our required packages and instantiate our client, assistant, a from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -39,10 +64,19 @@ Now, let's import our required packages and instantiate our client, assistant, a import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' + ``` + Now we can run a thread and try to run a second one with the "reject" option, which should fail since we have already started a run: @@ -90,6 +124,27 @@ Now we can run a thread and try to run a second one with the "reject" option, wh } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]}, + }" && curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]}, + \"multitask_strategy\": \"reject\" + }" || { echo "Failed to start concurrent run"; echo "Error: $?" >&2; } + ``` + +Output: + Failed to start concurrent run Client error '409 Conflict' for url 'http://localhost:8123/threads/f9e7088b-8028-4e5c-88d2-9cc9a2870e50/runs' For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409 @@ -120,12 +175,25 @@ We can verify that the original thread finished executing: } ``` +=== "CURL" + + ```bash + source pretty_print.sh && curl --request GET \ + --url /threads//runs//join && \ + curl --request GET --url /threads//state | \ + jq -c '.values.messages[]' | while read -r element; do + type=$(echo "$element" | jq -r '.type') + content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end') + pretty_print "$type" "$content" + done + ``` + Output: - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in sf? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'id': 'toolu_01CyewEifV2Kmi7EFKHbMDr1', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -133,11 +201,11 @@ Output: Call ID: toolu_01CyewEifV2Kmi7EFKHbMDr1 Args: query: weather in san francisco - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.accuweather.com/en/us/san-francisco/94103/june-weather/347629", "content": "Get the monthly weather forecast for San Francisco, CA, including daily high/low, historical averages, to help you plan ahead."}] - ================================== Ai Message ================================== + ================================== Ai Message ================================== According to the search results from Tavily, the current weather in San Francisco is: diff --git a/docs/docs/cloud/how-tos/rollback_concurrent.md b/docs/docs/cloud/how-tos/rollback_concurrent.md index 6fa34eb95..cabc584c0 100644 --- a/docs/docs/cloud/how-tos/rollback_concurrent.md +++ b/docs/docs/cloud/how-tos/rollback_concurrent.md @@ -4,20 +4,44 @@ This guide assumes knowledge of what double-texting is, which you can learn abou The guide covers the `rollback` option for double texting, which interrupts the prior run of the graph and starts a new one with the double-text. This option is very similar to the `interrupt` option, but in this case the first run is completely deleted from the database and cannot be restarted. Below is a quick example of using the `rollback` option. -First, we will define a quick helper function for printing out JS model outputs (you can skip this if using Python): +First, we will define a quick helper function for printing out JS and CURL model outputs (you can skip this if using Python): -```js -function prettyPrint(m) { - const padded = " " + m['type'] + " "; - const sepLen = Math.floor((80 - padded.length) / 2); - const sep = "=".repeat(sepLen); - const secondSep = sep + (padded.length % 2 ? "=" : ""); - - console.log(`${sep}${padded}${secondSep}`); - console.log("\n\n"); - console.log(m.content); -} -``` +=== "Javascript" + + ```js + function prettyPrint(m) { + const padded = " " + m['type'] + " "; + const sepLen = Math.floor((80 - padded.length) / 2); + const sep = "=".repeat(sepLen); + const secondSep = sep + (padded.length % 2 ? "=" : ""); + + console.log(`${sep}${padded}${secondSep}`); + console.log("\n\n"); + console.log(m.content); + } + ``` + +=== "CURL" + + ```bash + # PLACE THIS IN A FILE CALLED pretty_print.sh + pretty_print() { + local type="$1" + local content="$2" + local padded=" $type " + local total_width=80 + local sep_len=$(( (total_width - ${#padded}) / 2 )) + local sep=$(printf '=%.0s' $(eval "echo {1.."${sep_len}"}")) + local second_sep=$sep + if (( (total_width - ${#padded}) % 2 )); then + second_sep="${second_sep}=" + fi + + echo "${sep}${padded}${second_sep}" + echo + echo "$content" + } + ``` Now, let's import our required packages and instantiate our client, assistant, and thread. @@ -31,6 +55,7 @@ Now, let's import our required packages and instantiate our client, assistant, a from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" assistant_id = "agent" thread = await client.threads.create() ``` @@ -41,10 +66,19 @@ Now, let's import our required packages and instantiate our client, assistant, a import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" const assistantId = "agent"; const thread = await client.threads.create(); ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' + ``` + Now let's run a thread with the multitask parameter set to "rollback": === "Python" @@ -91,6 +125,26 @@ Now let's run a thread with the multitask parameter set to "rollback": await client.runs.join(thread["thread_id"], run["run_id"]); ``` +=== "CURL" + + ```bash + curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in sf?\"}]}, + }" && sleep 2 && curl --request POST \ + --url >/threads//runs \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what\'s the weather in nyc?\"}]}, + \"multitask_strategy\": \"rollback\" + }" && curl --request GET \ + --url /threads//runs//join + ``` + We can see that the thread has data only from the second run === "Python" @@ -112,12 +166,24 @@ We can see that the thread has data only from the second run } ``` +=== "CURL" + + ```bash + source pretty_print.sh && curl --request GET \ + --url /threads//state | \ + jq -c '.values.messages[]' | while read -r element; do + type=$(echo "$element" | jq -r '.type') + content=$(echo "$element" | jq -r '.content | if type == "array" then tostring else . end') + pretty_print "$type" "$content" + done + ``` + Output: - ================================ Human Message ================================= + ================================ Human Message ================================= what's the weather in nyc? - ================================== Ai Message ================================== + ================================== Ai Message ================================== [{'id': 'toolu_01JzPqefao1gxwajHQ3Yh3JD', 'input': {'query': 'weather in nyc'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}] Tool Calls: @@ -125,11 +191,11 @@ Output: Call ID: toolu_01JzPqefao1gxwajHQ3Yh3JD Args: query: weather in nyc - ================================= Tool Message ================================= + ================================= Tool Message ================================= Name: tavily_search_results_json [{"url": "https://www.weatherapi.com/", "content": "{'location': {'name': 'New York', 'region': 'New York', 'country': 'United States of America', 'lat': 40.71, 'lon': -74.01, 'tz_id': 'America/New_York', 'localtime_epoch': 1718734479, 'localtime': '2024-06-18 14:14'}, 'current': {'last_updated_epoch': 1718733600, 'last_updated': '2024-06-18 14:00', 'temp_c': 29.4, 'temp_f': 84.9, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 2.2, 'wind_kph': 3.6, 'wind_degree': 158, 'wind_dir': 'SSE', 'pressure_mb': 1025.0, 'pressure_in': 30.26, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 63, 'cloud': 0, 'feelslike_c': 31.3, 'feelslike_f': 88.3, 'windchill_c': 28.3, 'windchill_f': 82.9, 'heatindex_c': 29.6, 'heatindex_f': 85.3, 'dewpoint_c': 18.4, 'dewpoint_f': 65.2, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 7.0, 'gust_mph': 16.5, 'gust_kph': 26.5}}"}] - ================================== Ai Message ================================== + ================================== Ai Message ================================== The weather API results show that the current weather in New York City is sunny with a temperature of around 85°F (29°C). The wind is light at around 2-3 mph from the south-southeast. Overall it looks like a nice sunny summer day in NYC. diff --git a/docs/docs/cloud/how-tos/same-thread.md b/docs/docs/cloud/how-tos/same-thread.md new file mode 100644 index 000000000..f48e72b6f --- /dev/null +++ b/docs/docs/cloud/how-tos/same-thread.md @@ -0,0 +1,310 @@ +# How to run multiple agents on the same thread + +In LangGraph Cloud, a thread is not explicitly associated with a particular agent. +This means that you can run multiple agents on the same thread, which allows a different agent to continue from an initial agent's progress. + +In this example, we will create two agents and then call them both on the same thread. +You'll see that the second agent will respond using information from the [checkpoint](https://langchain-ai.github.io/langgraph/concepts/low_level/#checkpointer-state) generated in the thread by the first agent as context. + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + + openai_assistant = await client.assistants.create( + graph_id="agent", config={"configurable": {"model_name": "openai"}} + ) + + # There should always be a default assistant with no configuration + assistants = await client.assistants.search() + default_assistant = [a for a in assistants if not a["config"]][0] + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + + const openAIAssistant = await client.assistants.create( + { graphId: "agent", config: {"configurable": {"model_name": "openai"}}} + ); + + const assistants = await client.assistants.search(); + const defaultAssistant = assistants.find(a => !a.config); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants \ + --header 'Content-Type: application/json' \ + --data '{ + "graph_id": "agent", + "config": { "configurable": { "model_name": "openai" } } + }' && \ + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0]' + ``` + +We can see that these agents are different: + +=== "Python" + + ```python + print(openai_assistant) + ``` + +=== "Javascript" + + ```js + console.log(openAIAssistant); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /assistants/ + ``` + +Output: + + { + "assistant_id": "db87f39d-b2b1-4da8-ac65-cf81beb3c766", + "graph_id": "agent", + "created_at": "2024-08-30T21:18:51.850581+00:00", + "updated_at": "2024-08-30T21:18:51.850581+00:00", + "config": { + "configurable": { + "model_name": "openai" + } + }, + "metadata": {} + } + +=== "Python" + + ```python + print(default_assistant) + ``` + +=== "Javascript" + + ```js + console.log(defaultAssistant); + ``` + +=== "CURL" + + ```bash + curl --request GET \ + --url /assistants/ + ``` + +Output: + + { + "assistant_id": "fe096781-5601-53d2-b2f6-0d3403f7e9ca", + "graph_id": "agent", + "created_at": "2024-08-08T22:45:24.562906+00:00", + "updated_at": "2024-08-08T22:45:24.562906+00:00", + "config": {}, + "metadata": { + "created_by": "system" + } + } + +We can now run the OpenAI assistant on the thread first. + +=== "Python" + + ```python + thread = await client.threads.create() + input = {"messages": [{"role": "user", "content": "who made you?"}]} + async for event in client.runs.stream( + thread["thread_id"], + openai_assistant["assistant_id"], + input=input, + stream_mode="updates", + ): + print(f"Receiving event of type: {event.event}") + print(event.data) + print("\n\n") + ``` + +=== "Javascript" + + ```js + const thread = await client.threads.create(); + let input = {"messages": [{"role": "user", "content": "who made you?"}]} + + const streamResponse = client.runs.stream( + thread["thread_id"], + openAIAssistant["assistant_id"], + { + input, + streamMode: "updates" + } + ); + for await (const event of streamResponse) { + console.log(`Receiving event of type: ${event.event}`); + console.log(event.data); + console.log("\n\n"); + } + ``` + +=== "CURL" + + ```bash + thread_id=$(curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' | jq -r '.thread_id') && \ + curl --request POST \ + --url "/threads/${thread_id}/runs/stream" \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + "input": { + "messages": [ + { + "role": "human", + "content": "who made you?" + } + ] + }, + "stream_mode": [ + "updates" + ] + }' | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n\n" + } + } + ' + ``` + +Output: + + Receiving event of type: metadata + {'run_id': '1ef671c5-fb83-6e70-b698-44dba2d9213e'} + + + Receiving event of type: updates + {'agent': {'messages': [{'content': 'I was created by OpenAI, a research organization focused on developing and advancing artificial intelligence technology.', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop', 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_157b3831f5'}, 'type': 'ai', 'name': None, 'id': 'run-f5735b86-b80d-4c71-8dc3-4782b5a9c7c8', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}} + +Now, we can run it on the default assistant and see that this second assistant is aware of the initial question, and can answer the question, "and you?": + +=== "Python" + + ```python + input = {"messages": [{"role": "user", "content": "and you?"}]} + async for event in client.runs.stream( + thread["thread_id"], + default_assistant["assistant_id"], + input=input, + stream_mode="updates", + ): + print(f"Receiving event of type: {event.event}") + print(event.data) + print("\n\n") + ``` + +=== "Javascript" + + ```js + let input = {"messages": [{"role": "user", "content": "and you?"}]} + + const streamResponse = client.runs.stream( + thread["thread_id"], + defaultAssistant["assistant_id"], + { + input, + streamMode: "updates" + } + ); + for await (const event of streamResponse) { + console.log(`Receiving event of type: ${event.event}`); + console.log(event.data); + console.log("\n\n"); + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + "input": { + "messages": [ + { + "role": "human", + "content": "and you?" + } + ] + }, + "stream_mode": [ + "updates" + ] + }' | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n\n" + } + } + ' + ``` + +Output: + + Receiving event of type: metadata + {'run_id': '1ef6722d-80b3-6fbb-9324-253796b1cd13'} + + + Receiving event of type: updates + {'agent': {'messages': [{'content': [{'text': 'I am an artificial intelligence created by Anthropic, not by OpenAI. I should not have stated that OpenAI created me, as that is incorrect. Anthropic is the company that developed and trained me using advanced language models and AI technology. I will be more careful about providing accurate information regarding my origins in the future.', 'type': 'text', 'index': 0}], 'additional_kwargs': {}, 'response_metadata': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'type': 'ai', 'name': None, 'id': 'run-ebaacf62-9dd9-4165-9535-db432e4793ec', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 302, 'output_tokens': 72, 'total_tokens': 374}}]}} + + + diff --git a/docs/docs/cloud/how-tos/stateless_runs.md b/docs/docs/cloud/how-tos/stateless_runs.md new file mode 100644 index 000000000..e28363963 --- /dev/null +++ b/docs/docs/cloud/how-tos/stateless_runs.md @@ -0,0 +1,180 @@ +# Stateless Runs + +Most of the time, you provide a `thread_id` to your client when you run your graph in order to keep track of prior runs through the persistent state implemented in LangGraph Cloud. However, if you don't need to persist the runs you don't need to use the built in persistent state and can create stateless runs. + +## Setup + +First, let's setup our client: + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" + # create thread + thread = await client.threads.create() + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantId = "agent"; + // create thread + const thread = await client.threads.create(); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0].graph_id' && \ + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + +## Stateless streaming + +We can stream the results of a stateless run in an almost identical fashion to how we stream from a run with the state attribute, but instead of passing a value to the `thread_id` parameter, we pass `None`: + +=== "Python" + + ```python + input = { + "messages": [ + {"role": "user", "content": "Hello! My name is Bagatur and I am 26 years old."} + ] + } + + async for chunk in client.runs.stream( + # Don't pass in a thread_id and the stream will be stateless + None, + assistant_id, + input=input, + stream_mode="updates", + ): + if chunk.data and "run_id" not in chunk.data: + print(chunk.data) + ``` + +=== "Javascript" + + ```js + let input = { + messages: [ + { role: "user", content: "Hello! My name is Bagatur and I am 26 years old." } + ] + }; + + const streamResponse = client.runs.stream( + // Don't pass in a thread_id and the stream will be stateless + null, + assistantId, + { + input, + streamMode: "updates" + } + ); + for await (const chunk of streamResponse) { + if (chunk.data && !("run_id" in chunk.data)) { + console.log(chunk.data); + } + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"Hello! My name is Bagatur and I am 26 years old.\"}]}, + \"stream_mode\": [ + \"updates\" + ] + }" | jq -c 'select(.data and (.data | has("run_id") | not)) | .data' + ``` + +Output: + + {'agent': {'messages': [{'content': "Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you're interested in.", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-489ec573-1645-4ce2-a3b8-91b391d50a71', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}} + +## Waiting for stateless results + +In addition to streaming, you can also wait for a stateless result by using the `.wait` function like follows: + +=== "Python" + + ```python + stateless_run_result = await client.runs.wait( + None, + assistant_id, + input=input, + ) + print(stateless_run_result) + ``` + +=== "Javascript" + + ```js + let statelessRunResult = await client.runs.wait( + null, + assistantId, + { input: input } + ); + console.log(statelessRunResult); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /runs/runs/wait \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + }' + ``` + +Output: + + { + 'messages': [ + { + 'content': 'Hello! My name is Bagatur and I am 26 years old.', + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'human', + 'name': None, + 'id': '5e088543-62c2-43de-9d95-6086ad7f8b48', + 'example': False} + , + { + 'content': "Hello Bagatur! It's nice to meet you. Thank you for introducing yourself and sharing your age. Is there anything specific you'd like to know or discuss? I'm here to help with any questions or topics you'd like to explore.", + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'ai', + 'name': None, + 'id': 'run-d6361e8d-4d4c-45bd-ba47-39520257f773', + 'example': False, + 'tool_calls': [], + 'invalid_tool_calls': [], + 'usage_metadata': None + } + ] + } \ No newline at end of file diff --git a/docs/docs/cloud/how-tos/stream_debug.md b/docs/docs/cloud/how-tos/stream_debug.md index fe3245947..2dac3d7d7 100644 --- a/docs/docs/cloud/how-tos/stream_debug.md +++ b/docs/docs/cloud/how-tos/stream_debug.md @@ -14,6 +14,8 @@ First let's set up our client and thread: from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" # create thread thread = await client.threads.create() print(thread) @@ -25,18 +27,33 @@ First let's set up our client and thread: import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' ``` Output: - {'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', - 'created_at': '2024-06-21T22:10:27.696862+00:00', - 'updated_at': '2024-06-21T22:10:27.696862+00:00', - 'metadata': {}} + { + 'thread_id': 'd0cbe9ad-f11c-443a-9f6f-dca0ae5a0dd3', + 'created_at': '2024-06-21T22:10:27.696862+00:00', + 'updated_at': '2024-06-21T22:10:27.696862+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } @@ -56,7 +73,7 @@ Output: # stream debug async for chunk in client.runs.stream( thread_id=thread["thread_id"], - assistant_id="agent", + assistant_id=assistant_id, input=input, stream_mode="debug", ): @@ -70,30 +87,67 @@ Output: ```js // create input const input = { - "messages": [ - { - "role": "human", - "content": "What's the weather in SF?", - } - ] - } + messages: [ + { + role: "human", + content: "What's the weather in SF?", + } + ] + }; // stream debug const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, streamMode: "debug" } ); + for await (const chunk of streamResponse) { - console.log(f"Receiving new event of type: {chunk.event}...") - console.log(chunk.data) - console.log("\n\n") + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); } ``` +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data "{ + \"assistant_id\": \"agent\", + \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"What's the weather in SF?\"}]}, + \"stream_mode\": [ + \"debug\" + ] + }" | \ + sed 's/\r$//' | \ + awk ' + /^event:/ { + if (data_content != "") { + print data_content "\n" + } + sub(/^event: /, "Receiving event of type: ", $0) + printf "%s...\n", $0 + data_content = "" + } + /^data:/ { + sub(/^data: /, "", $0) + data_content = $0 + } + END { + if (data_content != "") { + print data_content "\n" + } + } + ' + ``` + + Output: Receiving new event of type: metadata... diff --git a/docs/docs/cloud/how-tos/stream_events.md b/docs/docs/cloud/how-tos/stream_events.md index bbaaaf7be..fcac1dedc 100644 --- a/docs/docs/cloud/how-tos/stream_events.md +++ b/docs/docs/cloud/how-tos/stream_events.md @@ -8,6 +8,8 @@ This guide covers how to stream events from your graph (`stream_mode="events"`). from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" # create thread thread = await client.threads.create() print(thread) @@ -19,9 +21,11 @@ This guide covers how to stream events from your graph (`stream_mode="events"`). import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); ``` === "CURL" @@ -35,12 +39,15 @@ This guide covers how to stream events from your graph (`stream_mode="events"`). Output: - {'thread_id': '3f4c64e0-f792-4a5e-aa07-a4404e06e0bd', - 'created_at': '2024-06-24T22:16:29.301522+00:00', - 'updated_at': '2024-06-24T22:16:29.301522+00:00', - 'metadata': {}, - 'status': 'idle', - 'config': {}} + { + 'thread_id': '3f4c64e0-f792-4a5e-aa07-a4404e06e0bd', + 'created_at': '2024-06-24T22:16:29.301522+00:00', + 'updated_at': '2024-06-24T22:16:29.301522+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } @@ -63,7 +70,7 @@ Streaming events produces responses containing an `event` key (in addition to ot # stream events async for chunk in client.runs.stream( thread_id=thread["thread_id"], - assistant_id="agent", + assistant_id=assistant_id, input=input, stream_mode="events", ): @@ -77,27 +84,27 @@ Streaming events produces responses containing an `event` key (in addition to ot ```js // create input const input = { - "messages": [ - { - "role": "human", - "content": "What's the weather in SF?", - } - ] + "messages": [ + { + "role": "human", + "content": "What's the weather in SF?", + } + ] } // stream events const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, streamMode: "events" } ); for await (const chunk of streamResponse) { - console.log(f"Receiving new event of type: {chunk.event}...") - console.log(chunk.data) - console.log("\n\n") + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); } ``` @@ -280,9 +287,6 @@ Output: Receiving new event of type: end... None - - - ## Token-by-Token Streaming @@ -297,7 +301,7 @@ Token-by-token streaming can be implemented with the `events` streaming mode. Th # stream token-by-token async for chunk in client.runs.stream( thread_id=thread["thread_id"], - assistant_id="agent", + assistant_id=assistant_id, input=input, stream_mode="events", ): @@ -318,7 +322,7 @@ Token-by-token streaming can be implemented with the `events` streaming mode. Th // stream events const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, streamMode: "events" diff --git a/docs/docs/cloud/how-tos/stream_messages.md b/docs/docs/cloud/how-tos/stream_messages.md index c305a7d85..d52e69ebd 100644 --- a/docs/docs/cloud/how-tos/stream_messages.md +++ b/docs/docs/cloud/how-tos/stream_messages.md @@ -22,10 +22,10 @@ E.g., the state should look something like: import { Annotation, messagesStateReducer } from "@langchain/langgraph"; export const StateAnnotation = Annotation.Root({ - messages: Annotation({ + messages: Annotation({ reducer: messagesStateReducer, default: () => [], - }), + }), }); ``` @@ -46,6 +46,8 @@ First let's set up our client and thread: from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" # create thread thread = await client.threads.create() print(thread) @@ -57,9 +59,11 @@ First let's set up our client and thread: import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); ``` === "CURL" @@ -72,12 +76,15 @@ First let's set up our client and thread: Output: - {'thread_id': 'e1431c95-e241-4d1d-a252-27eceb1e5c86', - 'created_at': '2024-06-21T15:48:59.808924+00:00', - 'updated_at': '2024-06-21T15:48:59.808924+00:00', - 'metadata': {}, - 'status': 'idle', - 'config': {}} + { + 'thread_id': 'e1431c95-e241-4d1d-a252-27eceb1e5c86', + 'created_at': '2024-06-21T15:48:59.808924+00:00', + 'updated_at': '2024-06-21T15:48:59.808924+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } Let's also define a helper function for better formatting of the tool calls in messages (for CURL we will define a helper script called `process_stream.sh`) @@ -182,7 +189,7 @@ Now we can stream by messages, which will return complete messages (at the end o async for event in client.runs.stream( thread["thread_id"], - assistant_id="agent", + assistant_id=assistant_id, input=input, config=config, stream_mode="messages", @@ -221,24 +228,25 @@ Now we can stream by messages, which will return complete messages (at the end o ```js const input = { - "messages": [ + messages: [ { - "role": "human", - "content": "What's the weather in sf", + role: "human", + content: "What's the weather in sf", } ] - } - const config = {"configurable": {"model_name": "openai"}} + }; + const config = { configurable: { model_name: "openai" } }; const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, config, streamMode: "messages" } ); + for await (const event of streamResponse) { if (event.event === "metadata") { console.log(`Metadata: Run ID - ${event.data.run_id}`); diff --git a/docs/docs/cloud/how-tos/stream_multiple.md b/docs/docs/cloud/how-tos/stream_multiple.md index 2f03fb550..39c87aa7e 100644 --- a/docs/docs/cloud/how-tos/stream_multiple.md +++ b/docs/docs/cloud/how-tos/stream_multiple.md @@ -10,6 +10,8 @@ First let's set up our client and thread: from langgraph_sdk import get_client client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" # create thread thread = await client.threads.create() print(thread) @@ -21,9 +23,11 @@ First let's set up our client and thread: import { Client } from "@langchain/langgraph-sdk"; const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); ``` === "CURL" @@ -36,12 +40,15 @@ First let's set up our client and thread: Output: - {'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', - 'created_at': '2024-06-24T21:30:07.980789+00:00', - 'updated_at': '2024-06-24T21:30:07.980789+00:00', - 'metadata': {}, - 'status': 'idle', - 'config': {}} + { + 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', + 'created_at': '2024-06-24T21:30:07.980789+00:00', + 'updated_at': '2024-06-24T21:30:07.980789+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } When configuring multiple streaming modes for a run, responses for each respective mode will be produced. In the following example, note that a `list` of modes (`messages`, `events`, `debug`) is passed to the `stream_mode` parameter and the response contains `events`, `debug`, `messages/complete`, `messages/metadata`, and `messages/partial` event types. @@ -61,7 +68,7 @@ When configuring multiple streaming modes for a run, responses for each respecti # stream events with multiple streaming modes async for chunk in client.runs.stream( thread_id=thread["thread_id"], - assistant_id="agent", + assistant_id=assistant_id, input=input, stream_mode=["messages", "events", "debug"], ): @@ -75,27 +82,27 @@ When configuring multiple streaming modes for a run, responses for each respecti ```js // create input const input = { - "messages": [ + messages: [ { - "role": "human", - "content": "What's the weather in SF?", + role: "human", + content: "What's the weather in SF?", } ] - } + }; // stream events with multiple streaming modes const streamResponse = client.runs.stream( thread["thread_id"], - "agent", + assistantID, { input, streamMode: ["messages", "events", "debug"] } ); for await (const chunk of streamResponse) { - console.log(f"Receiving new event of type: {chunk.event}...") - console.log(chunk.data) - console.log("\n\n") + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); } ``` @@ -482,5 +489,4 @@ Output: None - diff --git a/docs/docs/cloud/how-tos/stream_updates.md b/docs/docs/cloud/how-tos/stream_updates.md index 52234e5f1..ef101b268 100644 --- a/docs/docs/cloud/how-tos/stream_updates.md +++ b/docs/docs/cloud/how-tos/stream_updates.md @@ -1,6 +1,6 @@ # How to stream state updates of your graph -This guide covers how to use `stream_mode="updates"` for your graph, which will stream the updates to the graph state that are made after each node is executed. This differs from using `stream_mode="values"`: instead of streaming the entire value of the state at each superstep, it only streams the updates from each of the nodes that made an update to the state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more.``` +This guide covers how to use `stream_mode="updates"` for your graph, which will stream the updates to the graph state that are made after each node is executed. This differs from using `stream_mode="values"`: instead of streaming the entire value of the state at each superstep, it only streams the updates from each of the nodes that made an update to the state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more. First let's set up our client and thread: @@ -23,7 +23,7 @@ First let's set up our client and thread: const client = new Client({ apiUrl: }); // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); ``` === "CURL" @@ -36,12 +36,15 @@ First let's set up our client and thread: Output: - {'thread_id': '979e3c89-a702-4882-87c2-7a59a250ce16', - 'created_at': '2024-06-21T15:22:07.453100+00:00', - 'updated_at': '2024-06-21T15:22:07.453100+00:00', - 'metadata': {}, - 'status': 'idle', - 'config': {}} + { + 'thread_id': '979e3c89-a702-4882-87c2-7a59a250ce16', + 'created_at': '2024-06-21T15:22:07.453100+00:00', + 'updated_at': '2024-06-21T15:22:07.453100+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } Now we can stream by updates, which outputs updates made to the state by each node after it has executed: @@ -72,13 +75,13 @@ Now we can stream by updates, which outputs updates made to the state by each no ```js const input = { - "messages": [ + messages: [ { - "role": "human", - "content": "What's the weather in la", + role: "human", + content: "What's the weather in la" } ] - } + }; const streamResponse = client.runs.stream( thread["thread_id"], @@ -88,10 +91,11 @@ Now we can stream by updates, which outputs updates made to the state by each no streamMode: "updates" } ); + for await (const chunk of streamResponse) { - console.log(f"Receiving new event of type: {chunk.event}...") - console.log(chunk.data) - console.log("\n\n") + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); } ``` diff --git a/docs/docs/cloud/how-tos/stream_values.md b/docs/docs/cloud/how-tos/stream_values.md index b5c25d11d..d43bc6cea 100644 --- a/docs/docs/cloud/how-tos/stream_values.md +++ b/docs/docs/cloud/how-tos/stream_values.md @@ -1,6 +1,6 @@ # How to stream full state of your graph -This guide covers how to use `stream_mode="values"`, which streams the value of the state at each superstep. This differs from using `stream_mode="updates"`: instead of streaming just the updates to the state from each node, it streams the entire graph state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more.``` +This guide covers how to use `stream_mode="values"`, which streams the value of the state at each superstep. This differs from using `stream_mode="updates"`: instead of streaming just the updates to the state from each node, it streams the entire graph state at that superstep. Read [this conceptual guide](https://langchain-ai.github.io/langgraph/concepts/low_level/#stream-and-astream) to learn more. First let's set up our client and thread: @@ -23,7 +23,7 @@ First let's set up our client and thread: const client = new Client({ apiUrl: }); // create thread const thread = await client.threads.create(); - console.log(thread) + console.log(thread); ``` === "CURL" @@ -36,12 +36,15 @@ First let's set up our client and thread: Output: - {'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', - 'created_at': '2024-06-24T21:30:07.980789+00:00', - 'updated_at': '2024-06-24T21:30:07.980789+00:00', - 'metadata': {}, - 'status': 'idle', - 'config': {}} + { + 'thread_id': 'bfc68029-1f7b-400f-beab-6f9032a52da4', + 'created_at': '2024-06-24T21:30:07.980789+00:00', + 'updated_at': '2024-06-24T21:30:07.980789+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } Now we can stream by values, which streams the full state of the graph after each node has finished executing: @@ -76,9 +79,9 @@ Now we can stream by values, which streams the full state of the graph after eac } ); for await (const chunk of streamResponse) { - console.log(f"Receiving new event of type: {chunk.event}...") - console.log(chunk.data) - console.log("\n\n") + console.log(`Receiving new event of type: ${chunk.event}...`); + console.log(chunk.data); + console.log("\n\n"); } ``` diff --git a/docs/docs/cloud/how-tos/webhooks.md b/docs/docs/cloud/how-tos/webhooks.md new file mode 100644 index 000000000..a7148add4 --- /dev/null +++ b/docs/docs/cloud/how-tos/webhooks.md @@ -0,0 +1,125 @@ +# Use Webhooks + +You may wish to use webhooks in your client, especially when using async streams in case you want to update something in your service once the API call to LangGraph Cloud has finished running. To do so, you will need to expose an endpoint that can accept POST requests, and then pass it to your API request in the "webhook" parameter. + +Currently, the SDK has not exposed this endpoint but you can access it through curl commands as follows. + +The following endpoints accept `webhook` as a parameter: + +- Create Run -> POST /thread/{thread_id}/runs +- Create Thread Cron -> POST /thread/{thread_id}/runs/crons +- Stream Run -> POST /thread/{thread_id}/runs/stream +- Wait Run -> POST /thread/{thread_id}/runs/wait +- Create Cron -> POST /runs/crons +- Stream Run Stateless -> POST /runs/stream +- Wait Run Stateless -> POST /runs/wait + +In this example, we will show calling a webhook after streaming a run. First, let's setup our assistant and thread: + +=== "Python" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=) + # Using the graph deployed with the name "agent" + assistant_id = "agent" + # create thread + thread = await client.threads.create() + print(thread) + ``` + +=== "Javascript" + + ```js + import { Client } from "@langchain/langgraph-sdk"; + + const client = new Client({ apiUrl: }); + // Using the graph deployed with the name "agent" + const assistantID = "agent"; + // create thread + const thread = await client.threads.create(); + console.log(thread); + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /assistants/search \ + --header 'Content-Type: application/json' \ + --data '{ + "limit": 10, + "offset": 0 + }' | jq -c 'map(select(.config == null or .config == {})) | .[0]' && \ + curl --request POST \ + --url /threads \ + --header 'Content-Type: application/json' \ + --data '{}' + ``` + +Output: + + { + 'thread_id': '9dde5490-2b67-47c8-aa14-4bfec88af217', + 'created_at': '2024-08-30T23:07:38.242730+00:00', + 'updated_at': '2024-08-30T23:07:38.242730+00:00', + 'metadata': {}, + 'status': 'idle', + 'config': {}, + 'values': None + } + +Now we can invoke a run with a webhook: + +=== "Python" + + ```python + # create input + input = { "messages": [{ "role": "human", "content": "Hello!" }] } + + async for chunk in client.runs.stream( + thread_id=thread["thread_id"], + assistant_id=assistant_id, + input=input, + stream_mode="events", + webhook="your-webhook" + ): + # Do something with the stream output + pass + ``` + +=== "Javascript" + + ```js + // create input + const input = { messages: [{ role: "human", content: "Hello!" }] }; + + // stream events + const streamResponse = client.runs.stream( + thread["thread_id"], + assistantID, + { + input: input, + webhook: "your-webhook" + } + ); + for await (const chunk of streamResponse) { + // Do something with the stream output + } + ``` + +=== "CURL" + + ```bash + curl --request POST \ + --url /threads//runs/stream \ + --header 'Content-Type: application/json' \ + --data '{ + "assistant_id": , + "input" : {"messages":[{"role": "user", "content": "Hello!"}]}, + "webhook": + }' + ``` + +And that's it! Now you can trigger your custom webhooks whenever you want in your LangGraph applications! \ No newline at end of file diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index f9c8138a8..2efeafb7a 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -229,14 +229,14 @@ nav: - Invoke graph in LangGraph Studio: "cloud/how-tos/invoke_studio.md" - Interact with threads in LangGraph Studio: "cloud/how-tos/threads_studio.md" - Different Types of Runs: - - Run an Agent in the Background: "cloud/how-tos/cloud_examples/background_run.ipynb" - - Run Multiple Agents in Same Thread: "cloud/how-tos/cloud_examples/same-thread.ipynb" - - Create Cron Jobs: "cloud/how-tos/cloud_examples/cron_jobs.ipynb" - - Create Stateless Runs: "cloud/how-tos/cloud_examples/stateless_runs.ipynb" + - Run an Agent in the Background: "cloud/how-tos/background_run.md" + - Run Multiple Agents in Same Thread: "cloud/how-tos/same-thread.md" + - Create Cron Jobs: "cloud/how-tos/cron_jobs.md" + - Create Stateless Runs: "cloud/how-tos/stateless_runs.md" - Other: - - Configure Agents: "cloud/how-tos/cloud_examples/configuration_cloud.ipynb" + - Configure Agents: "cloud/how-tos/configuration_cloud.md" - Convert LangGraph calls to LangGraph Cloud calls: "cloud/how-tos/cloud_examples/langgraph_to_langgraph_cloud.ipynb" - - Integrate Webhooks: 'cloud/how-tos/cloud_examples/webhooks.ipynb' + - Integrate Webhooks: 'cloud/how-tos/webhooks.md' - Copy Threads: 'cloud/how-tos/copy_threads.md' - Check Status of Threads: "cloud/how-tos/check_thread_status.md" - Conceptual Guides: diff --git a/examples/cloud_examples/background_run.ipynb b/examples/cloud_examples/background_run.ipynb deleted file mode 100644 index 6279402fc..000000000 --- a/examples/cloud_examples/background_run.ipynb +++ /dev/null @@ -1,388 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# How to kick off background runs\n", - "\n", - "This guide covers how to kick off background runs for your agent.\n", - "This can be useful for long running jobs." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "b8e6408a-b37e-428f-9567-077fa55d58e8", - "metadata": {}, - "outputs": [], - "source": [ - "# Initialize the client\n", - "from langgraph_sdk import get_client\n", - "\n", - "client = get_client()" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "4947e9bc-111f-4991-8c41-1041da9bf0ba", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'assistant_id': 'e90fee30-be91-43aa-a33c-d54bd219072e',\n", - " 'graph_id': 'agent',\n", - " 'created_at': '2024-06-18T18:06:55.102231+00:00',\n", - " 'updated_at': '2024-06-18T18:06:55.102231+00:00',\n", - " 'config': {'configurable': {'model_name': 'anthropic'}},\n", - " 'metadata': {}}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# List available assistants\n", - "assistants = await client.assistants.search()\n", - "assistants[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "230c0464-a6e5-420f-9e38-ca514e5634ce", - "metadata": {}, - "outputs": [], - "source": [ - "# NOTE: we can use `assistant_id` UUID from the above response, or just pass graph ID instead when creating runs. we'll use graph ID here\n", - "assistant_id = \"agent\"" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "56aa5159-5583-4134-9210-709b969bda6f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'created_at': '2024-06-21T14:58:02.079462+00:00',\n", - " 'updated_at': '2024-06-21T14:58:02.079462+00:00',\n", - " 'metadata': {}}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create a new thread\n", - "thread = await client.threads.create()\n", - "thread" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "147c3f98-f889-4f05-a090-6b31f2a0b291", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# If we list runs on this thread, we can see it is empty\n", - "runs = await client.runs.list(thread[\"thread_id\"])\n", - "runs" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "8c7b44ef-4816-496d-88a1-2f7327cf576d", - "metadata": {}, - "outputs": [], - "source": [ - "# Let's kick off a run\n", - "input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]}\n", - "run = await client.runs.create(thread[\"thread_id\"], assistant_id, input=input)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "d84b4d80-b0aa-4d9f-a05d-0744b2fe8f72", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n", - " 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n", - " 'created_at': '2024-06-21T14:58:02.095911+00:00',\n", - " 'updated_at': '2024-06-21T14:58:02.095911+00:00',\n", - " 'metadata': {},\n", - " 'status': 'pending',\n", - " 'kwargs': {'input': {'messages': [{'role': 'human',\n", - " 'content': 'what's the weather in sf'}]},\n", - " 'config': {'metadata': {'created_by': 'system'},\n", - " 'configurable': {'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n", - " 'user_id': '',\n", - " 'graph_id': 'agent',\n", - " 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'thread_ts': None,\n", - " 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}},\n", - " 'webhook': None,\n", - " 'temporary': False,\n", - " 'stream_mode': ['events'],\n", - " 'feedback_keys': None,\n", - " 'interrupt_after': None,\n", - " 'interrupt_before': None},\n", - " 'multitask_strategy': 'reject'}" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# The first time we poll it, we can see `status=pending`\n", - "await client.runs.get(thread[\"thread_id\"], run[\"run_id\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "3639da3c-bfe5-454c-ab1e-8ed7af394dfe", - "metadata": {}, - "outputs": [], - "source": [ - "# Wait until the run finishes\n", - "await client.runs.join(thread[\"thread_id\"], run[\"run_id\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "8fa206ed-515e-4607-9a80-bebafe76cc24", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n", - " 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n", - " 'created_at': '2024-06-21T14:58:02.095911+00:00',\n", - " 'updated_at': '2024-06-21T14:58:02.095911+00:00',\n", - " 'metadata': {},\n", - " 'status': 'success',\n", - " 'kwargs': {'input': {'messages': [{'role': 'human',\n", - " 'content': 'what's the weather in sf'}]},\n", - " 'config': {'metadata': {'created_by': 'system'},\n", - " 'configurable': {'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n", - " 'user_id': '',\n", - " 'graph_id': 'agent',\n", - " 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'thread_ts': None,\n", - " 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}},\n", - " 'webhook': None,\n", - " 'temporary': False,\n", - " 'stream_mode': ['events'],\n", - " 'feedback_keys': None,\n", - " 'interrupt_after': None,\n", - " 'interrupt_before': None},\n", - " 'multitask_strategy': 'reject'}" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Eventually, it should finish and we should see `status=success`\n", - "await client.runs.get(thread[\"thread_id\"], run[\"run_id\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "8de4495f-7873-487c-b1a8-ad2a78a1ff35", - "metadata": {}, - "outputs": [], - "source": [ - "# We can get the final results\n", - "final_result = await client.threads.get_state(thread[\"thread_id\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "9da76fce-66e4-4f1b-8c24-09759889e50e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'values': {'messages': [{'content': 'what's the weather in sf',\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'human',\n", - " 'name': None,\n", - " 'id': 'bfe07fff-cb40-40be-84d5-a061d2c40006',\n", - " 'example': False},\n", - " {'content': [{'id': 'toolu_01QUzhhfDQkpbPSediUrXvQb',\n", - " 'input': {'query': 'weather in san francisco'},\n", - " 'name': 'tavily_search_results_json',\n", - " 'type': 'tool_use'}],\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'ai',\n", - " 'name': None,\n", - " 'id': 'run-6d8665ca-a77d-4b44-9a7b-4e975b155fb1',\n", - " 'example': False,\n", - " 'tool_calls': [{'name': 'tavily_search_results_json',\n", - " 'args': {'query': 'weather in san francisco'},\n", - " 'id': 'toolu_01QUzhhfDQkpbPSediUrXvQb'}],\n", - " 'invalid_tool_calls': [],\n", - " 'usage_metadata': None},\n", - " {'content': '[{\"url\": \"https://www.timeanddate.com/weather/usa/san-francisco/historic\", \"content\": \"San Francisco Weather History for the Previous 24 Hours Show weather for: Previous 24 hours June 17, 2024 June 16, 2024 June 15, 2024 June 14, 2024 June 13, 2024 June 12, 2024 June 11, 2024 June 10, 2024 June 9, 2024 June 8, 2024 June 7, 2024 June 6, 2024 June 5, 2024 June 4, 2024 June 3, 2024 June 2, 2024\"}]',\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'tool',\n", - " 'name': 'tavily_search_results_json',\n", - " 'id': '257a1f29-2f66-4f9e-b35d-c8818dbbaa3f',\n", - " 'tool_call_id': 'toolu_01QUzhhfDQkpbPSediUrXvQb'},\n", - " {'content': [{'text': 'The search results provide historic weather data for San Francisco, but do not give the current weather conditions. To get the current weather forecast for San Francisco, I would need to refine my search query. Here is an updated search:',\n", - " 'type': 'text'},\n", - " {'id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx',\n", - " 'input': {'query': 'san francisco weather forecast today'},\n", - " 'name': 'tavily_search_results_json',\n", - " 'type': 'tool_use'}],\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'ai',\n", - " 'name': None,\n", - " 'id': 'run-ca41dbf8-7e89-4ff2-a245-87098d7928ba',\n", - " 'example': False,\n", - " 'tool_calls': [{'name': 'tavily_search_results_json',\n", - " 'args': {'query': 'san francisco weather forecast today'},\n", - " 'id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx'}],\n", - " 'invalid_tool_calls': [],\n", - " 'usage_metadata': None},\n", - " {'content': '[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'San Francisco\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 37.78, \\'lon\\': -122.42, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1718981382, \\'localtime\\': \\'2024-06-21 7:49\\'}, \\'current\\': {\\'last_updated_epoch\\': 1718981100, \\'last_updated\\': \\'2024-06-21 07:45\\', \\'temp_c\\': 12.8, \\'temp_f\\': 55.0, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Overcast\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/122.png\\', \\'code\\': 1009}, \\'wind_mph\\': 6.9, \\'wind_kph\\': 11.2, \\'wind_degree\\': 200, \\'wind_dir\\': \\'SSW\\', \\'pressure_mb\\': 1011.0, \\'pressure_in\\': 29.84, \\'precip_mm\\': 0.01, \\'precip_in\\': 0.0, \\'humidity\\': 86, \\'cloud\\': 100, \\'feelslike_c\\': 12.2, \\'feelslike_f\\': 53.9, \\'windchill_c\\': 11.2, \\'windchill_f\\': 52.1, \\'heatindex_c\\': 12.0, \\'heatindex_f\\': 53.5, \\'dewpoint_c\\': 9.4, \\'dewpoint_f\\': 48.8, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 3.0, \\'gust_mph\\': 7.6, \\'gust_kph\\': 12.2}}\"}]',\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'tool',\n", - " 'name': 'tavily_search_results_json',\n", - " 'id': 'c80a3720-6a9f-4ff0-9ce2-6112e66a6f81',\n", - " 'tool_call_id': 'toolu_01RLJEcWYRvRoBhiHdrhoRZx'},\n", - " {'content': 'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!',\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'type': 'ai',\n", - " 'name': None,\n", - " 'id': 'run-4f23b53d-a8ec-4038-b3ed-08b2560bf81c',\n", - " 'example': False,\n", - " 'tool_calls': [],\n", - " 'invalid_tool_calls': [],\n", - " 'usage_metadata': None}]},\n", - " 'next': [],\n", - " 'config': {'configurable': {'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'thread_ts': '1ef2fdea-f879-65a5-8005-443b6a4039aa'}},\n", - " 'metadata': {'step': 5,\n", - " 'run_id': '1ef2fdea-814c-6165-8b2a-a40e2a028198',\n", - " 'source': 'loop',\n", - " 'writes': {'agent': {'messages': [{'id': 'run-4f23b53d-a8ec-4038-b3ed-08b2560bf81c',\n", - " 'name': None,\n", - " 'type': 'ai',\n", - " 'content': 'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!',\n", - " 'example': False,\n", - " 'tool_calls': [],\n", - " 'usage_metadata': None,\n", - " 'additional_kwargs': {},\n", - " 'response_metadata': {},\n", - " 'invalid_tool_calls': []}]}},\n", - " 'user_id': '',\n", - " 'graph_id': 'agent',\n", - " 'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'created_by': 'system',\n", - " 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},\n", - " 'created_at': '2024-06-21T14:58:14.591805+00:00',\n", - " 'parent_config': {'configurable': {'thread_id': '5fc20631-47b7-48cd-8aa2-9f2eace9778d',\n", - " 'thread_ts': '1ef2fdea-d44c-6fc4-8004-d2713436777d'}}}" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "final_result" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "ddd6e698-4609-4389-b84a-bb8939fff08b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'The updated search provides the current weather forecast for San Francisco. According to the results, as of 7:49am on June 21, 2024 in San Francisco, the temperature is 55°F (12.8°C), it is overcast with 100% cloud cover, and there are light winds from the south-southwest around 7 mph (11 km/h). The forecast also shows low precipitation of 0.01 mm, high humidity of 86%, and visibility of 9 miles (16 km).\\n\\nIn summary, the current weather in San Francisco is cool, overcast, and breezy based on this weather forecast data. Let me know if you need any other details!'" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# We can get the content of the final message\n", - "final_result[\"values\"][\"messages\"][-1][\"content\"]" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "langgraph-example-dev", - "language": "python", - "name": "langgraph-example-dev" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/cloud_examples/configuration_cloud.ipynb b/examples/cloud_examples/configuration_cloud.ipynb deleted file mode 100644 index e6d29170b..000000000 --- a/examples/cloud_examples/configuration_cloud.ipynb +++ /dev/null @@ -1,198 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "68c0837d-c40a-4209-9f88-5d08c00c31b0", - "metadata": {}, - "source": [ - "# How to create agents with configuration\n", - "\n", - "One of the benefits of LangGraph API is that it lets you create agents with different configurations.\n", - "This is useful when you want to:\n", - "\n", - "- Define a cognitive architecture once as a LangGraph\n", - "- Let that LangGraph be configurable across some attributes (for example, system message or LLM to use)\n", - "- Let users create agents with arbitrary configurations, save them, and then use them in the future\n", - "\n", - "In this guide we will show how to do that for the default agent we have built in.\n", - "\n", - "If you look at the agent we defined, you can see that inside the `call_model` node we have created the model based on some configuration. That node looks like:\n", - "\n", - "```python\n", - "def call_model(state, config):\n", - " messages = state[\"messages\"]\n", - " model_name = config.get('configurable', {}).get(\"model_name\", \"anthropic\")\n", - " model = _get_model(model_name)\n", - " response = model.invoke(messages)\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": [response]}\n", - "```\n", - "\n", - "We are looking inside the config for a `model_name` parameter (which defaults to `anthropic` if none is found).\n", - "That means that by default we are using Anthropic as our model provider.\n", - "In this example we will see an example of how to create an example agent that is configured to use OpenAI.\n", - "\n", - "We've also communicated to the graph that it should expect configuration with this key. \n", - "We've done this by passing `config_schema` when constructing the graph, eg:\n", - "\n", - "```python\n", - "class GraphConfig(TypedDict):\n", - " model_name: Literal[\"anthropic\", \"openai\"]\n", - "\n", - "\n", - "# Define a new graph\n", - "workflow = StateGraph(AgentState, config_schema=GraphConfig)\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "f69c9a4f-2ef9-4998-827b-fe86d12bfd76", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph_sdk import get_client\n", - "\n", - "client = get_client()" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "9a37bfb5-7331-4004-8054-508838e54f18", - "metadata": {}, - "outputs": [], - "source": [ - "# First, let's check what valid configuration can be\n", - "# We can do this by getting the default assistant\n", - "# There should always be a default assistant with no configuration\n", - "assistants = await client.assistants.search()\n", - "assistants = [a for a in assistants if not a[\"config\"]]\n", - "base_assistant = assistants[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "70193a08-127c-44b3-a102-10db260d7e3b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'model_name': {'title': 'Model Name',\n", - " 'enum': ['anthropic', 'openai'],\n", - " 'type': 'string'}}" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# We can now call `.get_schemas` to get schemas associated with this graph\n", - "schemas = await client.assistants.get_schemas(\n", - " assistant_id=base_assistant[\"assistant_id\"]\n", - ")\n", - "# There are multiple types of schemas\n", - "# We can get the `config_schema` to look at the the configurable parameters\n", - "schemas[\"config_schema\"][\"definitions\"][\"Configurable\"][\"properties\"]" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "99be5aee-9a6b-4515-b72f-ba135a893c65", - "metadata": {}, - "outputs": [], - "source": [ - "assistant = await client.assistants.create(\n", - " graph_id=\"agent\", config={\"configurable\": {\"model_name\": \"openai\"}}\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "4f10d346-69e6-44f4-8ff0-ef539ba938df", - "metadata": {}, - "source": [ - "We can see that this assistant has saved the config" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "3898ca35-eb2c-4b12-97ea-e0cc6a7c6a2e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'assistant_id': '40a3a2bf-5319-4fae-a2ac-05e075615cdc',\n", - " 'graph_id': 'agent',\n", - " 'config': {'configurable': {'model_name': 'openai'}},\n", - " 'created_at': '2024-06-05T23:12:30.519458+00:00',\n", - " 'updated_at': '2024-06-05T23:12:30.519458+00:00',\n", - " 'metadata': {}}" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "assistant" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "68ed7a1b-74be-4560-8c55-c76d49d3d348", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "StreamPart(event='metadata', data={'run_id': '1ef23911-c23b-6d8c-b1dc-94bb982ca7b1'})\n", - "StreamPart(event='values', data={'messages': [{'role': 'user', 'content': 'who made you?'}]})\n", - "StreamPart(event='values', data={'messages': [{'content': 'who made you?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'ed93c1c9-80d6-4f2b-a048-ef859ea533f9', 'example': False}, {'content': 'I was created by OpenAI, a research organization focused on developing and advancing artificial intelligence technology.', 'additional_kwargs': {}, 'response_metadata': {'finish_reason': 'stop'}, 'type': 'ai', 'name': None, 'id': 'run-6560cd65-5c9c-434b-8835-0baadc684760', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]})\n", - "StreamPart(event='end', data=None)\n" - ] - } - ], - "source": [ - "thread = await client.threads.create()\n", - "input = {\"messages\": [{\"role\": \"user\", \"content\": \"who made you?\"}]}\n", - "async for event in client.runs.stream(\n", - " thread[\"thread_id\"], assistant[\"assistant_id\"], input=input\n", - "):\n", - " print(event)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/cloud_examples/cron_jobs.ipynb b/examples/cloud_examples/cron_jobs.ipynb deleted file mode 100644 index 98b6a1b73..000000000 --- a/examples/cloud_examples/cron_jobs.ipynb +++ /dev/null @@ -1,132 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Cron Jobs\n", - "\n", - "Sometimes you don't want to run your graph based on user interaction, but rather you would like to schedule your graph to run on a schedule - for example if you wish for your graph to compose and send out a weekly email of to-dos for your team. LangGraph Cloud allows you to do this without having to write your own script by using the `Crons` client. To schedule a graph job, you need to pass a [cron expression](https://crontab.cronhub.io/) to inform the client when you want to run the graph. `Cron` jobs are run in the background and do not interfere with normal invocations of the graph.\n", - "\n", - "## Setup\n", - "\n", - "First, let's setup our SDK client, assistant, and thread:" - ] - }, - { - "cell_type": "code", - "execution_count": 110, - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph_sdk import get_client\n", - "\n", - "client = get_client()\n", - "assistants = await client.assistants.search()\n", - "assistants = [a for a in assistants if not a[\"config\"]]\n", - "assistant = assistants[0]\n", - "thread = await client.threads.create()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Cron job on a thread \n", - "\n", - "To create a cron job associated with a specific thread, you can write:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# This schedules a job to run at 15:27 (3:27PM) every day\n", - "cron_1 = await client.crons.create_for_thread(\n", - " thread[\"thread_id\"],\n", - " assistant[\"assistant_id\"],\n", - " schedule=\"27 15 * * *\",\n", - " input={\"messages\": [{\"role\": \"user\", \"content\": \"What time is it?\"}]},\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that it is **very** important to delete `Cron` jobs that are no longer useful. Otherwise you could rack up unwanted API charges to the LLM! You can delete a `Cron` job using the following code:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await client.crons.delete(cron_1[\"cron_id\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Cron job stateless\n", - "\n", - "You can also create stateless cron jobs by using the following code:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# This schedules a job to run at 15:27 (3:27PM) every day\n", - "cron_2 = await client.crons.create(\n", - " assistant[\"assistant_id\"],\n", - " schedule=\"27 15 * * *\",\n", - " input={\"messages\": [{\"role\": \"user\", \"content\": \"What time is it?\"}]},\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Again, remember to delete your job once you are done with it!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await client.crons.delete(cron_2[\"cron_id\"])" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/cloud_examples/img/webhook_results.png b/examples/cloud_examples/img/webhook_results.png deleted file mode 100644 index 81b9fe7032d7cd8c122ae5bf8c03f37cb12045b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442633 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
$XL@`Ozv76kEO*?q24<&6Dso_q4fsQ(rKa zk7iMd&NT$noMX*7*cGv&2--iAY>u>Cd~{04U_7>m+*gXeqPmH7NMefNh|D(4mJRWg zEw}hSPz1zL*sXXT;PMgozH`ej^hh9CUGdy_pr75ZQ`fBH`_TDQ&RR}72GkKLnFW8G zR8iuV*qgW^!Jyakgh60jxO#7PL5*G8PLFS-TG6uGO|rE-Da9_C$GrT3;eYzL5yke3lpmu*utp0=l(ql;%Vgbv3!#p`GG_%H9t;l9FL#~TS z?jYKj8LekzFnm6Kxn~$LTCzQ}7^=)z$&7_5$tweo?$IF2`C62MXj{wa--<7_RIami zuA)Jtp=y~|-+yn)?K?GP+3H))e_^1cUwJ2>hKVm2JKHLy+5f!h%hD68-pfx(k%#t; zqQAbVY8WQ*mAEdbhP$l`=}l4tU^!jDNT#X7TEYQnB$Fqq?cTUJ>mE{3IG%-lcsFBbd(bz!-mDpRprp|JoOtAtI=Y+)z`bZz?W=dPN9$S^Zb}Bjc$|JNeKSs|!e{O_m zsH{wo{5di4ykbmg5bzuK+1Ta?HSj2ewtF4@25rBXqU7Y{`X#T<$H(=#jcBGf-0t4J ztEtG-?teZ{?Y=n)lA-dR|K*bZ@!gu58bA!^+7KWxF)?|0d4qz3HS^;#S%ig!HFK3_ zyQ|p3z}DT0^b5PI%L@RvrfOem7Yann$0a08`<-oI;ZaR~diDs=q%1^p-CrALu(bY? z_9`Mr0ix~I-~?4wcWKSP=(Dr4+Y6=)t~{-Bm~wAAn*w=SM%{xl-oUXT-YRXoOrUgg zZpXu8xxS`m%(8+tj8{a&Ypyl){QO*+?!|a@l3Xn_Rd(F?hFjB2sZDbK5~!6L+v&&S zG}_-46uf!!#-P$puioRu%a>pJm%4j<)A+1Z*y2~Wwyp-#FI~@%UubDDYunGgc`@Bs zL0BmN5a#0S45N)(Y7IB#)eYNRJ{6&d?2p1BnjcPY>g=A0jFO3+me2KkUix*q#`VjrWwx>y zQb4`yJ70-gE-Pv9$~Nw0vB(9BASY;Cqore`=1Umg;9RdN&X;zt!-&oG=@W#VGw#vCoysFROi@EvLV1UYGdr$z6RHw(3OY5Vb1x+l|E)ct5 zTbdb8obc`3sGtYC54cUe*!ufZU*aSb&mP)iIG!gF%uoKD=tzOc1URRCte#;=m$#)X zaU^$=^2BB=IMvPd^dw!Wx22^mJCcj@5!JZRla2`wOB+q2u*y|UakJS?7rUSpxq+hM> zcpP(kdw6&_IlUc;0JQ9_H;8&%b3Z=`W2<4?FVk)l_C8X5lJv(ujQ|i}EQe+C3kYmX z)s1W&fm$EvL>#)GK9rRWtQ`QtrS~0AU}^vUcls#1@y`mVSN}?hNW3@~?CQPUNUr+r zW*`TE5ORz5!L-MG?aMV7Euh~3rI?(%v&~Oy-V%g!Zs=pfR5dX*J#F;lNh{xTkGIKD zm7Y9gPPWTQSZkiGo^vzJZ$zv_8VV$Pa;cFg_u!b8|*10B~MqIk{n&;O`aX5_QX|>wO%epBl++f1c8>$}6 zNO0w>NlJ<_t%W}e3u=Mn5*o1=#-E%EkliF041j@ZORB1>9ta4iD<*OoJCx~8Uk0#; zfhg_`$eP|%;fl)2%G}(byE7atEXQ3sjl4oa9{bDcMzU8IjUR#Ths5Lfe&;n7d5OuO z5{E%_8zu{*m)OtB-emRl@9B!4J*r~`KLF@y@9@II@*|eZMr`slqe-B5_jE$ zM%V*efy6+Yu@$VRtg~TbO?t!Xx$eVMea^|jz0<9IC;xBd{dH+caQ5=v!Z%)Lm6MMm zo-B_tF|hW%#xy#o>}0$5ocIB&c{`~Y_T#SY%e>^4H2l|)hNcdm zqMfkj+oB)?<1PPrKZvyTrzF7+60T0aQF^BKVHQZ`^h^V>Pab1$d52|ZexIclKIVWp zc%1l!N9Wj*Z$ck(nd*4yHzp3216H}T#ZQLhA8X$o(9|u{pY)WaV-Y3~a>&wZU)<(M_GySIu{t!irVel6XeH;Bi~_7t6qcCn}N2#s?$CpDncC<;!m` zch6SZlenvFzP?wKL>&2gMc*7!BQ#|lKe5~@bXRn990)5l4S)1vsnr`7PQS=u9RQOX zuX-a|2^h-Vu%)e4Q!^rVL54!W1!a$Rrm*{bl7lQz?|w;zW_@~Pz4N0?2h0767N;(5Q(HQ;NfoWlUCjI&GPZHa7J_upt zN6&X&S|W)Br}bsWwo_DT#t6jAK!LQYtS6!U$*7D@;Ts7Sp8$hFSsR(n%zLVhY|C+_ zAhhQCB>1e!U^u)xuVa$6O*(1tv`Z1ymfRh=oK6+8J|oB z>e`gtPPzA@?wb_kvFXmo%j;_B`26{ETU&I9!?-auFOM|n zn^m&-lp0bl`~uPuxg}=fSYfiQkFUjl}NZsi`!Oc{`TAnW!aLNc^Otps3%d zoB(yy+UhEh6F}+V8PvN3==$mDX~3PRYpef@E{=Z-fug1T9jHXS8UP+iQ)xQi?Pk|0 zSqR)pO--Hehptyi1!7ua<9-gnH7UIbNlBi^+p|Y!&IBO1Tnb2qKrtWllk`yx^etHF z3bo+8hlV!lX$!_zn#a z>Xs#i(r6dS+Pzz4Whj)TgW(6k5lBMcU_iN$C>@Oyw-($n4aLVut#6XpxeI@<9fv`e zN?{!olPQB0`u^>PAJW3eAx)i5Z%RxinV|XOEyhJ^ln3uus9FDGM@W>ELc#`F%rFjP zTl52gY47h!GNw~!;>33|*_^D!Wrn9n9vRFoPGZ)`tc7rK27AX$B=t9>CO zuT5sgK=~N=OGL5PZCaWe1{CE}{UOn$LhVY*ZkWx*DfIRRWQ<{=ci7z^^~@vgtS9B3 zpcIm&%dRdba>q>%5BQWyZhI3V4}BYrt><&ohgOYh3KKFJ3uDl7GdBsJQj#gRG)xh< z;>==-DVEo^Pa=NylHH7mH;&2{3&vxb{b=eJN&}*e(c&;T!GBmDMl34Xw{_}72T0vv zVDwRVNyiY8ZUND_#`maGkgcdah8M&N`rX+C;ws7}CoXPABGPfe2Xy00UhX14oBj z7A*&xo1<{W#C_&Z-Omgva0{E4TJkZic^5za3 z00f6D>&Ei($Kv7x4>6#0jFm51NuG^;{rZ(L@oBm9qM~B{wNrzeDhdM0xFc2S*K^uW~IGwn#AGZZNEwk zMPIHC0d^J0-_=b`P1)HCAffm6_gkR=GYfFXPQc}v@6*2qCA0vSJJo|Pe7sou`KhU> zB0J`R#ysLpMoBqU=f2f;*SM%)b)@m|i~U69*v8>vcOn-0D{$<*bidQ@(e!Z|gZy_n z^|RN!0MoL&2%HLzB5CcU^vGkc0>KncF0QQv0|#NBkMzXCWPt^$Ddp1RZeV(VRbAzi|_UDogHm0g|`X{3M$69 z0;MD&J|0jmIgb+);^H6(oY#sJbA|}9T)S4Xcj6(?!LlZXmsZvEqxnD^`eWJdz2wQRX9 z6TKx^?+9H>6wg=4r1P6LA6bwtomUKgEV+%Xb-iFMKDA8DO*(8QlD9}WcRT6Lw;k*4 zjc?{NTx3xw8=(6bwkUB5kCJapW(o08dLEz#c7BWato_>kAcld8SdxqC7@{$6rSa0x zFr4m^$D0LWvLavGfobgmB1 zx9pTO$kHtr>!X)7FX1qpMvc2Xw|}KE*J*(YifUk*sQ)FE)E0|{ud@F@U0k51@w}By z08WQVN?)5@`@~U#5tHxbv>w%-1?qJuqdgNM44#@wBs8!XYs}XUGhQWxzOpFb3KDYS zmPvoUVK$@`L@I!WJdcN@+F7!g($T*i-=Y*{xWVo0Krai!=NXjcZ4Jo~La$6+bsc<6 z+ASSEv1ePcR1$07sMejUyOc7|l}MTufIn?4yTU4b0FTh2AQvT4(;#RF?s+0zE_D8~ z@QFX|03jf=(WK2*HsCyb_T2?+)xTY0u8~_Nvm37><;_7rZBVzqO2g}@JrZ?(K8zwL zY-)Ya*A{IM{)xiQ?`G$@vneFSpeKtfjs;=&RHx!&JQJW?-I(xk7a2O^BC^km{q#ai zz}z&9f!4hLz4+ayPH42|y~zTAE}*BU2edxt{rh0@0mISath3Qb8Bo&8%3c6^0D>+( z*y6qRdLIA*^u0tnO#)=x3xfrl z=28b7&zMiZ!!CvUl}z*i5uL#_>&(8vFu#q5%AtW;dl**n!f|_ z0EALpd0yHe2*Ml)NK1>0Q$P(7d)C3<3&N7uQePVCdia6cBK;^6L_aynQ*}UnDkyY* z1`M@QQf5Ix$(m0s5Ohlz6t2ZqmBvgC=qvEpq|zwF82KIygOv{CG3%YVwup!bQZt1Z zmK?68b7upCl!Ah}mY{3rCN8(Q$UCY{DH2f#QshO#;4Mm+z2-Lz)_2|qwqU(~sQV(3 z%V|MXfPh{)3L~TgeN7DA&)LlCYS3j@J!HK`lavRDEp^71@%Yr82FJH_e|(LdWNtV# zJildzqE!fuqM)9>dNMty%IZvGHom%`mV!@x61-;37s3!iTr~Pw&An((`MITV0BNDh z*sNSw;BseU@saF!r9Otm^lA>o4Y9OpWFFC^xbc*amJMggVQJ8J3}#SSeRE~ggOH(( zOt!Z2WjnXmp=W%QgJ$&QeBB9Jw0h#w;Z{Rjx0H;F<*@3E&v1rgQTTFxzSS2vnQiuC z0x`oaAM=CHXrzBH^aSvE-$u)Vv&vT@kP*k+0|fHg^pq2iI7`j+4zvwTx(no^+%)LL zS1=*blbNy``(<%%MI!y4oNDbXF-6gbl$$?2<;%^7jGI|P-KpP-318M~!8=i;ov+MR zq6r}=NO6i;%$_n(IxP&`Q&9NH%=19Fo^C7pSx_pFf-=?&knFe z?y3ojicWycbTsJ#bi)eE-tsX!kjVIqi$Zw*IM>(--h+NOo^7&oaxMnW0z-tRqUYv! z;H(*$nQ^QbTXz#JMPNW5;`EDuMcjMPmn=~D`SX7LUSEp1cO`>@&yQ1#E@GimYj)jUDsCUi-^Oz{~Xbeogj=4^moM2zk2bQJ{RbN8CF}R=GYN zH|aunUFGM`soJL$h-$}~#=bt47H+15#}Tqw%qpqY4h|AdzoGVbR&$z){wLkWRHANn zo0B!b{%~Xgtg-dZ;_b(s%;`5EAb^^nT4uLFDY8{PANjnbrhUWhAL%AnImNO!d2z>M zZl`);!{Pb!iAG=1CPtIwKi>keJa67pTmcvi&u=^M`Z9}(P{AXyoeQrpu-<$AAu}A2 zaf`ZM?}r|O{ASpuw-j$T^JWo&oc%B+nOrz}S$w(JC|P*T)@lG-QAy6BcpbLCRUNg; zdO5QFc_WQr+;V+L_q??VxZR{a3i0|5-<5Z3Dm_KFU9C?Rl{8m!+RUbwiFQ>vIoGwT#RU+B8ajjyCkr*P%^oCYc>`aK_TvFm`jzWPZ;em_%xLr6 z<^jUGjT%k6F@cjM|K#Lk@j7xkVF714ac8lgmq7v*Jqd=f{+Cv$){pf5Y#Mg?(TBSI zXwN^y@p!G)kV-nhd2JC4>YClDFLK1|yi7U*^D{`S)Xn-{-xDVer-@mw`4Tf6Php!A zgbA6WQc>l6my(!Jb}laC8+>}RamHU#D}XFa$zQHBdYiz*(zz0SXfpWt)kkmqFuMhswr<(%yw1xWxL1>gFb<&r*j&eEH)-uf|k2_)+*<|vR)9U zplmyC++3G4@dJG^;cvN#9GiB2SzrVx=z6~9W06TQbONIlgRF@7coMFgDpAQvq7*}JZu%x!2wpKr3(VG97F9T)! z?(S}uTof>V0&yK=TAeEL`7-2S$+iN1O5cJ#kkGu}I82KPhPX_&Bh z4eEiSI$xNwdm{l@Q-NX71O>S@}9fCx8EROw2i(p>f__% ze!H>hLKL{cEzgoIg(#Siy2aWs+N;moS=1LHT&;?*SuGA`ykAXM7hCCqZcFJ>9>b5h zjXV>UM#K(=G*u-|%w=UU5iCTvlX5%VTLagdPI?`=+ru)q=i)(Vn5c8-;%XuUPaRqt zDP6L#qQLFw=4z^c)`Iz-g`s=b(f`cgy>|beua}pXABZfwHy9Od7zhSlPT775QzZwW zzIoG_*QOT1t+x}jki-myKmq}*qujH=Jm(kAAA~0>aXObi z`!FKa%1tbOQqj__un7Dtr#IAkAA;t2Lobe#k?6JbQd#@^BNO;nJSviQ%PcCbtl z9B=d=(6{|5)cf;2z3Wjsz&DQ^B^39GqzPg;bSn`d$NAU(rT7_Xg@{!7ed-q^eXV^; z-nb?x-1XBtjJXvbzUlE3Le{x-pgbaRn98g!f1Azym1*;a6tc`tM)wu$-cB?q*h}VQ z$L2m+sXp|PQ9It8+kl|2q^;L^*ph9o)G*`vTj(%S9y)u2U_7x$`wWnY$JRUq>Ac9Q zvy*@VRy#om0cQ~WB?wEhVG5p@TD*g17WZ!U7iU)R0|W<#X?BpP#v}X69>q-NIp1J1 zdN|ws7grXIqOua-$YxAAhqO`$KnG`D!J0`rm$Vu@GyZs7-Sh8C_53nyoC<;=WWqps zhtivK{DeuQI|jmrZs6Y*Xu2JZjPp-Y$o7i}1#Tl1D`FXF3s)`F zmk{(xzqp=M1hEVXT@T>VJ@;$k=+034&W)|5MGKl=)&X_^g@HtR6r2DXlNQ-8amGR2 z1;jlWb>Kaqbf0_%xO|7B@84luKjmT3453kuk1~D$1fI_lHnTOt&q;x6hB_&!vKwn= z7iSA=mOD6Kd-dwowV8;}?6)<4vPvEh8%Z(#_5e}yf2yuNo@xwU`V_eH1=Z)1D%ZnI zbpzUl#QQabH#qBU8Su>g*9>NoA2=F`Q2{Gqli%;JXu&PtNaxL5jf`(XUt8w3%+KmY z*r;drtWK&3`*(FuItf1JiLCJ!5FXs`7h|d?JCBHU=igaxjr3?a4rf%DioJFM2NmB}c&YJ-FXo{vc3Zst z&H|b&pw5%R6y3O>uS!U$xqSFhr~}h$d2PnRVD(2)IJ9GIdQ)&ZafxOnN-4mn!`P)& zlk-2B@0qr#d`2dPaoZ{u5E@Lur83)kpDgVAHBkS*q{`1e z*)hugRFWa(DKm|yB$KZspwE}Xe3wt(nOR+Njg6I}_SXhzAvECFgh7zJh~noP0cz#7 zpV}Vc^va{InMoomCJnnG`ITriqt%PlQVHAD@+szt`Y`5TU{Ip`Nq>PNRRb+nJ^Wx- zorF1TZrO;@5E9A&h+EM|^L|h2O96UWU_pq`%A3OmeoM}w7M54K&0&TwEX{%HFQ7)X zHa$|eQiV!}K4|hz=D*^^9X3W1r$ob6qtWcj==B)Aekui#@h}z|>@3KWFy{dHZaZuA z^>{zL3ZhL%-|0Ut^Hsf=;yGXFIB|-@TJEQiY3fx=@PEbgC(!BA$f=S?jBE7!<1wZC z;}Sg1@5Ne)%}oB5y~b@dHzxZn9?ZWwu8?R8?k1hT?#2N~8+i8t&!+eMIXf58p5T^m z@WhhwIbn?Pw1Q^;M%$KI-t`ppzlS1_y@iD6R@Qf<8`0SYI&RlPurje78Z(FvdtemD zMpmgECQS;Qb6-Dpvb=&n;)vMlM#6VExd$N=pPI)=G9EMt_81$IRf5sB5@f}xqthY* z0TkIJ`v=Wc7Q3BP8b)VhXY4Zq=QZV3giHt{gcAe#8s$QAV%vJj%!U@oj^Nz^8j3hD z8Z)!dS(L`~?=oe$na>?k?yiqKen0y3Q8z&VEG^*^w2{JuMG%iB?Gu?{&+g=rnhlY9 z-^kn{e0UO#21i$95kSK*!wH0|DTboWX1m;`f#Qs`Lb2vkpnZ8GQX>W8@GEMidK^t~u zew7_?CJMaBPqYM-V|6TEExpZPy8f`);I*0hU{#cs^5vQ6j-rtEA8!rfZYz@dxm_(!k%ldHCR}0 zK9(r7j9ZSrW{v^s^I2otvl4w_C2qQ>&SSiTZ?~&~=u?=hq$EKMX3e!yVD=DL z`Y^H8=zT4@DHU!9yu%Mn9&eqz^Y5$#GwWzTSf%b#lDEt`{~aUSzoW5jA_Dy`+Hp0_ zMl*wFR`uEcU&H2c-l@sWNtus-i#VZM2-uE+WN3Tgw-B`KzYaARHH^CG|7#K55>_w% zs{_HO<6Zjh-2K0v(&GnK$$!qK{=L*2507DAM2Me&QWkhOh_M{a)A4 zLKKZwDMusdS?Z&n3q!2QHtzp4oleIeRb}4EaRs(i!ITI%oo)Bu2irVhhGGpX2bJ5a zS7>?(GxlQV;ETYhJMVsW|L-UqM_G`zpG`niR(A*7{%-SNKK_0(snI_p^OX|uam~xd z#zscwO|Fe;{eN0o9q~XzA7~2+wn^R^IVM90@$#BpcPnij?ff;>An`=n-*I#&=h7nk zW;eF(x1{laE=-0_{$7`f^uMEZF6TM3_k;!s`tr7pp8e;G{)})0YDbu%rl~GKRMk03 zHmkOzX)B%L?@-MKMroWjas_6ybe;K<{%4~xCh`Y3bPDb5@bK{dXZ~C6iGL@@ta1Ap zl~ZE2dxLB2f6mKHk33Hwaaw{SfYkS&n+_&5%e85oDgJvMH)hjkp8^v12>ZJ2@!$8X zBt`1NhHhE=2j(L3C5zjB!p>kNADdK51r~YUyibYG581~N%{+>kNgwtj|Z{_|vhB3MQ z`Kpo9t;@fU5-A^3W{S4nFq-^#9AI16W+?csk;O3qg4B$3zG{ZvY(4GCrk~@zJ=KZQ z?ZfFmMB6>?u-PO5qhrlDjY~Jg_g2k%k=_L-VKckAjXcAM^9HN-Uu{~~t!XfH_xot> zna>YVb_O(9-Cbm=<*_{bWn3qcs0f+gvmP{5h)cX1R~|8<0==#t{NppX@Lz9ZB(87! z1ScfF8U z)P%gZHdU0T@86r*7xg*hOg>GZ*6S;O!&j2yA}--L_C@o4wgQf51CQ0`la13e5>e4J znKNzG24%C$&59+D>S>#qZw7+Ll<7tOA1)l-hc6li_f<+qU6uzVe;5l%6s}*LcIbhQ ziz!nSu!vVpfF{jnq*oq21C|w?z^hzGA)m=a1h_QQRch~Fm)hE4~OQ_AaiY)2ySU< z-J*@txb|_y0B8J? zvRjKeu4+9Hb_r+b&c^Rq=s)#Y{??AzAe`B=KGSXW<(QTraSDdYUGPYxOs(^OJM9HE zeQr9%&RTd{wYL?j-olT=(Ft#8Tlqt47^2cAWa27Tz2{H(?bX|45k|}e4!D93SMGaD zlw$eq>DWn@M;Iv6G3Q?s!YFmqY+7mg^7K*|%HEd_pXV2GbopkK%r z8 z<}XPNd)*L19y>xk%zSabF6jKX8sX!FPDx5QovAHh(+*9muEZEA&@_HBH_4Db?viwV zbO#}??&g|H-FK<&NxBzE#uMMTfh^XtsBt?9-z)DB?>Q}U}%Vk`VBsA$dP z+i2~#N!jDwx`@L+AwTU3p5+9!v$T~jQlGcoq@~O4`{-HOatAWY%|L4>HPO|0%fRQ{ z6~z}rg#7fekm)*tca*)@TF|?s$#JENNL#u>z;IevfQjaONpfar$Q2az-^<1DgWh}a z63%Z7Y2kAF3`Dl52i?$#4kPsm1jkZ>Y3mFx>4#%u!k2}Ju98aV> zw>p!$tqzlqq8~8DD;i#t<#+GbFTM8$J@itnK=CsMWT?0I8IU_ci>^(d6_xC)?>ExN z3zpPqTvW0l~}ds9uUkBEJ;fDjT`GLE42st z{W=w5hNh=4o9}R$po+EEauSV_ladr*vv*S%6iORUZCzX{IJ)PC{;6x6a^3Nrf?-Me zwXZMtyh){TM*9sdpH*sQ=QQR7$+ZG3lVp0>WH ztL6>P-Ip6vLPM2z6uOeow2_SMq>Sy-^#qT38JWx$u1sqo=RZTWt7U0xF@pOf(^syZ*Xc?@(m-e6;nu^0dcpTQHqRb+Q(`zgw zVAYkm>D%8CBuXnSsG885;;q<2i3s85`B|1RKlB3)0jJB%oAQho76?C$s;63U6lat@ zIf{(R&)dy4daET|W!6C$-KfoxP7)OqM-J|+K*fKeQtsz~I){N0=SmO9A@MIgifLC@ z^Ur%~bffg0+U+ppXso1+Mp4{5F=|RF>HR0JpXMnAneyDq9+cL)^jCCle?5+Zq)G3s zNNW#^89$rap;TSYA8kFbTV<`2oB>dB&pa=yWp?_{sQiNja|qw0XGpW8h(OgLzHLQS zCiu~ma;F+~^H1S^7G9j*+5RKqU-^9~S688-35t803srQaJob%Zh0NjjF1tunt94XD z$Tm9(ozi|>m5HME@cHzk(pJo!4t-GWJB;;><|dz-fp z_-#TzF&KDAwWb-+7WVBt4o3@M88B-}JNLUqpUYm@8mnpZ;*>B;JmY=t6Z7NG?1u{n z{kjiZWReyuqUyKnTnaCtKrDYG>tC$(AYwi zy>%1Akz2sqtH+TLN_Q{l*>>Es>uygh&@_90|Gx4;AzRDdTYsGkiVB36f6Eb zGeoq~BQGzFln0dtTS*HG!gvIgH3#{dW{eFC)?7tE-$VDJ$a8m=9I=e|{@vcuDtXF- z(y;4O$csDHfZs2;mlxMn4%`C^A!3d(Hp%52KtgvF0S%c%j3nWp!5!!oK=0J$u)MZy zFN_T$--7>>1~DiU6ok!9ZHczOYniPEqCQvk@7I%)PyI9iq1smX4UQ5!Jb3F#WTk?!u6?(Y6B zp6~tVh6AiwthvrQj{PhLM~Ddd@r*&_tR=U?xbaYm3z%VIVqlyoj278e&46cmn}a{rgWU3m9j{~TwzySLqY zG)O*7P@&bcnamH4^BIn>?Qjy<@fHb^S9UUvm)ClzTWX)@a};ve?TD!9@#M4PhbZae zauuai9IoZhNPCUrC$llEceuvo=WPY2qyduV;AQ1Q&-;$51e zl$1R4XRf_yUJI}7p5x=g4|jKt4;xy7$U0)`)h6qyUS~&wH!t+=uab*=SE>(9nkx*8 zuxzkHexfAr_?MPBu5=%j)i+UfUv&pIlofFpbI4ZN%RAf>mo^Q?s7*RK$Hb#hz2t=C zWf1V_aO};^rzFA!L9)oP!}dMyFE4pW+i^G)np}6n$5|et7q<|B=8{@c%IWFCEwv%t z^N#Im=AGAWByS7Zr)E=y_6=P-!)qu~thXVZNDCe_2`1}7^?J_F8FQ=Wb!(~+c+oM4&iqTa2{i4XC#jV^a zYo$qXVd=4Jjn;22#?$tr4lGO--|c}$H&4gaPu~v;(f7`{Y;SlvJy@w0j5g1ebRDHV z9H1*sdat04bh0vYCR|6kFwayNkrXPVT(etIpzMPJedS-zcPVh|;VH-|ORFmursV$} zJmaW2!&Zy83119RWH}7<_9Ia4`CC*zlIr*m*i)8z&hBG9Me_5I$$5YNg-VNMeXTN% z<|J335E{rpP@3$u_C$#o^fHNO(d3F?K;y2xEm01J4uRw#)n7IP-(wApZ__I$Mn$u( zWqNv$GI=3BstLu(ZU9ox`g&f$>u^b+kp$MY4@Mw^PK=3(Nlvz?nHSIa1dLKl_-n(P z>+A2|L;!(uV`C#w56VP?2?C%3Wzflf{`~p-_wNjG(bQ&rvGf2G@&@zF0}mH>eRGr2 zAH3@J_SSiO+`2|hMdjlBe7jypTl?U|88{@s)Djp?y4UC#7{FKc) z$V?nfC_U1oE{!yRs$mFV5W3e=Q&U%L0srNbtcw#6SKfPieg6*Y)F*oL#?s0vjyl++ z3J5+=oKaCx7p$*FMn+E7x)~W5K=n)+l~-6;F>47df#0^Z=fP{bTO9P`b@j zkp|(K?R)qA8GujvP=0n7l-|v2xB2WfB_RPK^?g%Q(|&LtAML9(^t-&b!=n<5wTBQwv_fI$zkvT6hLX* zg&PJHAPOCkN2T}x`2-;}K$Q%G3r`W`S8v^}Togyaj%Tta%ff}H>k@hc+~VUC5WQHS zID8d`6GS7U3J2-ng82J{bcvX?uWTJk2OyDuK~9khE?R38*llaLV5zr6%C z$G2^mHt`zr2PN`&h@QYFi(UUDzJDu@i1ZY(zahSyXQ32gvA-MD5xt>u_|!6C>_N8K zEmaO@5i@$o%kqa4XqD>6$97KUJHN8ki45+WI!0V9Jp}6F!pXKwtru~0ziW42(n=}5 zdH+=5#iUjCqqb?IBkiJmmnqpxW#c9Rxgj}8r;*qom>`u_GD!&x#-hWh`+LnvQP{=g zmDk{;sC(7RAwlPcdG4cgAUzf$iXe4afnz3RN^<$`=+GWk*i5uz5sY3LH`MTB>Rrul z@5E#7(Z_&WG?SvfL=Lwum1U4obxPH{u;ZgZC^pagn^##2CUCm05$(8~_c4^^pyGf5 z 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�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

5n zq>Nlh(GH{i8Qqr=c|z86LNZ{B|khK0kSWlz`Z${Shq$B+)Xhhcn7wDG%{*WGB>H_2;2 z(uE#EB}V}qFTTzsZi8lU8c;Jnj8rooz0F5^dx_|dx_Qp0V|925?%Zd{Q%iDL|LQ{M zqmk}T^9CBKSMMmO1KNCgX|>9*sJ83`upo-V@f`^_S4-CR^=biCh5X#J>nv`#2Wq6< z1M-Ai5-{(Q_Z!M$DqqrBNtoBT&7E8>^T#OY6<1JoLQ=J!I#DmZLf77JO1)lL^Hz2; zPR{f~Og9Cj?!g1h5;4yqYy+NXvQ}Gbzy!YnUFOj4hR`3b^~#y!D>z3#K;@`5WVXqU z%0^&XY*+7VMRWT4&Odpz^cZ0p2T%5{*5zHL-3j%Gc{a-XX3GSNNTzVMM(*Nid&C;fn&6}g%ow|tv^(xw?%$%bp>T6)N!7u^$lPN0Q)uyLcg>Vq; zZF)&*pRWdY3lANVdVicrNaA2@s?YN*FF{E4tFIm!0Qxl7S42xo} zm@jH5N}6bRQ2xH2#~QK74UIi@rlWGK&bnV#W-wqtkB7c04HGWUCZ4fY$jKu!9`%$0nOo;h+PS+{(K+nQ&rKg?4C0s}d-tOJ%U zGslnZ&6^x!F2DsrLDC);#yMoQkZ6B8yWgItv)@WD7VkO^bPsVJDjTidixkCHz~YW) z#ORP7nB*dM_MeO~kB?1@JrR$x4G&U*N&oThpN|3~jeMN@{r+?$fHz1g)uM{|{buS_ zk{U!i61P5ci0U2g+E#d`(bMw_skM7;R)pY1jZ|eC^Y)%YwmdFr4uv5pBC*7Lmxbmy znrYt0_`_Ro1sKkB**+o4dZ_tOqhAZ`C#a>Yz|^tZ|1bDRmev=rsHh*6bI3JDRmOxV zHj)FT=0$%?%ZCBxrGxSO#Y2j4A!L0qM-z{>zJI@uk%YveqWAHiH6IBw(0zON^4HQ* z6Mq-&8t|i+=eVXmOxCjHQ_B5Cm892ebYt?OmGlczuGz3*c!CjDBYIJlxu=SXxQ_HM ziDxbldDA8PA#z3L;Lo|IJ zRXe-iVq}fyTE%K)Id|{ek#vz-?+Y@GU3jI$A@4&@7vpxQd9Q< zqA0I8GyOOf1pW+LZ(+sB4+jcxPDeK@h;d-OyD9ieOA8YHALuRA> z@Ae78oBjyyK2VIBUv*rHq>4@NruhaKX*E?Ozk(fubKKr`mCMbP9BwuD;w13DVJO|jo;**@Zm zUUi3%TwEBDSo-G{`W~ae;s1m4M}7UYsZ$eNyQm(}SMuQu4PCOmvVISg*hPS^ zb{k%%qeiU*#DrW-(;||6rc*CeByWC$EvU;l&oBJ{Y>?(^! zdjk~g-W~hfYWXYAuBji}+qpE9lO*n#={1O4vxAs(pr($I-bnd=VSFOaMzt;J>5dMc z9s6^$eh%)C7dl3GCQwiRLSvxxY1&w@ zG5%6;rd-O-YCbu!S>#S|^UD$DIdg9xRllDe4Q->P6F@2$3g%91fA?;q0qE7rIdl4D z-a`?i)3-0sv9z>wbZ+e8qK!)1w31vVfEw=K zb*J%$XW;kltN-2dZaes6a66w$v>|z)&{ZB_a#SWc# zR{F{HzDc>o#o-JF@*b-9Ze69_pG0-I&FHr1nPcAMKYp7nB|a4~!h-O_&#rGSqvdh25B!`ePfb70mSO2d za3}Nm`|8%rHPJ}5n|S>AasFmd^|&HgwUi9x6jzouCKURbga-RRA~OL>Hc^Vtkt{Nx zegJY)(SxB13g*%J=`}&`-nO_cp#G5S~k)xXcUi zrtjQY1U4qXo6-_u+qUa}e{XR^q*wLfIc6nkHgQ>*z^$x$gzN<+{gu#iXqjopk zjRVW!!vkUzJ32dKddoE?qJ7yPQ@;?4ad?|GK^DUNYWx%5^?k3p3?1X_jK$#Xd-vSd zXnZ<^_}63O#gC=U{qJA&B#Nc((4QOK_oCCRS}pF{&OIJ$Tj5``VIiX}}-}d_yq-?SBRejxcXC|Yi1P?t2~Z9=@f0p@F>V?4@c$Z4~qsYaa1#ml^QWT+bGtq z`{z%DvlMrkX=%|BX+BR z>&)uUfn7;>M895k9@6RhSuWnt6ryAAEwn=FRntG)Gj}5Ddof<`5Ue5bs`uY_O3o`~!k%&{s`cq}^x+#2WsOwVE{jYCniw6b z^y}A%VyfZ`SeS6<|Ni&Y-F+!n{#E?J7>kq^L=qweuG39N$1028&veRmSYQ{j{&r5e z{G&*@=;wc+%hAB1HUItf+|{^M_b9II?GaoyaZTu?l&_(Wq1(V8T~S>STOf1n3`c}R zch>Uu0M1pZ4K%fa=Pz7(C}S&EtN`qVNk_&YT}DL%hbp=eLV}J!)ig6R8#BwCp9DeU! zTik!HPlXp`nlYyRoor8y9hJ}U2y%CqPAD5VXwZvLYx+os_vsV!UHa?hjO>Wg%U^^D zc9t4OBXK@O=w~Q9lrQWOoHV%eIJ7nmI3N27ZedJQSjP+wB$63ILlrC9IX`8krFV{6 zYqeT_JE2JePm36N$j|??SVUhyhzQun!6{M_5|;=-VY%_gy#{4eS5bE%lMB&Qtdf_O z7SS%`B>m6cW(1x`s}t%M&b}IX87m8Nv*;2aDK<3;9F`9s`nTI-!BAMJGH~mVOC3(g zGH_cyc5K61Mp%5kkeZy);5^o6=+q9DR3BgWv(~oKzUA_1-(w&|#!nN^1L&~xM@+Qw zJ2U-9gk3>@Z}selA;d_ewLO0HNU-SQ%6^8}VdTi47?9dU{kgPf&l8keC8gT7Bb_!( zKd!b$>3g%dLdvX47x@IFVawpXK~op>8G{BL!Th4E*~QHbaXi$BqnMOezMPE@4g7w{ z)}%KHh*VV-^nk0RSafHVy!&OV!3=-FO2b_ZW!(WH*b~C+ZcV9wqh*B1RaNN6CZ(574r^$ZR>M5hxM zNW5#@mT74i_{no6eEali{Ww##6g+a|uOF9`RYNHGk802F-;{nh?EeC-YE0<(_BeB^ zY4QZcBdQh*3&xK9DU`8rjhQIX_QRRxn=AU|oC-{Ixr$Q>KP*o!5d6nboak9&9yC8X zujD?u?I`%}7!TvuK*iYpVK7gY^7t`xPGn7#X41)=y(rX~a%R7ixvV%aS?rNjh&+fp zo@sL)@T-BrVAn$WF$ByZM`7sDr5vDKba``AQ&QHx+VwBn_Tl~eBpcCD?%BJSTplV) zuAcX6Z8sh%DeH}iQ` z<-v*VOCmpO$tO(W0-7-+<-owfgYi}D2r&6F+udeupDf8F~6EL_>TvoHSHx(_cZz|8FH<`wzd~N@FvT#+;vXI3g zAwX9?Iun(FAUmuDo9((*? zYxoQaCi|c22Xn33n>P>B@y+`5>GS7otC4+&;MLCPC^+Q6qv(c@_BuoBm)dB2sqpHJ zs{+UxoZ5VDXQ&kDZhLo?sK@+MG-3Q{8Fx{~KxW*7c5DE>148M?;d5YV z`~2dnA%h1mqC)330Ow-h1Dvjp%7exk&pS#1dmS{6E)}@7#VJ2aRd5r3U7hU=Z}=VVv$$J%ow8tPBo*6x;f-UB&L>63Dt#Yk2x+)kfD66Olaxt|Od~{`+uF%o%ds=d9+si@o={5rGgV;o!mJ@+a@N zTi0-C_M-m?{Z!^17G#WZGtgtx%K4SO@Bt5GRD;Q=?q)|U)(I$m(dTM(FwneT?Dkhf9uwk*RSzuAWifMXFXiXKC{EK0o>I$|3{P_ z7<7FNwCLI>5a+_nmocsXh)6mQmI%P5j0~s4g~;DF9^-L`UqKz>XNasB2TEQL^f@8B z3Nhl-rx9q{geXys+R_QyhbD zb9dFPVa`SYir?t*FP{A$@DCBv^}(MSnl-kRx0sO^6jWDM%AP6clG;@pB-M0v;jbe3 zRca}$w+>ohfL@tpk4Z}4KBpGnd!wyIQKO?@TWVp{)0mlZbuwR#W^KAB4C$F4j2N++ z{73g>hU6A@C9gj>FE?Bw;Ug1iIOV>6dd2f6PX1d2+Y~|12J;k-mpz?5p)3xowe2Hq zp)6y#B?#aKs-Fp@P&Hl$z6q{|?Ep(!b*J?wk6SW1)i-E&6W^%x>danq_eNm+ z^v#u8oLM?LTS*{X;ML1)WNEZoR$J3n(VaM}Izv=NKXOednA!*%7m zx|njCEx}^%5~1hA^P{H9o#=7%h3=X6T%o0byYIZ>VF^nlNGk5)S-3^qg_9?DAqRDy z!x>c%2nVO+lO;08mq@I*tvPP8E^2NY;QxhA5hfHCn9#t!yqO|V^u9BIvLF7#ph1JM z6xkQlRwa_oCD`_1{j;|Ac8Fh1a9SPiC7y}`kDGoRQ|@{Q_op{)GemJ6QvuV;&v|(0T&ee|AaZiH9L{ z+S>=uh@W%&$;N#121v_v3tetmW@l^yb#dVUvnats7 zpf=xx`+f=7l2{GSQz{rr7>~K{I5v0_j@#$y#60Zg4FrXQSTL|xL&AteLl%W{z4KT;~8Y_(Z)&@k62 zBNPuYAv$<4fP!qpbIOa6Ha1k*4!XtMnSw3ie=6D@yH=nx@v^j5SU(&n$XE&di_#9L zPwBRQK{J|uhC@8Y)qEH|m(VT@sm~>qH4{(Aumq(5mdA0QwMe*8Mu`<|z%RnW zA_CG9x6-}J%CdMsELs$Nr`GG?TSsQNXEN z5AolILl`>R%74LJ*AW`5sLr(BU^L|;xvGV zjkU*tn(vI$h+)I<3Ir)Rr*#>iN>n6oC+|){m>^K#-pBl#Ff^pE{_Q1e>mUsA)ig9b zQm;!<+JF7hVpm-bt&m>n z1OHPZk`_44zUICDp=?+FHMDh-z_-I`8&eD;1%ddv6|{ph9K;3w>oKTvOCNDT5-w>Y zujFc;2n)~g>w*3YSa}TN5`&GOY#KIq<_0R3%j`~)%5UGg<=80^TS~-`SnaT|sqr^O zr#|;Hi}?Hd8=PCi-h<&58LKKM3~BQ*RIb12E;4U^p{MZ@?mB6&ce-I*b}_BD`HQ6q zpUV5pK|oAhibqlQ-(;wn410Ft`oEr{&H0GFS|kVG!KV!icQe|GsCo5mq18`Di$FiX z$G{}j+T-_Hr|;V3;qb=mcl41gZdt!4RI&`JxvR#R$mh-F5F!L^7m2W3&nYQmSFax7 zKNhMysagU+2oOi>AF4?61m4~BRery2muQlUdE`q8;{k&PyRH>iVPOMM~R#Zgd@3`@7@f(i%=w;VhNwSABzD9(Zp*hwD`= z%8}2qt^ge{`$g=)Z)<9lT^G^OhL-mkAu+BU=gQ7@cpaHQjXYzVHd zsYwSmntxmMlPJYYYehO1cVev#%(~3f!E^P-87D)i=1i+Uzx?o+T{L*_D!w4v!zvV-O^ob8UZ)v5V2ii|-#PSu`wqau&ucWgQo+uUmsc(b=Uz(UC`+~8U7-?g`-R}0I}fym7D%w)mz6C@7pSkCtP(wYXRcbd?DTmPuNVQCR8sQx^|cn9GVG#HT1Od`s3 z>XcMh@v~^5vB4UooP-C{oN3eOiqLB?G_xg05>&xUm2yCMpVV?r=DbTw3kx!#zo1d2 z>mI`(>Qk;@0MuiidRTTLaO;KY^H|>d?y3ob2fEK{k>;*{VNHVARfQzyaFeqYoydqU zEAyDFaJ(O_X|p-g^(Xk@am7P}HowNuU3ZM7#s7O+NOzM@KI8Z! zzW&_dp;@4~XydAvi#%v;X;D#C4baKIvux?od@z4HZD|QGVPP`BuV&Q44@tu~v>={= zWft$u|4i`etA5&h_vTvxn4UjFc5$I+jpMlViycck&kyrj*jDYPfp-~W`M%1^I6h6e zdEV?(5<(h6J>bewgGIkwH%B(%Pl$Fjm)vM?@U-+>{gxZ3%Z2y*({mzAw#`0?``zr>v$J3#M<>r~T4BD+Q!Mum2TDXllcvK7(XPH=W=|8mw?1vo{tO4P zvvcXnm9QT5C7%@sLlWjR;MN$EOC&ETG*9l`hPH}y*nDO|BTN%;i9jht*x!DUJ#{fJ(e>bw|5#r&;Gna=fmSdT)Z*h z0nW!-gN=>k(4PfKW#*tl1k>y@07@WpR3PwLeqwKwWpFnLQ(#lmG! z#u0Kuj~^drx?`)^9xB5y;$5EceV>w4NJ>}Gf`UM7Z=$n5W=)B|7(ZxM6$`ll(rA)$Yd7Y0 zk`fY$xQ~HBSC`AHD>F^85m*!)tf#ners-q)OrK zBosG1LiWUzVn2a4GW*}p8c8n7s;aaIaRGUJis+WDzg^Nw8XN6(b))aSHkcG0A!X>| zJ(>2@YtEauZy%CtvEwnO1dy53vk2ont-~iRntuGZPs{Es^~V02rR+3$T>NYhXUFM2 z6YcUm)HBx#fdo@MLWFZCazd!cCutf~sdV78i?(7dKzQ2?y2HxKqmW1L1iHDn;M~iD zr2$3=ver_y3RnF-WzBv|+JomWw9>i=LCO0HnzUhhX)kEIxGL1f)CufbnGD0&C19lo zQ79Kq+`*MC)WgD3*pht>|kd+*4gZ4R@ zt_J{dHzeElLwozF^Zre?p8baz8cK8xv$e(lhcbNODPL*HO-5l9(jPCr*6odGm{WpD z?0-=#(pItu!#`PUTlC}SPd+jKvBgO#UG#S~XTY^XHuKo!`3Mh%2M?UDSyd0qHo}*@ z*_T|dl=5pUA>kti@U|3nxR7c;;f2wL-J-^3>F%{PH8Il{W}S5SR(R`6*{XK+fzo>f z^;*W5Z3pTv$#<)7g8}xC_^Mw$d%U!E$0BKjwY9aGnOC+KSLro>L;6=aPD$!s02~f9 z9%b0r9@;S4#Mhp9{1y!hGEv3gg|8f*_L!I-M(G@;CDzGBC?zS$c8)1;w(&AJ(JMzL zT0y+nA%F7yvir4(ay0ZJ|HsvP2lUwX|Nl`cp(RO*mV|7Qq>?0rGDF(3ijYyLG*n7c zi1v`JBnl~|l38ZsLJMVp`I)3*bM58V+&`}XgjsyN_c-)~j?2A|T8@883pXUcA8mxTRTVWH52 z*+bXGoDSi%5lSZJ%;?rZcW4?vVKcP16U@lG^h2|M#2dS)Vj1|s3*;sb9X)o`D8|+6 zRc->AM-7UM6|BinvSBeqWZs z$Y|6K%5-q6v>YpLwJX!$)91~npZ?GA$;}j-bHSnZ^Jk>ue%2PJT5BgKHz$OXg6^>S z2gEQAeaRdMe=|1R0f{b8Okk}aJ2p7HO!!jLH{z!a9uOa7_jaJBnIeWJ*D{ubY$FoJVv9*GunACb;{4>1&R`#wR z?Z01QlI9=LOt;*rrQe!V?EL!m`ySzCZyz~pUXu4%N~?FWHv1Q^6~E*r<*P zb~f`~jwgp_m~Y)>3Gr@Lt>%?!sa^NBjNNH<&aM3bb}lCEgwo^d*dlBA-Kcz8xziI3fLue8n9acpZ} zhe1Dn+orir+HiQL>i&6$ByNAt@z9R7xHfXNVdfOihTo)I+*QnHPnO*9*)}u(ce5d+VE+;{x1e7?=%B_tkUr!|il`}bc6wxY?F$+YX1LKwP4 z(szX2*`ysu^cUYX-ERkm`qOYfx#pybEc@*Cea9d8i^p4Tm8iTGG|oO-%_ZQEhF7-g&&j*~etzQpQszC`U2OCC%KC*yr`2t5|G7MU|E4K^kxGAb<%dXA(s~j` z!N@3X5}@&3v?H`o-rrBYsgP;9n@~L4DYdV3@bnL(s@%4pm)oJaz*JIGe`#ps=4u4oT;4Vy6j@W8mug@xKv+;Y!GbXz~y!q^ql-r>((^Z zQF(|*pP4eX33pYmHrtkMJ#OyI2BWz2aLy(z4JN;-YCA2aumqt&{)U;MUr^WrqxCCX0^lkL*~JmlwTud#k6FU#d>IfD+ zOm|}6%c?rR;wLJ8X*%tk@bKcI4WZYq;y&p<4s_o5SkG0-BkXJE=`p>hDST9IpP&42 z^ry$KoX0v2F_cP@OK6nygZ?1#&;L{6Tmi7xAC3>2T+rSjp9)eo1)HZE14bc-Up);A0Z z+$!Bm@915Hxu)Seu?-lPITy9p9D68cl%9IAGE)6&V#Xh@FwZrgS4BT8 zGtbB&x4Rc#r%?@w5k4Yuw+=-0gI=-81Lmn9^(RO4U2MinDf|98~>% zz|gftGN1biPtmzGbj$gc`d{sNolSH1zH{!;I&G>Wl^jyn7MrL3{$Ffk06aBLo1u{d z!~`PFf4<}e`#LW-dJXB$2~}5fUG&gqXV&^HsR;&CPuw<3m}kDIdt+g3Onkh+>=R`u zrs!E#qy)I=oP^X7HqN5Srq;#x;gr(I`uI%Ta7c*$T~XOv3%y%9hJL9_ z+FD~Z&}Yngj>&&D#-q~;$ECkiSj{!Hu`j;p8l-gpTIYaoe-Oxe5xrGA@?G?7qF)3` zi@lznWTsm6Q(B-p>xVR&i{NHjdomM@;#UyHG@wHz0a?;*X+!^$U4*DUL~V^4m_yt$d9 z{z}cinCPietOmOTQbF8tg#7|IYSWdTS4$I=q8+!JhxI%ao;pONqWrZ~A6;$GNTa4d zTXVYW631$3Vf^9;Q<<;ZcFkYE!V;;%P+2qm`F^oUV`eRqQi|!65pxu|Jax>N&Cm2# zY|>C|1T$p^jNVk=bX@S`^;TxFIP@Jjbx-OWyBV(R)dy(`~yoPW-!8zOCnC zFNI!51}-|W-0+o3$=$}#;1wc^|My9nI!1{qPuY0>v5{xk^0^%a&Pky|*AC>Jz=siO ztZ{P3fmc_1zV@HHp~Y6exh~7`aMp+Gnq~j{-MYGe*BjmYEv4wwmHszc zgYVuR-v9fSM#D`Nx0nBYwWITmzS3>qIDOy$T~fzlg)q0q9`z>!RfH7}pMvyZXTYTA zw$R6(tzkWdhZXx(wHofL`FHoq+q_-P_5O1$OLObmeS7G_|GVxzJ`>BcdZs#VPaOI0 zY8t(tb`%spcXf*x>6+iu;`mME|NZwn$K7-P->3a|%l_{n3U17dvb2z0Jfi=*tij>) z0=xhB+5X>u`~Uro=*FHGD$2)84=sHCOPK2m)%*Kg@>kaNFy_x)iKc(Xbml({4=d;j zG;A_-)qP#kCsAW~@wc(^_x*=Cd@pSKH84EA;H`JtnQE7>1)mP24*Mwe-#E?a;KyK$ zJyipCyGeQ1ZTW2#7uXf9z2>j#!0rQa?=uXJbdPh58@FU&)I!NEEmAJm=De$`6B*Vq zNA7c1t>ODy4qdlSb~-NncI&a~vNpS|u@%WZ5rLuJmw)Xql8SpT{GabO(&(qXz4g@1 zEg@`aoyoPkrsra%vpsz_n$N%3b0zrCd#w~569WUk{h4dK9HRSGNr?mFF78otgabn1YvT`0Z7ANpUt-}dp{U7J4< zuXZ(cHnvU+DM?v!2HIP6xl+z7-{+e!|Hm3(UtPxW((oLsd%ebVWXPZSwJxDY@!E55 zU!CspJ99Cj%dQ5i087bN)(E&6N*%t9t=C7`g6Q zR&KK^YHBy-Y}Zu+{5M<~2)_&amH7WkR|S+f;09W?i^TXWtIU0P4nCX~Yc4m3e3z7c zp@ul@!+Vc4Z&Z@-(7vAOaJIjw-~G3Gw~X>_wq{w~b!A43QrIe)G zcg2OHzd#O_vl-I}KFz%X;UWWi?^o$(grj`qu6Bxgc5}>3YZX z@Vcb1(e@*!z}Jo)8C`y7hxlIZKnH^??q$jeZW%Y&aLh^d!suChq^=JA(d&!Y+bZYb zMv4BTHP8NN{8%+_XuzGerO2P_3QeYFteI3L?=tGOgZiD|iaP@fpS$jp_~FqY{LI9C zzW%;vhLL_ZzUr5+cUzjdYp>+I;kl!7i+;t6?)rA?yVB1kJrN;Bhno*OB|N+1@y<%a z^UX1|u^TchR>14Kv7^;3*g)r&rq2v|k+*1AJw|aj(u-$6OF8d#wq>SzyHPI`*k<8gig&|ka~8#;fpNW0q$~F%jvi9gF~a6O{U7vO_S}ItTA=ndyC7}%lAb^jQf*Sv31)8 zahXLM7PM)(DCPw>&e+r}FQPr)dWZEC=~7)K(UXpsb`EUFN)$aWYN0z_uGB20Nw;>6 z_W5m}&h*`A5Vl57N~d(Uu){AHVOQ~pC3rkL`uwbHQ38Vv~ou3z?|H*c2%zW$xW#r{`^!3S) zq6c$+6gNacp{lmFpM3k**IR2LH^O#3d_6oTEj|71yLS>IzT8=^@?S|Y+wzFH=8lNF zs5(&pzWeyG_RSl7aLFFRmMv|`#@)NGJ$w&Fx%$GvHpd<2+FbSb*Vax~L`=f>B-kP`OiXKQ?dp$wyMwa}J_bSM)e*P9Fn zWW`2LH7Bw;?8T4a<2AGt~9i7KuWX2{zehkoak2;^UFdQnAULHtgle(wAF$ zY0f(ZdlJ-8Co!SOaf%}D0F0}QR4mRtclWy&X+J5JPnBf_kNvuFL*2l-Q0DLqu5R5z z@Y;64a5dgc|r5!rcE1v`HUq5~f z@z|bcooBsBUnR^%3U_q;N1bbN2WcQTeho(b#@<GKlbnz+Wzd0;a^`I| zUqbU;L&IfQRTx!bd|bBpsJ47$Xy5!J;{*{us_t;M7WqFaaQ+;T0c8QIJxm)&QGoey zjZ%kWCTzZF5upAe(r}=;Em**53>_hCvJvj8ehV~YnQP8E2ZgiAP1mu!E77ywG<71$ zFG)#!gI>zLnoGnLjGTHEm&YY%IQ*r4%PUPxe2i^GXh?`(md=M%UVgR_b93!cqaZ9Z zALg}EQ%iwb36`oEy)mbTwEQ4_Im}`~uotzu@j*LPHO%dQ#OWA^Y z7vB*$#$*8+scZm#j?@3)IrF?1Bdh50G)y0g+5*LuJK~W6T4`Th*_sqx=g42)l*4$F z31Rh~Va;1D&plVe{$0*kQO_%>?i}g6)zz^E1I8U&pro=VWKl+A+a9Y+I5lhGZ2e^L z<_QYm;rbeE6?^tnIZsR4PASW%XIS`L?^}LLck)vM z{@L8CnIUkq%r&vQWTJ=H^#?&>QFvMO{kN?-({<-DFTz2NZ9gGl*kRMjn{qAFm&oK} z#mb~rd~^4e{<>DGN^Dm3=L^%3S9shqA%Ho%$MmN6aVaLL0Cql8R>_xrwK7+G-_^0j;?+2LHOVhm~hC$LWH99{PgT< zGIz6!occ94YHlQ(Y)__Nmu!3OoV zM>IqC4;B|$tlwtB?Zeg$nuO4=fq})V$g^0n;^@C{4Q}{(xg=D^EC>>xz{%kv>b#aDN287feyT+p`nd-^xl$ z;n=Ub^pZUnV#Awh=S6OlE>VEw1FZynHn`)wobYXiLEeM>f?_(m9lLE9t1$8yYrcQ~ z{*raxH<$%T$0QXKSps--f#R($VZm8wo=ut!yWcI!gpklf!>8r zNBt4iN7(fYjL3HnVqx~5J#&T=4B^lHhYz8h%*9z@e`0^N#fuhsfrW7uSiS<#@Gg_e zxoq`nPqg0?tes+G%};R*viI@^G8LNL1UiD_dFp|(Lx*;MPwctLpHZ~HHbfuFM?k{Cx?+5N(Dw$Wy69#gG!{NM)xB?k zsNbbaUj-VNhV+sNiihzqKYR8AG@pQjSO^JX)+tP&d|>ywtYuF0~1_aFP zY=Rq`puep_gg7@}xQ*XgW7tA)Y!lB)mfUPH;gwZJH0xuDGc*nDc8XYt#LSW zH#Pl$UB!8ave*kw=Y;J_`YS#O+)-a#TsuZuoGcp)Es8ZxCC5pzEPfm-;9No}Kp+as z0~#312MI$!D3ZP^f>8LpXWsdp{0r&x=I*bE4hC+QrjUn$jd}!0JrwC%1mTM1$`A;c z@syt@@b%=VL$NA|0>wgVj-|>c0VgaY18o)QYe;`#iorTX(l)DBAcI77%9A(KAk>~U zOl~e59l`PVY2@+66r-HP446@#Zin2_Dw)_Hg zx2vz5PD}G2w$<;z9p~&{pKEs`q@aDL2EfP@x;6!!-|dR0Pj{g7#H}Urj;Y3y^VWHj z3=Cew6uJNQ?DG$B>AZ=FHhXzFZ&a=iX{vj$P{neE`YB~j2(Nh1dZqaxl^f)OO@>3i zgiAin{P3?l|vlGpDzfqHn6eb@}!q)HI066FO|&@@S}rd3D%>bWBm~ z+~);f5>Y+_?TS(knr!EXh~8S@ltby0>YET5xvjPCHgd;59|tK|G_sCXp0{elAsCyB zH!EY(rCJZ5ZX=Fq?ae(hVKKw^_Z9z6@{SW;*@(TR;!gS+y;Kj^SCb9i^5)j77cX>1 zf9<{SRC>DW>zkc;`4TyW0!$?JH0SH3OT!fujvMUnnk_Zc{dY$jZ1mdi_T38){>?2(j?i9;Z30t z=GWnC!ehl52Er8DMyxJqI!Hw(Q5Lx|Zy39upBYU}NXUK80kXTG6R{=3FIU!I739OC z$2RkJZNL1W;(9@R9;W=TnsW2>;>W40J93z|b|Vhv8u?AHZtXw$>>WhMORIB6YiXIO zNOKe-0o_5zX4hCX;7|&dF%iS?%VN7Db?L3{8A^Y_1#4;D-7v)=TBe3T*KQhf6E&HW z56NI!N-h)R1tXP;4O1-4FuJ_-wB^$fl|4wp>(MDeTmdua*!LG*P=g%kapVu{pnl%hNjA~k$PEYB>X;1sM|UVJjeth9mPF$18UA(qU45)J!b#ZLi9`3K>Q196l@v9nX*sdhIJ#+0jMI zm*W?`pB~j)w@mX+HX>vSMBY{#oJ=(}osj`j>5yqjGW^W&GAeC;cfsGEmnhj*URXT{ zZ~KG<8)DQ?hdgw~MlT=gEF0^iqa4L7$i5=JeIJT72poEWNd`*F>#(;ya#61wvcMNyvHe5E1Dik9c88if zQGQFTtOR^(4q!oa#%T9aTNfjv!pC0ilLL%tLwV#YMNslZHa>1gVk?+7Nj6**b6J3 zdUKBM$yL88Wxtpp619F#(W;dRHbzdu61{4xPDw`Y>!0%I)_j?QjDniK!f6V_dsNND z!@iDuPyn#Q#S)v*ZP%}_U5W^4oIQLSk@UpXyK@ywN+*a@?SYMc-Bx*Qrg@kLh6K4sze zbB>1pkoPR?=`|tG%?5)JXsvyeQe~z7)j(#!2Hxaw7vS)xKHS6plAEz->DVEjcrDp#gN@#!s4VhMG14O$mtTuY>8&I* zX?ca{1mC+Ct{hYA7&t|0(gDj~{)&gQB4!WSY3QmmU~;cE^C^Qx|66lr84=Og4px;N zDpzb9*L=xjYqtHnAG5P;!)6Bi+SXgxZ3TkguGP`MYDU!Mc@F*dPXoxmS^lHEpGfaB z%W5hre4OC+{%B4OAGi7n2BSOO-AT{H5#spq=7t92X;RG3(TJ2H3Qev_Mu5} zDyus&Tca|%gxuWa%a+N>$@xz;aNDlsaF~~rt8CbwM_=adYs&_~p2)ZirwkMjHZ}>A zpows?LE^6GO?|N(pqFg`@oe*_Pe;m~?iS(I0TFC(kfb`4Dr7BU23 zA0HpQe~Ht<4%u!X

f7kWBPhkkTL*Fe`>Hh+1jOKy$_Qg0MdXM&A*WVeZ@K9pQs) zIlLz<3O04?ansoM_+aCQ6MT;-eii$PFUzJ5|J?v@P*g9`} z>gV_KERV1jk}}pmsNS$8G;(zo2QCPoUsmYo;h!_?FP-;27g4lp?#0mOoBG)H3GvhP z*)y_!vWpHg3bc-bXmiAIKhZfCrn{c%^}JunsnoM)bZWF?69zohG|id$PPG1z$&2tq zM~x1ftzq>o8x5z~?~`wemNsgWzh^QMY6`r2Fq3Hx$w?d;ndU!e_PkU5l+{X1<3~?D zVX(M;llD8ut=lTR837EEC=sMJ#4kFA9SWMbl&j(T!&fat5Q2!|ifL*mo)nkxBLDAp z-)*baQrq0rQa|TxxxDt7!rGZGi(c3WU$Ojdx?INczsX0_9aBx8w1`Q|yFDQhfJ%69 zpEbH&mV+mqb)KUYw_*Q}$8}@b|EV4YrbwnT;yaC7C*83EHtl^QyT0D(M(aGNPUEJU zyEMKWm2qUr6P0?AeFA>=O_S z(4EOk1W*$87gg8M(fObA@w&$jS=bC2BE%z2THSru*VhaEAE8eK0d!DzCx78+sJZL) zygP5VkB}_~5VW}znEex(GkL@jn(fV<#N6!V%V}C#rwa;(`--0I|7-a1#j|GhDw4G@ z^Y!zC<%v<#)%))z8K0PKEgKh2X;NBJg1P)IcXvou$U@m`fHLz*-_RMN;W$7+t|RTR z_`xa;|NX?+TQtp_Fro16c5w9xk5MQ4OM_C=A@Nt}m$0Y6rGq0(c;))+YK92v>Wy4E zz!|}4q!Rw)TEiA-g6t+aEFVAe%j*_6J2TQvGS~dx+KRi@ww*iw5L>xO%Ch388iu6( zdJoa(=mMa3Qv^6;XG7FQ>-ExRFQ&wCcfMvlMh1c4irNJFFe^FYs%O4{&q%k-CE>zA z%>X3IzYdfLpfC?m!C@LT`c|r_jT`IR1Q-*0=dbVIyYJin^^Fjnnj!=sMz;X$tEA^{ zrWvjJYS>Lc#V5}v^O!HC>GRYBmSf{7{0&%trm2xudeliMVmvsoT|?h?cMT|Y zc&GDwz%I2XoqcVm#+u{r%BBLafvWLzQhLEF7ajI37_6XvgXQIsE65&o+b7*OWO(q^ zgS%y>R{agpvALIZwfC>VD<*77Ieb4vF>45i^ftG~+zX~Nb1V&w+GGE&%(sy`d$m(v zC;H1(wM`?Hq8x(_yH_G}<*09PPQGy==!HaiknBLU!CoqVYfZ`uis~h0&{5yoP^s4J zcDgCW)T{|%I{gq8A=%hDR@xLE05KamWiaCBKU;NUPz~bbL%6c{FJqv;3 zWcS<|QM4`fveXt6!M{x@TKH_k6r-2}bta#b@A|aM=~~|Wwk^TOqqn0;##3f9=8k*z zD7eHwen0SnVh_cXisjy#!eEdDKxd;X0Jwl1_Rzi=KjSX!m5wvSsP}HP+T~Cr+$hu|h{z_jpuP%FBcV;Y{p7Xr>;MN~c%^E?_p<5gJC-6%_=7z3)2kcPCQbS#a1x?$M@?za z*);G-Zqa_xk-f*M=dH`N271U_a^~jVUl)-s?$|Mg0EjGyIm}=IBG#>Y+Y#AcUw_&( z1UcX7mAuIf0xK(VXak#!09Q&(^v0B;<30{7c-euH?*7&^Pv@in2F2MT_w;E&Ty|lG zKvv$g-2-vmq71whiS-izPc}BM*u`&!n$!Eof{DE&zXn>1e={o`CXnb1Hl(Dx2GQT1pfo^Ad`e37S-r- zM6l5j0?%rAxVOm-CA$?sxv5EHY|w*VYRv5bwNmHD2OTH*bl6N!xx_omYY^0<8>BWx z>&%XXR}TKH>$>y>ND3AJ*z8im7VYXSF$-`DD(~V)X6Z|Y$mKTw^vtsk3lWtK_%HcQ zpSzw9o+?NVIwATYK3Vy|F-5l;64jFL4BAwzb}iwqg{Q(+RT&qx8=CuKF4k3^yBk~{ zI6E#OL3DZR9(||M$iTwms^8)t9d6Uo$}*D^+WPF{&x$dbOSI1U7m&9hdLenDlh(gT zh>4JPspt1WyvZ6K%>@;Ck@yYo4J5*e6lnyxP=;+A63VhEdbgQV}#2GU}d_>|sk7>Le;QrI& zxmlcIyL8$5^CG@FbrSpA6t~900q4d*s;$;_CV=>de_>srA5c@&qR8)_kH~C zpf>ULetC9Vwg^0H>B(5Myry(7IwzF}4VtQ_cPJ!j$r&&z9Kx}w7C7fj(a{k=qrB?# zkz*mIMv(c!5@3=lhY>9CVd=EBX* zf&@BV%Jj6fIPzlC6_~<|D}E5HI@@;>r4$1s$||8*=mM{V;+D93~EoE~iHm2siEN? z5D;_ZNW?Br_kO+G#T_MA3+z*3R@$67cMhwkSye4Oi}1sT*WTL4zNY+Y5B_aLJ4pwd z2*sqv(^>??@KA`i7dj61G7}NO+5Wp-7rojLjx@I4Mz~H{k(pn zM}$TWR7DpF&%7Ykf1Waq-|jC(D&N#)99%2aePmeuK?{{}x=Lea7975~C}O$v zaWCPWyKVOS<&M`nRHXE7T|f7qp*E5W9tKQIdT1On!L8wsP-CE5u3zwfJX=6okU~D6 zx+3OgzE)_FaupB&bFiq(}M zm<)edsu18S8X%-T2&F43-0<41)jT!vU7@cauxr~Ry9uMmjF~ul`+9~bRQ&&%5BoK1 zYGHEBoErW~)P7*qfX0`vNLu8w_2MsHxgwV*`a{_-ctKC3mzJ((mzccgeKld*&CnHw4w96zpr6qj*OfN|xUH;}t(s;a~_?;bzbO@c%Q zDzfNur1b&CSuAiozwRk*N{o(*VkWm!Z!Mb*kA-OK<33F5xVb2jF~nZ7bg3Z4SZVc= zB}H$y_j;4%`Eyf%F*K@E7FrlNeram*|1w-q=eM-X(miJ2A>1%=or3iMz4r%Fqz4U( ziHJyAagL4&(pl!x0yFSs zHO6znOkl-xfN)`Khddo9RdxEhOEJ{}4=^sKq~z$Qk<@6%BN*I?nI-@XZxGL5p-Z}v zmRP4c(?Q@!e+)GVfpV_Z_Rnqf-)G;ap6lVcoiPy4HP~PyfsBHc<~_`D*TO+misu3kPLM6x#x*t z2NF&EsSuE1LKN#Ypxo#Y{?+gC{NxWz=gwt@(Ci<1{P>0~TVB`Hcv70lhpcmT9bhTg z?)l$V1MvzX3z+N)D&fCotI|8Dn?`Iydrkn)Nh*2SK-1#?e@&cYacW!W^>J=Ni z5-<};JCI5k1GA`DLP8})GYXxv*Q{QBC?tfiWw0h%$jGc=Lxy%<=heW{}1HgeY_x@Yv zx$=Vs%{q=l>X7C2ObU6e5ht6Oo5KUf&}0S$@u%lBGth6QX#4(Us;T2rC8U*%Aho=F zUe`j^r*;o|3BesY{rm%)L6#M^fa@kKj+q~@I+}4fVg_oa5~RToA3cH`SNQwX-d}fj zI%ZjoS$PqyM1W^sRs6X7dO%wy3XCCrfU!MZ8-y<&c<(#$rQCa@ea6v#P|+xCqPFi` z(vS&v0H+oLeei$E{2Y{q!!i2J8UcCp1SkZfd*m7x z#hz-@^U=6tU?LoY<@oHL_uhZS7S{8Ii#I8T72_qzK!%lvq~P-9Z|u!?&dwU2$Q*>3 z@9o>OOz4Fd!YTvr;jmP?44#6dh^Oa}Mg5>@XgFMEaMBx0>G^`edNMW^UrW4`u<47D zd?WJG+{NX5Ow4jR5>hgPzi4Q`ASZ0($iUlnByZhwx{UDlaux3=`h#(_^Iy*jcfnd6v9}NGP!^g%}!?R$AwVc$)a1Jb0^0U|! zI``fBpSv%87sc_mZ4;Nx(mgV`a&&T^*s!=6vDfa?&a8lI;q_|WSTnbqg>PU|HaVy^&L3>YIgSQqNyhzVioU~dFtF3-BTVHoV%)B zt1@rHK)&=6mKb8^ff=$&Pk*GA%?{eyCq0g`*XP!(YO+~0$z#~KF^~Ci-v#iQFh1j- zLN7nEFHNu{*~{EKl&;l8ZTjAJ?HZHh|H$@yi7DyXvl7M`xB_HmMvQZZg2A(4MsDQ) zZyMbBZm4jwU#xoHgVMof8iztd8BPJJM$BjL)*mJ%ZNx4o5x{HVK>5LcYdzKSlT)gT z4I6?j^)0*Tr$<``E?EpfuWAdhdcdFDA1XPX2JZ(Hg;`8Y^=I*|ZcV)uAq_DDdy~5@ zH7~FF$rIV$l{GbbYHGcMy!mDU1kQ}d=;H1dbA+t7aAIIi?ep_alJE$s>#*W_l>?p| zaJGw`%-|nyhLVy#okPq@A3u7uYsU^^|L}84nfuaR?-fX_sJ@uURQOL(Y#uT)^-Ok` zEdW>$+eUJ#HPXUAT{}Bk?l`oOs4*hajNXMWr%AXRz2^iDFis``4%39p5(1!6*l`{~ zLt)V-?)@^Iy`lne(S&rLvu_Q$no~LHDrS3lC<)F^O{A| zXh5Dkq0Bj*=bJ{iz>aYuHkOx7eQ)qYF$MM@$aJBr2WUpGTDGhSX1I)u{rdG{`n#>v z2xZ`S0)%C`QT?%H9vA+U!@|^cBT}gHhxpgSVPOYniOlG|Da|xB4;l#E%Mig=znfR! zgJbv)OjoxEk{m|;O5tUg1I*a*u7Aq`YcEOzDkr;DTqmXM zov9OH?OR*-caklV5cgJ}^r~>v>~GSldmD?>-@l9Qx-jC^)P@r(^+uUYt=N^iF+L0r z4<|SKz!xe1p7k)u;J$MgH!)3}pumOlQbFqJFN#MFRo5lB+yDI+_hlS?3j6!j%a;|0 z450!xdL+GnEK>uv3$B)Tc*~e2)jky^7(!yZ3xrJq9Y^z`!$;gE#y)_AL6gC+XVAxh z9S3lald~qh^Ax|dDqB-U#m>p8iJ6?Xdz1+f~=JYFM zzirB4w#DhC5p!&T-t8Gf-`prx*0cZ)qatL#2JVL&BOO;Jp;{+1v@G5Dx5&fuXYy*4 zco(h@h7w0Od{NRF)J~qh~T2MOa6D_F+2b{GLt$`FAGMu3b zyIBg9Fsk)htNmvDuz%X(Zar?LhHPc8M|V$MV4nN(<;qJhd5$liJ(FHIxW8g^R+aYw zBfHI;7mUuWw}-S8@u%aanTGEcC%r%70mfIR-3e!?%`sM`=8}u9d&lFMgO!v*tDNai z9y|zTGPvZ7Hcz!jdRX|e_;^=5R~#Lo8A$A_VS{?nM z$%Q-$d`Yqe<*=7MIRZ;(pH)M7 zzB^X%dmTR8E6xQ-=+2z^`Q5ux=WIN;M4g&21l&rI$;RvBh4J$Ig$wsHn@}}_r-muy zG(A0u3gHO{XqWKTR8DXZ6AO%sQ+l8@8Y}+6r!PXK%+BWWu57rPnW=KciPzl0Y~V$j&544MTheU<|EHe(Pg7+i9!$K|}seuY#M5EW*cPBD7p1| zDiv@d`aFjb;RaLgs)H1a8r8zhr;IOlX?Zn@gOQejevHzpqa&BTz(}Rg9-xVjL}jTJ zbC11@^2%uWo3Q0 zLVUS$WeNeY_V$(d_9ZBR2kMQ{?Q0!t=BlD%Ruv5~3)MobxeoL6Nt2A@dLw>o?+#6R0UndWm+`mI9eQDI}T1gmlE zVN;r`Z+K+ZtC8!*&&SLK;LjU`Y;MdrUC}3NQ337)7Tw|LshClKQe#Mt&84}!SE=~j z&KoZuQvWvV>Q(3LYT8CSZHDn$pHYaw4iwK#8F$?2EX_GWCYt^Vg^RNL6WGuHrM228 zoN^iEc=59AHv$A;Z+mlo(HE2osmW82cSz*^2OD6+hSU5Dq7689KYrk;h8-a1Ug<^p zby@u>agQ3q4HM$y2LxzPUTDid-G9nSu}+opn8uAmn}bPL#?OO&6T>6Yxv6QMD2D`% zz->^~Fu$O{7U!SzK1+U%H0+$`;Gh<_#YJZZatt6zrVJ(6yT2i)w_pHN@HFKrox8dWwb5LxTiPYvNQ>$C`{P*ZQdT!!i$wE_52+5 zC)bh_X07+TYWmnuCt#)DL*s!$V{wnNw$Dujq`Z9ukdL$biljl}8*0|7Dl?HdN^5q- z_nfi28M#mh-EX#?LCx-+DMVM(sJfk0H?{a5~`Qc0kwVre%2a5UP z#ZuTlG9lXbf~uhByJ1gbU7b+tG=G0iCJRMhi+cj1E(CU=M+^;RH*T-JVE16>be!}9 zCHo$pIC7-eD1TNKt$#-oNG6n1p^YU@lMu>t)oc+A&VKF~>!IpiHy|q~XKID>Z9O;? z5ot?oY{G(OTR<_v&Mbp7v3s`B7)7FNA$e^0uQRCrhSi+(nOrN%Q{n!{YzvTUQ z0D+SU35$++Z^%yE-(C&%1-c45YTv%A3cU970+Zb$+e)SaIY8$w-NIoLj;*v-K)8^s z(qPVnCICQ9hemwm$(o=>}z}c|i6BTUjDUCCf2>w?Dl1tUmURj5*r$kDWDNU$G)<9?DZf}o8C==bO9hXi}|Qbz~@xlRc4)@9{7_KIDbi(Y2xu< z%^m@gbNn983Yy6hU_V1FmX|l=?bW)^cWlpBZfIy|pyU99GFm`24yRY-cWl7hEG{csZ?pfkdn#d53K#AIJkyovDoM7Ps)m`#MZtl}6c3t^XFUuzF;o#q zIhlztXjhL6n=4Wqo9hYr>bJ1FR!zyT=!q{Yx-sY!o6W5^I+;%9Z{xGpUbzeF0|HZp z7a4Ud7RtxF4urwO1MWTN0a`($A6BbYfsdmbh1-ieI;|&Dhb338b?DbmqS2!t0m7mz z1K8k&niY`GnS8}_^5pD9oNdr~9baQ&a_8AIxhpdoqzhN7d_#o^i6m)^+HR3BGiY33 zbO;=VfxAQRObgrt5(8dF?FBHSbqH&q>fCMW!@iK$w@gZ9e6NlpB@Q06EP8rT3F+`B zNSuCtcKK=DmFJ$b5kf}XXMlrlzj-NDPMaQ0M3yIMK`((BBR~U6wzAL z5q>|OntUk&EWrMoIRfL=b(D+NS`>0$Z*^~>0^K0IHpq*F67@a9c_mJq=)%Aai0fnX z)jmR@)JW9$2aKeL4<}Huvu5+9+xD!j1OhgA)1Z6A%%G)QsXxs&9n8t z@wUjSoZFMV~0WMua9nJKX>fh ziE^b3C;{TV&jdT8V-KeL=yGwL;XduVtIHw*C*J*81W=9zK7}iAPmq?*};)Cmu%C zRn!2idmuGPY=RZ0{Xa&eQTVeKcd8$nLojk8=jChGf>fV~a+649zk16+hygw?@4Q8e zG^hTYuz~MoqB9eGkC+2!gcP217ZI*dg=pWc{z9bsr~4z|#RuEm)xGx{T27t)XY#)3 zN|7AGW@&wvG94-fOJ1;`pUidu+(fUgQTH_17EuK3lQ@;}n%G2Kw&77hw3!vEZ0L0p z0EU@`Ml>)SP&KnSWrKZt2ri*PVlB<4%@uU~26FvRj$-!#=Ng`TzsT3RAmF^nos$H%<8)UpI2& zFe3)c+^~cx;5`%?gsU*r5Vjb>L93^CouDPc6M*BPa~d1>n(zgWM|1NB$O9m|0PP87ts4qL!UT)+-TUkIdcMRF5%u9u$lBWI$5=i z4J;fgv)2XQ-i+_`lPMg;9gT#lYHEf}-=2o@nyfC!Tu7;@0s)35)RcQtNibhozIrvp zV(L&%D0jVhI(up+N4PI&Az*lMt57)#QmHs@q5QGIqCcW5=J4n^h~VBJJf1QBR*&(}qIx&lpV#}oFFb6T&EH|~dD^X{c3~q##r~p= zq?t38AS&i&&);rKPxIaiK-{30jG~R!+;DG^y?I?Uuy^(Op7+Ck=^mlPMWth<776b2 zz4JHVG`%v`x6)_q@8p%f|4z4U(DQpometK&pI2Ww13IbZaQwv5FnFy%{uq-%?rUA2 zBm$N|(K(aJG(ae>9w1$~<&=dFpDR7AR}D&g&Lyg+am-VA^+i30T2Fq~J6M+QWMQ*c zQuvWmX{IjieJLOw$COlB=J zsB38WCYUgnJN++*HcsC|Ff93DzMSJP?TW(VrmNpgl*UY*x)U-1trdHSAfI$VmE!fi zhr`1cq(31z08s4el`AUi7i!57Apr|`_@Cyv@IK+#gIfS%@m^Xpv-guSsH&`lFVKHQ zxznUx{Te@fPy{c-6T7Bnew6*-Gf}-_#OaeKfB*T?7MEj;-nY=A*-dg%Y!#FiARz?edWI%Gn5?H)gQS*%1T$cj z*oH5ub*nR;LX=_=Vl{aE!i51{SC*VXqVScS9ZDe2W6rwR$Vf}@P)15B19)pdV%iz033ltyB=7hV zhtE}6>JuksvEv9LCqI{rDt;{;QX>5yYmcZ)%um^f#Ailb2@Vi#3*oQ9?I95?mdwud zCk$bZ9y!8fHswp&jT_y3Xrd=UJK=B&SX7*Zb<3Dyn?^+C>HzTz5|dyF0EY|mGbc`L zD5Eh#N2o1v#<01B%yKu)!sg8%i41`;D|Wsf&NbTS#OXUk#{(M9 zgi0eFht!Zy1@o7r>4P+Awr7?J#73*KP1Bd`M`y<-%O=Ea)R|}&+U$ap!_cia@koZL z#Bm)4*N&`h*^oJJjNyBH{yaeIER6h=5%J)|xJ%zYoYs#TFI%wa4Lav6Vu+taUI+f6t! zB>?9Gg(CQ0WY409^IB;Sva2>7X;uirpc_LG5XmHT#0a-@p`4ks^Sh&>j z+qCFcsTV;|7!l=My(-X=VnxAC0K|i}apUVPXq-GUL1-q86tQ3Zm5VYbpE|P4cdYfz zJy|I!MfdJ~;Ox+J-FnxP$oqlv@-LWn!slUoqv}^*;B#W&c`#}LR$%OyLb`gOGy3l)vKnr3@hlO177su6s=&B-s>)1zAPJs$%Cp+4`F8HN}_z@PB0II z>VptPVI9XUd@oQ5ZZgK|iN1Zl1N5u?-R<~>7H0E0pudKkN=gAY_BDpBf)6X{n*RcgNy+$J*c@+ch zUpLO@@=2G48|B-aw0X6q+7(Y#wmLzFOqL3sZBuzr7gped538{>dG_^znCA2A9_6S8% zRzvnkW*I3$M!cWj^Y+jCJnzHlob&sR`@ZgLG&Z(@I-Wjr=5F!t+$m!7HXvBiRkjM^ z>UnX*`g^0d^AgV`OL}51{^`>*L>{-+qHCQuuX9(!A5Q({kB;{vnV6D}RaZ2#&!?A7 zexs1)J$a((C;1h7Et)llDN~sB+Krj%x}S>$+%1g2r5d&H{KyW&`l-Hm>a4d7?bmM+ z4unwZ0CET}wOR0k=|r4uD0Lr)EQ>>?kaLN1PzVB};^i3!ocqHdf`4Yx(A0Z9)~C5n zGRJcz48q@G7sgVgV|@PHLv>%xiTwpG1M<33MDQA1@B3!$Q0>u!4c^+zwTBtzAg52~ zz1?^5A~Jv)hR%L|@+BIlu`)_eN}9s!aOcsNW{q8Mu60Maw{)WsbHWf5h-nl+TrX}h z=gtLlP?kk?!7(P&XT_uAdy9*^>8X4Ks{F|Zprm6Ba;7Ts1QZq7h4-6-fafXK0SB4# ztp0y0d@&R6#4a4PZRzbyqYF3*ESZ?2S)Y+tdGSi=lX_k_hz+E7E}7WHLN`! z+2}<5R1WihAgD$CV{4emM%0W7X)l7REYrPxSxkW3=*hL!5#=0T^Y^+4e&BmPqZu_) zOYD{qE*Y(~oNu~%kp}WgGH*eXZH*XMU$BbF) zj1sT8?05Zq@rMURqL(QTRMq~OT9~B!Qb4KGr#Yo~=XICGCHmrDuB%MxqGK7FakUZp z7l;S9oHUw*bI7#&$;%S80O7J@iov}*cgi2|=QZiQ6l1Qx>Y=KttG!D< zncaaW5k7a0pD+OF*q740@9y zCxyH6PHCNup4KltYOW1S?S=dn!AIGf0N^yf5K2UcXR>x3aJjk46573Sb>WNd-J7Wr zv0@LN;U1M0$tHx?1KNal ziS^ndu-q?Q(y2PZ$DsQueQz@{b9_FnGnhD?8fNfL3(mP4+w9?>6d6hM-^fj5xNo28 zPn#*R=?bfJaxQJ{A$b@*CYL=)IJkODZ2+bVl)+8L<@^)c<2JanG9R<8>B3= zHwx6;KW~O41HTLxk0AI0@(`U0AngY3UJeZ-wS&w`sXB&NQj2mHFo=>ZANFNBRU~y3 z42s&VqX!NI5w+Dz2^G$%6{|--n>^&Ax7?z-iLNLo&CHmKLlCKp@bW(nPT)U9VEYH0 zD>-sxs=i9uAh{1)s^9hO(c{g#cPHG8d`zIbGJP?%R26@TNl&vrLP#7-2&om%q#v_Y zmWs7mTMoPA=%rtBTjYz~0KGoGc%iJVz2VJ`9_MF552nW9S8^!SoI<*ZWY#i!tA8g= zcy0a!E;sHvfm7gJhBlHNJ6#o%;CmnjraNy@I>YBBrdy(I=uug_fL3PHxrgW(ZeO{Q zw|DQgva&`YjdJVOafG3^x1V$~s)!jK@H)=1KT+0J1J64d+HEu;dyFl~w+PB`mfBmr zG5Uz4c$r>R&le(&KbQTgqe37=?B6fzvzuSD7=f^8wp<9 z?4cIoBPUpM1f1KQlf(A0bmY3_yFfe%5}p+bwiSu(sz!}$LlV8?Rsr3E$Vr!HYtNs%%*txkro2k z+N<~>?8WQXVWeHFt!*P{{#kuJs!S#gP|A?>5Ry5Hp5Ti&04lU9tI`(|JZI!?K((wa zE&>3_q5fz4uKkuclkN{iy z(^50uy(n4kQwvZ;OeLiqPb*fb_x*pvDu2JUExg;UU$6eRA-CxM&XF}#O`t&I(jr5o z^uxXSdN@Qj!J>r*DSy%bQ-C#AF)v|7(TtU9^xsW!w+6IGXb(m+3*O{lLzT@?NG*v&=Qbo@W4be^3eDu*@Roe}27EdZ)Yg_@&*2+@{T&e%l_NX)9|`F459f zIIeT4w|6wk7D;j4SK#^xfaC3iWN+YMNSFTwokZk6Xby4+GjMy(*Hk*Mk74i9z`km| z>Mf4tLn@RAU#I`?%6gCK(*wW0okDsIZQK-w%C$!$&W6x4P`ozNNU%uS+QhufPoid8lX{@v_DL`^QU=Klt}kD{ z#B?Y9G=H|Jf1gonoq90Xh~I1Ls!<-DbF(ao8$mV`Jsc$ujgR-Brq6o*gMv;d4mppF za+ms@Al?6glTEnNCDZMNH&rg>4K!AP$DT2VUtrkDj^U6##R#HqbmFzg z7uTP$(p~rC+1dsDuV!xYr{6HJG3e8$th#!dar)huEl!5?0!uN|djB4NNmr(O_}YVN zadI|w>SGN6-;eH`yNl-Eby;f!d-k)yG(Zk6YVkImy=qk(g;tYCmVJW#i&q{^D!CB~ zuZ6%&E(fOjq(1{sF~AJtWWg&b&|E9UA^OHTbp6HyVC#WA<{7>hdUAVbZkC6k@pi|l z6ptM<=~%+rhiD`At8Di&^Uy9|qxUHs{eZvNS;!XYn1(QFJoMlL&6tgqDt`{2oOkcv zgZuHOF{S~2k{OagLyJok5>C2%iItls?G52nj1S-EEiq8ZX)c4P3~4D@swduX^(ZKRHcx+UCvN#pO<)KJDmc=bnT3xXA6vkKD?ZYGtj6p+urL|~ z+64dYT)HYFb%O$Yeg`SZntK3|Q6C`019C$~H)i;7IQ|ar4>W37Nxl*8-J~SEOkl{s zaJYQ9`_?kg@KLr`#7z?&aE!pz0gC%uM(J$nf>!iJjMjKEka@_R!zX7Yb}%-%CPRjN z=4iHhzTMrJ)F_;+6uWjEv+=Kmk5zuH7R?u>HFPAs6g(*T>G=6GH&JvSk;KNPnm(uI zY+wn%_7ce(g@>&zPUZrb!E*;M>Ysj-g}~YrlxzH5wZwZ5AI2TI z#+hw(R?l*V&ek8u*p@e3+5kZew?Xs_iM_< zek%r@KfP&@N?@}9q8(vSkAfJmBra^H;l9hSI&`#K^q&yBQHb{nqC(Zd;qm@mv|shu zbE^0HkHvrj>@Z5Eg!&uS5jk(aNKOn+j~_j9_^f1y+r9os9^TkzWn5eL@ctBZmbdb)GdzfQ))hZ1AFQAvWPyey(JD5{LTu0!-6-e zmY)7`{T0RgORV0Zr8hiwxMcg-b=4_3!Z*$)iCP$QJ~=y^eZWLMx4`Dz)jP~{CA)M< zw9_+^jOkkbA*p0)xl;1i8Q?5gSz8_tvWv$Whlet?P$KR-#A3aKJ2n#82HKuYOB$P+ zDjWULa>2-4aLX#Gubje$jmjBfZ$*15i;K9<#&tGNlj($TqkONd!&kjV`n2=mneV{4 zrKD!ho!fU%+Uv_r?~X;)`>vibqeuM~=DBRF4!s5aN12;*H3L02{CPVv`bcv5`^HAG z9B+0Rk!=9Zezzg_h?;f#Z+_hKMbyHq@bePI3)E}$H()>8RWH70?t*m*wQBNH*F;!} zR%e&UcqxgCL^&p{@^h~CSxcn!xSxq=BK||YVVqX+i#HL9rs?aszj zN|kD=eE7mp!;ac;JL`AC$S3budrM+aX8D<>C3gJ@@kt(_t1QDjsJNhv zU|luQS#`Gdmn;J0X~qMA*LVV2aP25MpMhVC(4QBuRg5h@F&E{t}Y7Ie$7kPOJ8H{?B>H@0sofBN)K(#Yo^ zsy}PDhN&tmhf5CC-!(mOUEJAC{?b^Q3XGn)bA^Jr-Ds)KRu;>q9rMQ#`(QmnR1MLF z-P7I@L!HM<3>`TV2ZNx%KxFULfSR0im?{h4y1s+P=Otlbe+#q2xY~b#CC1t1@jcOY z!^{KA=iFxiOqYKA(MPHl_yWKv{At0b`N|c6VqFj|Gi}o~bl9>$Bz)0DMTV+YId`;2 zm_#0eY5Jdk3PG;H9ETejh+p$6##;H==dh7S6V)EPKXbWru-s4U5me|?3>O}8>p|rJbLq!E1y1@2&xk~2J zY~f)$YUN_|81h$CV4Bjk>7?{9DzyI_0CSrHSlO*9wf*%pu$t?tYcTe1vw_2NxWjRj zh5>la{RkBg>H{v0MT@AEq^cH3MqNqTf2CBTJVROzqjSxg)1VI! zkLqUccNNTWh%ILJW7XJgU*WE0`)r)IQL~>qwVIXBe9u4I+9EtDZ%5G~<12fs{1j9r zzKCs~WdBPoW7n>}diEQ;s>q6IIXZ8%)3ZJnyTT>157b!1rcwP~?xjlZnUGfm=cdec zeAviL5h%e_M@KAva13STgBzeaD|>6_BBe4rJemtuH~nTr=NxC}Rdk9wk1Mr5%NcUt zYJ6X1)8WGr<8-EF|G`32Am@8}G7^w6s#VR6GMF}H%0<$a*Ey5uX8l<5tPqiRY%6n4 zeq{7wpPQBB^!9wZV_oS=a7yuyQmLI?s)tB+e>%75semWW*8KG$vNC0Vaz;jn;3^3O z$NA1%qZqvH=Qetq^FWoU-dwW@cP{rcQy+^%c= zZ6M5XL|2eaP%eg~#7p;#D#LT=ubB5%I)?MJmn2A8Z&5lq>9lIhe40GBP@Z44#aDSt z`AQiFw?EQO($pYZl9GtA52Z|?2JJ7(z01TL>v5@wu~R#a1eWRAIAli2*&;C0*IR=$9tdlyI>+A(iXwGnUxfUt{wn6}$o%&--Zn(Lgw;RfzvnQfdp2myiH`pjuys%d8S znqzIJLw;1$RnG4Mmu6;2_QxMI_KI%Dk`(hOn_Pci-ve}Q!-m~0 zO$OKf2@e*?07qSVj!g=hoNhIv8C}p5Cgv4kbtzoOiiku_nym|HNMDW+eo|^dd zr5%^PQpbA*As{CUiR!LhPY{SR(F8pyo4ml;W_O>LQpU?)z;TtAajxul&6^L`s0Y+9 zRjFg7O~UQuLIzA{&yuzN2{t53W3Ar3VG2Z#vN{Q$7#nKt-J>2C6DG|snWvXhs$nzq zWW+LY`(XuB)9*Co$U2SB+#Y7U6z0QSUZJbJJOFIVhYz-s`#E^0PA3&VU=$cz*<-KEC<_ZeHF9G)uazGiQD%J!QH=$4#4~eE)Z=hnEk#FWz&U>fHvLtP*0^HkQv3J&AtmXegME zs2?9cZq|~dfTeZGVlQ{xd;^~mvUOLTX?C?KR~=UVB#uRSQNQ3E|K2s4qa|*PANfFM zhfcSl&#J}@@s98~dgk=$O$V%BYh9@=n&=*Sp-JMS(h=o^#j~D%uU8K=aRbZs4hb1u zKcrl`?BlU)b5-50uRa(a?>}ktxpSjTWk@AwFkE?|+-s=+qhs^dS*jh({xB%=`P~=w z$1*Fwzkge?JpTGRTXnOV)PA)l!AG<1Z_LODeEm}+Nm}+?#;9(wK{hcP6}kn4C}o;lYOwuW@r0y{SU*> zdKHyRK38m--#@MPprWH)N3awpl&Do`Q``2&%5H#As?PCV@hr4l? zIeCgt!quUONZY6}xz_M2gFR}3g= z2`TClkBF2o_=X#-13MP#XS7lSQ-kb3uMMaLaTJ%_b^FM{gYfBSA5`KcWuiP~VimO` zCmVwb(iWnu;bRxoiQe=c7p66cE&JGR* z>7)Sl^rF-i-c?PZT(znndbXS4&&>%5Dl(GPfN^isLpN8!5?%~DGstZ3$dC4myMJ6` zoW34B1eaHAX7e=*ZE{6ZU8zdkhPH)+_B>o^bzWa6$tu3uJ?mj^@X$O}kmPNf)NgbO zlyb_$^JmZIv}h{9$`3b{UcBXWanSQcP6y7)Y|LM&QPpVTyejYCCS&8~adrKA?vK_r z64!h9Vs*G&`Baxcn=XSRK?4XE4?aHUFsB9L+41!TFz#`(6Xa6+U3q?{J2#(wysC|o zFiC0Aszds(7L?wO+g!2rm_4kflc>0%xNyhQN^^p~)=#zQG5zFh*RIX)N;Q+7EhxIG z`&_Kap`f-lRqa&4fMsvrLX7xXFFD^KiQz4DKm-~c%>!1+fJ%1ngI{xTZm|F z=_p3|(Q^VluQ}+ejM&9E>Nyiy!Md@+W=Tj$rVOdRVz~l=oxS}Y7@gv2YmsDg}MYvL`H*iQqsw_Cu?k1i9DA3S--HsmKP@}LRWx`{W{>z zkRiGzuo?xVQwa!{ogwOSoCD-2s(DvnCPU$Y$Te|;WI(e90)@!FEn->kWof~IsGmMkV z`f5pJq_~-bx~68-847~=uR-d*l6W&{5Ok8=j{PNk&4RCKcbsb79uXlHZ^tr%Lecp4 z?T9gBKG69AOTB#ySmwN~>fP7eHCLJ{u&F_+^ZJ)(mf<;(521rifJ z4TIAY@H6AP0IR^NzQX`DAbEM1}S zxkDpu6T#5K8Oj4YlZpulINp3`X#e17qB> z&)c;EP5(O-BO_^X0lLdZgrWCRTQEU?z08Zf~Jy5 z?gH1VJ^G>6OA@z}HBC)^Ej2e&XK!EG)b_(<8t^80+7xa+inf%m|~IL>?j zZSCT6WAxCWDF8sbcKxnJ4D|cc8Bgm(ce%S=BA$qEDPQ~Jp~i=)Up4ktT<+uawtZgY zj}pwNaH#Dnd!g>9=GN0 z>P`Fkm4Z>pfL_=g`zpa2XHd=zD@M0qbC(TSU6;<3zTn-enXpeRvd(e#9lbs~V`SvT z6*rnn?N0OiE1+VB=W?b&h$Q>qA-2z(-wIG<>;1BsFvr&;hd@n$=?%`Q0PS31z2qx| z&zF#gI}(#BtOR<%XXw!*=J-H2)mSS4M%$5za(YGC@JM06UWN z=QTDw3mW|lDLa1BJ-T;))z}!}I_XB`{r)#!x2*&I<6$TInGQGDF4LB=b0=_um_m

55?qAqv@-!KN)*KPiH|{Rc2$&a_d&jJh_U%QtkakMeF{wUGKTm zpu{;@SG_cK$E3Y%wf0LXt;-gsf=@#x6XXs<487dDUs{qp=xWBGl&91NP|fKU#zBTa zo?t%SN~e0aW+YAH&tohyHWj|PwQ!LZJ{{gMF{*5uu}!O_MW||wh*F~dFDi@ zN$!|=P|DZPbl-fKF!o0-JNCCdee}Oo3q_T2ic6RU9`;Lu^YqIz3-pz7*SNu;B4IX@c7_;=jTf_Er2=-Et_5ApNq7&q^&}er9hzENSV2leAnzB)g#i-xqkQO zlp~_*gD-}@JzuLl!SGpL?3bLo&FEl;c2-KX^&Q~YWpZyVEsNFf>nX95$A0I87(Amr zStg>{a?q>Qz09|%$(;|K_UbnkS(})W>g~+36Hepu?lx0uIrp-5-Znlz7*{#+NX+oT z1^|aoD&K2)#r->aurBB);Sa{#UJMx>{Yct~t^F;<;&=M%mF}=Tly#(0?9qw*d|UV_ zcyMFn0WAgGoWY2UWbW#L_RcQ*x0O$AJ9(ky*a)Y)@yB3>(Taz6BmAyu?y~bGxm#gN zBLm=S0uG?|z2H@!BKFN$S6SfA2DBsJ0I%#nwa5E(W|zeO1O_4tidlaJe7^nCAw+tF zzEpXWD*M)#;P=>FVQ z`(i!PLwtjjKECWdV#6lh2Q-ct9@+M=)CS{F+hp8{46r{1P5y|P54t^^GY(CM= zH@rRd@Gn%G3-byuTWeg22YL{jOfq(qt46_jQMc`x5+|7b^CNX*LTIQ(?A7KEA0!S1 zlV(D-kaCPyt`t~MO&>o-KR+Q;wNr1qr;R=;9Y|e1yVg(;uA8<0T!DOnZ)$Rs>%i4( z)}XBGt*sriuxC%Z30LpynmP7b8X+xVe40WAeKTlfr;?-khQ5^kVunu}8k8hksd!HR zXBduJ=gFf-!$*#kxGuX~g;Xsh5pDh%R-^YQHhSorybNy|*ngqov4H1A>N^e0Ttgmp z-!mA))VlX_FY__pTiO>cn67<6m5-T5&M)P`G6{!P9GMW)W>NKT;!v^Lj(20H?^aD6 ziFT8yPTUS)ddNG7Kb0stgvktaCek{XC>%YywfY^cm!qR&>B7ht3axI0Atc;QdC#Z` z1!CE4bhF2c<_?&%TT?(}K?%T(v1X&Y57V}r~Y*4X;xfHB`I8D`& zuwfbftY5dzeDr9h04c5$RF%1KMV{Bo$|8o&((tjEX5JT5i#}__3SJB1iF8yg3|kbPW2RR(t(f2a z;=ePUBDb=pCa^JoVAu2=R>8W*k*zb>hs(#Mfvr1J*1mYQdJUcA;_xC(ORntQDonH9 zYg8Tb?-f74DqI|F3kl|GMsipPLZcU3r*Zh=ii7jQ7o#l^cHV?Z`ur`ttC4J%L|w)?e(#WhBYl z{sk-fwC#!bmsxPbIKu2*mdWRpp3S9$8Amggkg`)9ZTUR)`SImXMwkiNPv>!$e`flj zbQ_S`L%$LFhaz&`wXM@qT!PJ@l@+_YsiCi`Ty1(7)=k@#l0)=@M0I1CVoM)Z9?eXs zXq)|_)TAT@e;s)*6`8NXjL~|#-vH|w{p9*MJBN2G$hLi_`%Clhq${fp@6GWUX|=L- zU~;`7`zBayz8G<(OZUKBU)3{5dAe~r%M|}pTvg1<-nX;}lO3*=UAOTMlj<;^n2|B7 zTL0mP#9Q6(cYm$xyxM6;^y{^f5;%%CKG{6VQQW7Sr#8r)mh8^M{*^h8vsxw3xx;<@ z&b~l^Ca}vXEDSyMa`)MrsRcOdhUuPvan&b4qTA*f7H#F-DaDb;3&}R|4Hg~~8Y=)9*^dLO4I2}NXb%?p@m*{R2L(?y}>kv_|p$Edvg z_|dV@!!=rWp2{J4yF~8=im@bo;MpgZlz6!H>sN{A^+@j{UBC(=A|je3C7#@eIEU!$ zf%59=cE3Iq?u#3jFLP0t+U3m^p!e_|dfOM2*RAsR7meNJ)r_DPVy1EWlZJ+IW5z67 zumHKU*lK)w&Ky2GW!!^>(#|dkd8M~lc0)W$;=)WgJ#*6fwGHjm>evilO~S=)Qd8B8 zU*{C#xGN}~IM6HN7e(L3P9*7hKxo^n{1jX)vF&&X%0n_B7I4Cxe%G!KuV2^0o8YW{ zya=1$rY4zT5qbA?ReH+3?9MOY)tE_E>dCCOy*$sm|5-s?%)6(i@TnK;9qD8!^h5)b z%Dsv?VMvo}+l;GSzOtC}Ag_-|VK>r6_>K52L^e=QuUskUWc6m)N!RyFN8L1kOWt`n zxF;{T)fF2bue0Kq;EJzrr|t+QwgZ>7mzfca|#&=D@@S$e029 z-bZAB9HRH=prQi@Zq(MMRKEj8$|w?d3A}OS)TwX6AHnShZ=vxkU;0_A#dRR&yW+DM zf)r%p(JE%L5XbqKpl@QOW&;NV1ttemTERD?vt;RX^h>M4=IQp08#dN$^G3}Xdsvk4 zQ7PqAQw<5ifH`>oqg!V_1d!BH8d2$KLOBO$P7g&=Xp-ThdHO28NB;T3?S=CMqnsPc zPkzI(ns(*Jl|DWuBS#7zYkT(K-0y}o+S5~a(k*$*%T7jJ%{7l*b&kW|eDd0}ujXU= zlAQeF%^RutE|AdDs^G;&kBKjNGp0rQqS^Mm9)70kjKdW1{Klrad-r^o{WY~Ir$!++FON5nj1#U?+7$iN)UZc&#dTqekivE{Lfn2b!F)8AKArUsNp zzl1{yty$c`J6Esv#jWu9&XBgw^22tf+bDm_+}!F0kd%&k9X0Qv`IjvR4Z3M3=4k=g zKWQ(9i8jY?&6D4aR|dw1I~~<}^*VnC+|YzA|MMn|e6=s-xuF zF=mtf6eEs!_@q7@S(^Ko3A~V*gQobl0I>kZ=%onRU=XVXMRj&{7r6su-kb*X&D^u+ zaIFu5I!{TopK!}@4`RHMwpkY53k#Df0Hli!2L-fQxkMG$1jPcUEG3@XuEbkuR~wPD z1+AIr;DE<;WeX8s<6EkLg6nl?VQ8)_vsW%&tfs8o3V6%<;Uae%rk1= zG=Bw36W!Z49z6IVptJt|gnJ)3aDajC`%j;iuUK)qxOgH?IG-S`5x}{1n_5OBVhpEr zjzi}i#i2g~I<7aHk%0m^!<;f74x@y3pU~3h&n+!1c3&6#gvDm;Fb~2$40=*%uXP_; z{cLRf2FXovHzvQGas*(3+%^*S5R{7s7N0p&&Ec?ENSk&YNIm=Q%a_zDZ(e{k8T4}t z9o<4NFL73c3)uB?Y{^FZAW51%`{#{8N0n6sX)A*=3R`&9=zkU!xP1Td14=FJ3baK^ z53McGeUz+zWmuB_1l5+IJqFA&Yl>aJskDd)?gj>U|2`qEV_u58{*={zez$NG0nM^I zhIb)WA?W&|F6ZLd)X=+g6z1xDSu$O{irqNZ8RR0$13YET-?_7bsqosqPS2{Ok_{v^6!m7KvGA zkF&H?Y?)M>nyegP+cq>{nPFyM%^TxP{aQ=m1x1cBiZAuNC%&Zg-o5Q}evQqWkl$?G zRs43WYXK%~uN`p|zEXECYLdXXOD@8wTf5L*{itFaJ@jG*LAeumEL^e#djbWe#W1v6 zJH;+>7;vYo#PSQ~C9Yrfy=1fvC)+jUbZ&w>MQs=ucmnIZ8tZ{WqwkcKip)odS2dNu zJ>eP0Z)0`CDXX0XjbCVwUD@}z_W2*aC3 zi)kE^&s8v`R>hDV?pw9FSUx!5` zTLQKXY`5H8vDuo_W>-Su3d@$8pIMMYV~}*UhK&Ak?QTRKF??PBmF4`W5;gb#Nmo27}oTeWW^*lDM5$G z_^`YhFu?k%L9|=)6qgw@pkJc*(`J2*NVHRXSzem!Hg|4RW6ry1(%6urAjcb6^m7dADg-&xFI0Fw13N5~?r1fUcALUekitW`fW}Sjpkg9^z)}dWm#3pWS zciFZ@7+D|#tJzCOM&DB#`*@pn*{0P;fqZcX+?2z9r)gV_2sr zj~|cRKsq3%v7mL)Hi9`m-d~s{Iu-qf4@o!A`XdJ{&&zZF6Vowcx;jZ|-Tz^oiNKUE zgh=UoB@2q(Om}`{#rrr>ig1B+S646ky+q69_`R|$5hP402iRxVjWH&}T4j0KT?k>%pz9HR zo|g8N*@k$RzPKUY$jLD?Q@l_3fWqTF|P3 zfgCOQa)Xm=*KNz-FN|*sn^&upZn3LezJJWb@xWs%Rv7QLq!Adn{r&5zGzb4-{iQ>6 zWB*Qm6=kJ=l&JthPd4@6s%c@V_%n|fv3SJ_$OAC4esk;5Oi~y?-ISD;w*2ul5IzMC zvWztJKT5uvMfxjswZP`!t1?dCm6~ev{ZacOlu%;EVZ@(;UIE)0WQW#)A;8*)4o1Na zl+E!VsOVl(7HUpwCpv8VPbdi+Lm(QEsLmr?HclP{wA(tHKEcxI-|MXaB+k#PzAg}n0Jy&flUYr=c z!*sze#vN0gd%h@D!*Gnn)Ed43$2~F(##2a0Deqs$Cr7W68IUnDe(JZhc^=r3$wMXvFr2l^VvbD zjwmefZ5B65L2AQGGnxysjD4npi8HPXu;}o>p^U)o{}pE`=&fD~A}qdBRDD}EZ$|F9 zeAV*k=m@4Lyd>2)XxH@VX$-O;oj(4ndtv%sm&5p+j2UyGq-1xMH@?-72LY(E%~#nR zeSp0lPEhy-o~Cv}389zrkeR-;^c*+0AS}E}g1g%fCL)WVNUUgkCSGG62$~B{NP)ru zIeh3*wc>uS)sSmJi#z`JoHVgohsb|{7)=Gu%td`43q`bLLd6EI9)h68kKY7~nKBnY zZ$f)fON514imr2{Mf#L61-prS4{QrcRFtabo~cwm4EIM49m*~quXMpBaMKhU8yhr~ zFJ5>~nUYktcERFB@u4oT!Mw&;Q5!HK6*p~=FTc_zH798 zY$*uoF!z8m=m!MJ51bpAwYbJfk1*IhsYVT{CA+y&da`G?+j?`C#MJSp<} zD+NWxEIfbU?(j+3CWCCkQ3z0vv3@FlnznsRULd)1+35B9yDDC_(L3>bs0M@~3=45# zcbTlM#gi7^itSle-URVDSS^b9r&`MWx*!C5M*Qx4gWBg3SW8%cKaC+^GnVgZ@tHnDnrTLGc zPvR)(r>~Fp6BAsKtlSa33ax_wY^qvXJjiJSG)vam;u#cFPe;v5aZh7Oz}!q-OA8-m z4h{|-DmA`l!)%-W3PyW4?sqy8UjLHISXGYlALreuJG+r9;{lIPNd)7A)BE>N9JrlU z?d5k|vw_M$9zQ=iVK8FE%>HS(DhMj0ydp}}|Lkr$*>5<-tPc(b-AIj5*V;YwQQBDR znV5z6;KlmyPxU{%X{M{*yMJHWt;ckg3B!=5i$}jWKwUgb&Skb6dx0 z17(Z)F~z}*k?^){lDBq`Y3)alF?J^!XJ6@?yh>EMw!ft056_787>0ZfLraiOI)s|| zHX2EXCNaIJ#uso_cr0(ozO7a^uO zim_=K8JHyC?Y!xbOZXX70^ujYQQ5%gq6iJBfBaakJSs=h)0Z%Nymf1ghnRf-N5O*4D`(XIr(*F?x*=F%JFz4sEyHi78 zR1!r0c3o$^AOGXCd)MyWj>lgN*GR%4je9qABd`>^HhLC2Ku{niE^$j8XPrg3sIYa| zcC0w8JZOA5$Z!)$=jf}hLp7XTky$fzQyd)iVH5mCZbA$rlH%iosX_n`NQ9y2 zPF9RrN+|1Sef@&{`%w|w9ewah&&m%6F&t1NEvx1@gidAvw0+VxKTc&1&^l~GaeIMG zOcLGkGiQbv8;_sKo%RNTImWTbYs$+dv#W8&9dDZnYS{Pg?v@jIYMz@~V>#xSkCH=w zH@uXu5v}SMCanQ17g02E+McJ8%*R==q68<}V?hY!K~^66t?}^CSwTEDffCsDaF$-? zo^CbgAG%0Mb^dyAnfJ1ca&EsXANcaGdq>%bOyMZ@8#CPorb|qIP`&z#iB64G-0Gv* zN)MN>Su+#Q*0<*eXa4oY%Y5$QrAw(&OiFyG+3yNK#s)_@F3(j!v#`JR+wH^f1Czx=kkwJ!$gH-%new_ zltxHkgM?6+Ew5@1(MXhEQXQzW8Yq2ny33Z7eb|apE{))8bjt@1Za` z@WYLe_0kab`xEFkd7FsuG47yKMwphYa*mcB--Y#{w_IL=Ija!bJKM!2GQ(g^>ZR-g z&jgR*D9%0bL%K8OGh6ZTe?i4E3i{MujNlw+Z}T`xIQZ3>IWDXBk2SNhhqb|01hJx+ zD4KF9tn(LF;cTgAe=5eO+$joDnO8Y!Tu@W8n-SB5<&!4KIV$dV<+OVG;K7nbi$Fa7 z2{E5&znEP>waj(eZMoqwi_p|mMSSb+#TzLuK~n_0|CDbJ#HIW9(@RyVn#9-*y~p9& z+Ul<#d*e+2j6UvZ9JOf>xPst4LXJr;^Mo_uM-IZ_nHZQr+%S0;QjQmQioMO?r=Qm3 ziXD{Cx$nO8y>vM&xo^S%X4#pm8kXXc{g2Y-$+;o$cDb5g<550CHk|1mL0nR zLn9-*O_Nbq!6pBVR5)hSCVWPLAo**kxl?b(T>y2DDH!Zi$Hc=z=O|&%IyThIFzGu|A;t$oNN*!;FQi-S&EKgP<#!QAzzbn$8Q4?2(+R5p`K_ zPjs0l(lg}*dzXP9vpHr^JRcH2mB-mCko*Q{qv7S5IQW)!S!a<2o5W_%^|#vP+VYQe z_v*JV>?DR`KmvRu>Kc48z$kwI_`GuYa{JThjDB^z+REq&j6^rT+XjZl>b-jdUb1W1 z{os87&xb~}9)azSX&wjekJB^O9y>+SedT}AP0C=Ny?V(8Jr+O{yeI2-`)TXwybUy@({q;@)5k)0!V1<>+O`P!!P1Q z+wuE1_4~q++a|pM7rgZA{XycX+$tRm{s>AvgrT1IuPzY+P+!j$_{yjNop$qqz%D7J z;bT{>uDN|`5J4{#O;1XKhh>U|0Z%wI*WJ}P9A!7r#<+MRy9*iG5tsd#rbYMmLfa+AQYUvO2rQ_D*5EEc8=Jp11O z0Kb2;ml!S)w!Y)i!882=j7}F8zQY6mH_Q)kV5o7_%D}B)(N$i>_?rSL06?^Qpo5}z z!J`4DjN`42T%W;ZvjcCxY}%-oDVnER_ia3LPpFSr3kxO`(b3$XXHK5nJt}Z9*KYf# zd)24@J-)p0GN$SjzDi1D?8Mzq4zxC?z@ERizzn98>z8;d&#P zA80PP8+r4ba8z!TG>;xXw%Nm|gwg{Y2?tcGah3ORoJIu~r1F`*(Crl!0oyCSNpAej zEh_Rq1eTo9ZQ}J~LCPz-$w)%RL=KF@=JI9Bu-E~XQ z95@G7$uV=BXRiF0s7Au;F@j1B|5c{>uU$Lg{LoG{^``F~V;`x>lwI!pw31eAOI#ct zasWqUx(H?s%M|67w#~ynkk)oEdZJYr+{EM=V$ldUnaCgykG~vFFpkDrT4q(h8#!VG z241!mgPUJYH(Oh`wkpT8)Fk(9s%O3LH<(_q2jK9!SAc|62@{o^|x*7XrCw_ z_fj@p{YQ?3VL$@8U~=?SIB;~uYXJ9J7o7B^znw4wEXeRcL>28=lb}ic zLmC$LUTl_Ljenp@{@?x!=VinyjGrEwWaz|RZTs?tNO5E<`_{#wz{ku2m9U+iyEZ>mwP(2VWu1j+pv!n`O9?O0o$AWUI23{~5bd3PPlX7!BL5luij9QQPk+2R zFc6RHUM(7#_ie7^nKWS$!U4$*Yv>1=4A*}UplG`VqW9lISo&YqG+o^{ow+KsD8_Rf zd0es3G0vU)NeKZo60<0JdG47{_cT|Z?YCuLaNm3?I7r?Qc)8SRYBpv0H`nbI+%G*c zpJZ9Sye;&>f(OdGcU|PqGu@&PQF^w*>E{pFY@e7%aS`C#@u9gHYY_Zy`UdNB+OjqQ zw}2kZRrN>AJ0BFkE=}I3XWb?B8{?SFC3Ip+(E?Nd)vJ8hfygUykRoAjHtB$4$Ib&% zW)Mvk^$eX2I{@$qmyLphDRwui2YuM-i1jeqF+016BjZ&Tv_V%pkX73xt?o=UsDF2R2~Ru$M8 z$<7#mcqW0hU>e>VdS#!YB|Ui(ahJ7A42(6&$ZdNHgr`dD#s z#GdE%_0i)mF$Jx6H@%K^;yFAAFghG;HfXbgZNCJeGJ#>WEwfP!YZuI%+Cc91x?V2kbklXsko;3Wy9r zq;eql!O17$*>Ar7q%;Q*fENX%2YY;I@NqRK{Hab+tHP#feE@$+yK*y$(mz9AsXZ6@ zyL(>*Ya0H$G$Y^Vli@^J*)@*;nW=%daB$!z;oZygNqUfn;m~|TIe*W!Bh{jV>2v<^ zwC{cRMnM%uw(^eX=7{M`YG8RK@8dVcLB#0}~-e zbz84T-PlV?hj+EHGT3+jiPPk777~qyG)(FG`F-UPT1vU zM7HOvwsl3p(HBC|JeA)9!w~|AS{@d^VmFQnYEqsm=N8?hXQ(X0VZyCmDodJ|Js_cLk-s$nRoQtIEEJ9aHMvM;H&-Gb~4e zILShbbJN|l?VZ2sbUAW*#gX47)K#tnr55%XBqa=yn}SBym}FN|6j620PU|Ruxf0+? z|3`LCc6EA{cjYFJ(ACGbh3~Vs(2o9^lcRK>N&4E=t52Ocp-G>htwL}I#w;9q41=6IMki3r(dn`HPe8~ z!#NfHJ*Qym;hAYN7Ncgq&mMD_w4jXps`e4H^2^RnEu;^m|JTnWBnaxR$b2R3E91f` zv1)oN7s_5J*l@MSu)jxmT4fe{|9Kp|_i__pM0+(5hlxBpAm!u4OI&CZvuFHTkz9AC z-)*%YG8Q{t`wx8^lFhIcH62hAetc$ydwb&#rI%8PSFyedgqp2ewoKAjc{yG(4&LS9 zA|GnGjI4bNpGA(a(@UX{gj&Pkf*a-GL-mD6a+TLSK_^sIMTjRLBiMeXAZ5%pHF*YhVFHN$uhwmv31$$AA z1@JdEgx5t+&~c7@Ed?cX(WzLSy}GYYUpELu)M$M zhM~r1y^ksQX)d>#ALM&y2kYJTZb|U`T-SkIK4lHC**Y}KdRh6`Q*@P;E%@)m;`PPX z?Dog;h~kD@08`6HFtKo2yR(#8K|$o26sgp`3ySHTqPwPCb5XtaCV*=oVj2fKY)AT& z9nlLXpN?xUHgVF=kySo7?WEndz1>F%e1OsCc&yyI{FGc&KE>8G3`kxp{V22OP?u`! zpHk6`Ki=RV`I@73)Tp1#gt$#AgGAGO@4U31Y_-#o3Gk2X2? zyqv;r=ZDi<`s)tZ{AsvVqIz$+{z|WxS@c?Rv16*u*Dihpp&T?MsK<(d?_DR`G?3m*uBDZ_hS$y|9KSPZB3-r|S97 zidHo+py*4?1A-cO%Xppxre&S<(@HSw@&T+<*2A zp98)Rm1pq}PgkrQS~D!Z&-f*mUk^4(XOjWX2RyHh+Az`T`Y~P4DA6qK_;*2D3%N|F z(i5*SH*_^rZJIALKIv4BlG?7@S^vRCwuzTwQkRDBwl!F%yp=1SkSh1Ne43yUH@%9( zHMv3G9gjjkq6I74PJP1iju$yLd z

WgDm2ntvt!<51)|_&!V31zG{b^ryv{ z-C-dJ%F38Al^2f%@YCsRIxbj&Cni2l#jA3lU(LkC4i^gf8jx;AV%n;zo*W3)R%dKl z@CC)nt>*D#v7j&(2jl20r+j9MgWOCXT4di#SG0%@j2JB8yD8%Q%%wN32#d+eiW)Zx z#Ya1+l7))rDkH-IDpELAS}QAhMl~nBZpz;4?ZKGNYs(hq4lwyOCN_ENX$NiJ53-CKZ?hMGt=FUP3&6j1Q%HaxH#N4-6 z_-#U`3cy$~vx2AASMjeCg^1coFCH#BhtrMgL&zS))A8so*-nf|*0HW`4yT_n_C`d~uwD>h%)$Vo<_8+d)@yDTEK2ZDwRP zZq6Yj(HsK^$Lr;Yc!Kd9ACD{;kD~DU`lY(1f=-+_1QgXvqgOO`i=wJ01pqT%ej8dG ziasqi*u5oW#_EecQ`FT-AkP1nzH|KDv8WAB{hf}VOm`A-et{{~*RNjg?h_46`u$Ci z7}z|>M_p*aq*1T@J}hw^<@@o`C8MCV`Oh%4rI^RMFmNR(np9{|>03fh82-gw?bm@( z(mKrR<}2A!AHZYF@!B&ZIxenH_G=a?n}9uqdLBj3S5v;muwlc74sF7`L^F;Wp9u#h zlA((y5rwX)`4Bh#;RalgED@n>C96A3M1uCf>tIzjg>6}*F$|j*2DxRYZY-R)ZSg{x ze7--QAME7fMa}JI*-stcTHW_)qaT5{@-JTp`9a*f&`8_ZpUvf^gbXm{<+1XqK*2OQ z`^OBh`Ow@-j^t}EU9;2bC3V2 zsr6{bMBKSw7f?P*LCxc1i+6zz(qGfV!-L>b=Zb(N<29Lv@rS@{{3o+7;|Vi-ng@v- z;lSsu9gef2}KB~I*y9U6XRwI3`H<*Gfs{hZQFVwWq2l0~5+o?3P zsS*&Vs%n}CX$sHvveHuV>}Pz>TqU`2Sykob;G~oyd|zf{`~Y$`6f`udYHKlEgff72 z8}uW-KaB&gGhxOyb@mBt7%eSn$zQ(k4wB zsIE6{-pu%nM4cShnUwPTJA(7JV>yv&Pr$k4B;3#id4R-{@!yQ@{$a#SV?(_G#bLbE zTx)AeV1}v#m6Wm5YxvLtJbGux1`E{rSt6)O7&>ubk_eaUTKW zomcQesc&WG&)K$lb4_bIUl!fWu6T!|62G(RQDx9LcWXS`Gc#q$3Zn&1 z*q5D-lIc5}ql=T_{C3<9@Xz>X|FQ2@!;%e!z0+z0-a>xh4m=*#rgHl+ixj;6DQ_j5 z*X28L7toII2lTNNW5UNXxbe=N>$~R}MLWf$L%2^NjAm#$~gWh)3De&mtQ z*>W8)ew38dUb{nyiDQ)ue1;x)nm&KXdS6dFJ3B8gjoAxX6<-ab239?YS6`Vvcl@xn z2d=poWXj*iQompLB(Ts^+kO|~(k1#G8+4p2&J$^}n=YS^H~ioodLu2P^E$Bu zZg7O3JW-YkRgPa9_{)nT(j@UKRWjrV;r_3g46=;8Ty>70jE=tZZWCrMIIZq=ndLiz zxf(vs5pM}Up&RCMGlwuSE<%6Pmc+Fk`iH*-`6)#ApQfQSNSHzoMg~G1(mak^FO5EIr_>kfO_KFWNL)M}^I?wCt}?191*NPmODX z(#)O>J?pe|=|@~V5@wue!CaXU7C9ms8iOo%P~%FRC+m}hAqBgSuU`xPOd#qsT8ksR z=&3C2PlK5G;Ku=v=3E6BTR=~;wK9(fe$#YA!$V7h*qV$m9Q5|HPqGbKX^cc-uFV^ zFifkCF=t`k4V&Qbvxo4s`6ZS7%Qzba#wCB77z=@LpZ#<-f2|5X(dUXiMPWOpM!lG$ ztF>C{lrCp0bP*2Fq%^)iw)a!L_IjBvN{Wh-wy@*=+G{h~at2$7)JlBxC@r{BL;_By zrZzz@Iz1IPA=bH^++Al4GCKoq=K;^tFvA?;N0~dU3$kK2ecjIp1aj8xRk-bK#?ReO zWlfF)&w?(D?UH7|>=nl*u2VQ^${E3vwsNPyemHYR5%A+;G8RevZewpTes>gZQ$~(Q94*XF=Tn(oV9`7b?K+XWyAC7U4XzSdj9_T0~nYr zlh4tDk2j?FapP9wLUaCn{}qtXL3?XF@?k^;Nz){kVDe9v2mg|x;i=NM!Z(8K{O((n z5Kk9k@?Xk-|3xp4FNsxkvY0;~7C#NA%H+w@u1t2l+mv^iacZak$6ecekFL4gJ?5P*Wx~B>`l)36eZJ+FXHD+yMRi-$o*3r{iHL-?EjJmU7$9S^eR7m$ zR7=;d(p~SaT|YUuSai#_2h;s}UMrN}`*rP|UqW~N0z9PnM*TDrtcvsE1uikX%#uwp%dIoc6 zhu_8+DP))Nv>@Xs+F5=S16|kLLKZbmH20U}R@EEYJZ-@%ze#!=cL1Ek?GMkMIU_1A zPQ)iH9x&1alerN%Rr%=BzJb8mIK(*Ygzk&!q49x1GfNFw59v$K@`)u3xHjBR^jyDPb%bkNGTCNZFIFr@|PAc$W%B2F~ zubSRm3JDX6rl<&(mrBPgFQldE?(WnTG0%Pr4;0F(@R>*uzyyOGSIBDUB|1_Q&QnJ#Pa8oBjRXax%$9F|I&mG?jU%bdY}60Gn}w%6 zaMoaux6s22p9UgJYdxTdLdBbK%%XY_!K*xv#6x3xj{!@vzRIaXe)DMgh)=?I7PNYa zORs=>gUdU~odHi@J$>rFdgkdR@^deME^&bP*p9hg_P;5&qiXb(ZC@Uaiq{wcH1;}i z&u~I2%pLm2$pH-w34XHXVQcxfhbBTlli!Zll&KoLKl?s;14xO695xF}5;$(!#8VT} ze@4z>5kRcr`bggE`uMyfd`6gbWt*qPS%RD4jHX!M%3)>RMtVFY8x7@Z%{cxajHanQ z=alVFN@Maw^MupMLYX1h>xZxC6zMWY31(x6lgKqH7UpS`rpOHlHxf)bXiUNk3-;`q zSg(6!rOt6sH~O{%raL4IabNkb^R=5Ic zY91+RfR#<%CZD=`8hZ`u&*7KfZ@kl2Zd!3|yLME0J>|%O;abhh%urQO*w%WG@v$(Y zmaCT&dzRT;7Z=4-I2GeOTtr|9$49-UQJ8=edwRf`>nMrNoFM;5NJJq^RY=Rb$gT`XG^By{HzUzEKMR zl0;gDB$q}lY!MK?k?QyAUUUD%bfM+W4_|k;Sc;vS%Z~!w6&`e2E98H+8vFpOKj2=HG9`pzIxSzDNvwyzd2GNB~OvOgJ1UX%fZK!A1yrUng9c^8BK%*;2W+_S+k-yevr7_u--OafkB$hj z+Q%p!_hH^EnCLz)#o=8(iEqo|-stVWz3NEL{Z%<^O|QMW6!3y5fXR4IcSs}4%zaA5 z4Jow+oHl3B>n>b<%@n1C*9(3A2)T?*pwm%!iw_hvXgm%~P{bf~an|zLx9*d8hrRzG zJ<-`=jTR+CvkAaDUGu?5Z)uc(Z3a<{_aGJOLQ75UlJ*%7+#R^pR(rnXk4%#FApi)# zZ0Z$jvzw(CmS5{2Zi-)!yvheejH+x?A&f1#ZEnx<+{=9TKK~3QaLs|Equ|H0hgQF$ z>8>v8x_&^$q>~kIram^JR1iomQLpvHo!}xanZU?GT!hZY4;m&jmLAf#0Ljm@zSUZV zMl&e_sq?^AxuKYhPS|{7KBU;2{*h&{Qq9A)!f#VUmPHpU`4a$|{e)T_c2HOnF)4Ug z10aO16Zu*Rp>mAAZxsT=D_5BDlH0-Zk4Z{{+Xp{k)xJm{=UrbloXnmju$PEFg%-v+ z&6Nz^m~OYdqpbx+Ce$xHRXZ^Gc$m$Ym?3bY}horPSYg^$^&4d#DMS0)hEIv zMmanisN|EK!!s+$1{=**(~!Hp%9|75rnAu5zDZv=57VyhI|A2WLyp7e`odoB0-nm( z@jiWpi_4icMJPq_;Rl}BzB=D&yi}j}5a)S}-ql7Ms}4un-*MJJz!PcJ&s|b-I>!t% zRJy8wfM4zH!n04xEzDrIEt3|qK4uX4NVk``+~C3AnCIcgutzmKER1*Q4_82M`t-z^ zBmDa5Z8M$V~O>n1wdsX`lS#bSJ|HK@j zt(@5T07@#da_&+i9(v>pVpoCF&o0qZJSY~C^o7?6C0^w2@KG6$R^LT8)ChP(&Tj__8CK3#wkvs7$%Pjv_yVU z;U9C59nF~|FomX%RP@U&D8K}F=JoCEDuFj6 zEmBlvGho`aLUzZcf`bG2w9HeeOL4*kjQB?zO8%~<0THD9(RXqW$;h;k?l)Xb;KEn$ z#=J45x%Neg!ld= zNF;y?YO-OF(E6MtKF^D*eo;xv5+@O_^bLMZ(*wEp^1-UP^XKnkZa|{RS3$7?#`OX?30`wSy`;|5bNbWKZh!V|f zJby~4)D&hbrza>yP3Je9IB^1qlI^S9;F>#KQL&L)aNCK}JGXBez*^7##;lBj4@y|r z`-l5)ot8dDC%jv0$4^h2lLwl(*pdQ_GJL$f6H8V&U!#hEUQRbgIhr!=zo+%1!jev$ zJXt;9xSY3y;GjjYz#boLze8)d+D;vHdw zvYqSs+}675@98N!-8x^={kkE0YeVMQ5HC+vP5eX9ai;8*3BrSM{mZMv_x6#?B7A#y z=Wi4?&i#^;)^L`yKLf1}ojWYRoZog|zcXX%4X7~!w>qx=4aS~O(BTl4dB+h0XOn&_ zKYpgaVf=0=)7z*b*M1NXt>FwMzaO~rzlpYIul~3{0|adXLV$*XSy#ci#Dy0#4`7*i z*RL)3$ZH4Y+*H^J!;8KHypw*-d)*Q9qft@0;B5D#&-dy41U-w7EFS@eF-1k?u1CJY zFr$Tbq@7UoSGO)ntzpnM$9hyMRLilq`CHdw zcSdZK4=A}V@nr4PeOQiOz8oDB!_h{$$;h7hKKxoHL~?Qjatka-q@|PXGZu~y6$-RS zG1+D>?$$5y2iN9QO?Svdb^KRKz6(b*G&a(` zoULhLqpa3Bj!e=%Yx%)k{&gT|)Ui1iD5H+LK3m~Xu<8^EL}2jy2jXm}t6M-N3s-Qj zJr!#kD1&*%5)VcBCJeMue%E>sM6M>&m*#9?{dt>oiEqqg!ji1}3(r`lD*IN?C~-)r zDP?pUZej0J@H|!>MO3wVRnOZg^?kmfI3cvG1@#bPUkmzDA)n^MO?U5F$cFc}N6bn6 z{}46Dk|pal@2ZGclPdtAH^b+ShpDfc?FoJFHViku5nwBe`3KUw1 zdl;(_Z(k&1hFd;7N=j0=mC@%2H=&?;%W?So>&u)&o6XF`I@fVavqdpRre9>Prci&8 zbC;dzCope6i=PmOOT^DBluykKlr@APOKSEL^4`<9Lf-C1;m6kvdu{0X1YwSxMPNKQ z@^0526Lx*5ssm2i48f0cC$o5YcP6`MErwJ%V^iaPr_9mAh8b0LGh6p+c_wr5aPp;J zqwNzQB7m$PTh>zAt)qNEciZf(Aur+O$^)JuD-Cry700YR1?2iGxp&xuqqo zh})Je?Vwd=yXw-89m{qAwtuy~RV}uIG*5ybW_Z08k}~4RH#eKvJbWrGIzm!>dKYqt zW>>Ez-z8RW%eitFSemysM{3VnC=)9mR(t9ntrR{>CnX?G-LFac6o2sRth{a;Ara*`EBE_1_#-&;ew^otZf{Ek*zR|S?)(T zZBL!rpfL;qN}mww8w8r-ik)0CMznl@j>t=htkrKP zhMCOFLu7=NCK+s&6oSj6JO*7wb|t0>$xbxRbzU=9!ykFZeS!{5G(H zV2RTU&N&S^Rd5nb*!mXIFD)>ht?Q4P*VH7V#x_@^inb*$lKzaX_PDOD>HT{hxBDYq z4%^CX;dP0o?XM!3KwM(MB=!N}z~x zg0@fDM{DrvU%DyBJWr2Be?Y-h;w0Qe5Ex4-huy%89PKpP)3<&cmNW`hu*O#-6_5Pw z0ASE?JcO1VS}jTc&bWx9f?*IcQLYB+R0Ie~$~wmlDDr3xDBh0>)jfJ7kc9LfbgW21 zPvzKMiUU^r0`-_3etyS}9<`+L<}d-j3+y^?pRw|#07C{N0?r&iK7~>G?0Gj7t3C4h zOtxn&r+dG-MYZBJ243Mv&v%usxvj0OV@Gm}_8S*)V#enR5<>V&Sukywc>ByY%UG~U zoZ=2%_2-w@Dw=*Avv|kf2VG$Uug_~*7(DJ{Cr~cBQfP^+uF(;7%oO>%q^>MtPDVR^ zdJ0Uj2k3^bUl*JvPwwD55?RWK6{!~d%OW^`AkC2W=pg8cF0)<$u|@ZZ94fksuKmigDp@jMy7=+oaA7ex+)1X}|-P2+*OGl}Y zk(n7>h$Re>*Oa+uE#n|$S3P+`;18KI5z)tls(v?^h?jWk2;BQ97y*Y_);y0Ar6sDe zqJOFXoSdFPTjsFGBqMJx7tNma3a>_%iwhEn`}Xx%zn)1ElfBmip`9r=JbE&{@U7P7 zvbD}W%nLDPGbW*3XZaho z9p3MJSqa|+^G(w(uK6-!L9tkhjXQuNeDw47t%NVZ8xn{q&~*gvF_kXA@2tVVKsk(i z$;{TfaOw3et@gQ3M>2gU{T7r;s`fP!USITN4+>Ze3M}US#5wrRPW_YKL)}o<)8-McF@y_fcQxAgH-6P zii$~KOZxg(DRCQr;xWX4iUuD&6}g$K*t|NTdxqH%r)!q%h$bG}aJ*xzl}#Ph8t&+i zo{TDkBT7t6fQ@b0V(S05qcscG)_oGsZwMg%U=w0D8ane2XQ%y!*~W)eJjyp23+wXo zmbT`F2hE}q5(XZwhAdcH+k50N{Q;x>ewG<6KTHb3wr&1I9kM70&){dOLpjseC*g`- zH+x~)5&f+n9U!Eg7N9dLz$u9UrRVE{9kMBSi z?Yg7`o>rDo9f}Ry>w(fAS64^(f4Fn-hP#y9NjsUjZhV>9`2ngF9DhC=I1YigCh6|g zMtfAhznCZCzb8jyj+Gn-H(l+WokNA`5HMenuL*rr)8~%o$m+C1r?7g#LP23z+`pbQ z{&AhY#bJq%5k;%R=Q=cg--W%2E)qg5!tmNdYw%z_Mpem=`ey28Jw|W z2mW1I_a<3PQ5(ZWB@|p_5n7UF*RS8^F3sYgMPYi^uWDEv>;0ccOBiZMGD7KS>CA== zAO4MaVRsiD`x2SPdO8`z`hyW_LZ-kgUaJ8%&@A-D=gxhj7a|+Jxuqrcm*EAE6@PxM z#@MK&5Jm`q6zxh|!q=ZtDjAOAdeA3AzmOhx%>u4(yz7DdBS&(gn96_t5#f8$>h-25 zVAvUq-{;JUg4{xdzirz#I!K@-63~^_d;;$psGLN51L0`t<=KBZ&$9x~pE;A!G5os( zl)~%&OVK|dofxA{?1=g{+H!F@a=%nmwqitZW;!QU^H7)F`y6!L-Q7c1uH2MNz-%hL zBrL*WrRH;^LYo{WP1-vBxKt9vi0_md6r*u-*7ERJHZCP1k8PP7_YyA%ZZS60mFv@I zhtSoK-+1E02Re+BmH&W*E?pWkVl@fdELOYBs|)MRPx<~Lf{`aBDVa&jod;DCbLWZf~}TN~P2f4uhpM%o}e z&U#RmOX$sR4j*&SxYDydaDa8p;lj*kh%gRBm?Rs{q*nM-AJer zwsUS!_v@V&o5oc)&byY0AlD+>fw`lSvd^Flm;An#)*Bj+&AMpC{N&WBPJ!0^?w!l3 z5)^WzQ)rx7{75P9UlAF)tnk&Vsgkp7Wi^Nz!=47Uu-{b(pof-^bWpp!~dB{&T@RgMs-F9K2dC-%`&ne>-1~6Q_>DlF7vkxv*kpd!02|o( zqk!8QvC?aLOBSE z{vU+}b`s5|%BaTY&u53*po1kPrafW)nd#m~Be|%MKojCg1Q*~3Jh_tl;x@PE{Oz7< zI2j5St-Y41LdTpv71_UQ=E>7t*)o@6NvwW{oVg-9txDH$fG}4QaexgeI4&$+{9$-w zgtLir$o02$JA)Oa1tO7`u|rJaGYkNraPqzQponPyWf^_{{_9)R-&LGsetv$4F6tMu zNx#9;O_vnHi_#_T94q>>^=lukVwGWmRrD)djA;9Z*T`MIbV=JNhkA|3BZ?@8W#2kF zf)+0(Hv?SGtX*j*K1Q^K?DGxFB_eid_L8mpW053-X2?q;xi*%)h(<;9j7rR6^Qiwk z%|Nm!T%#x0kTs=kdg;{Bqq1wxGge1HkaluIgpJ#ljW;q6|Idg4IUTX(yWdpz8GF6& zvU*f++yMgj>P>CFKtjF~C>wW;i6K}F+fZ0%NEmXBd)D0$-mpwl-vi-hMsQ1Cw2h8uX%Cfz@R5P}3kFS>Wa!J3mm3S!SB&yq^K1Z7m z(O^XVeA1X!vupS6o`z4UgE7H26W#W#e&(Vx(`WhmqJ4Y(_;Et~1*=jbAXK%rY1vo; zO_GOHu;G8#))u;K8Myygyj7BuyK%NgfGC>v1h)c4CfpLZ(0r$W^J@p<5qw@@Fv@n&RWLAFhUB zu<{rd2PB98@qd>pXX%toRw@bHOwnqR@%#?ww~F8{;x}+RW z=j@XNul*wXDa}FRrA*RTY_sS-sIwPZt!3|0v(b_{*$n8pma>BSDeCv*DBKUSrGmo4 zeUJP6eKq{5jDi9-O&!l#tN_hgkN~+4gdUHWYn!_v7B2KfJZdD;WWz9epyk{k>hyVK zs}#OxgwRqtO2ww{-km&gvCXsf?ut%9!NH;-)riQ(d7A9C%)bFL*3FwVnd*H4elTzj)MQ5Qh6wym*wRiLcB=P%G*t6g>aYpj?elV>b%@OQ^z~~bBO~T0x-={wu=1Gc>+7z0 z?hLQF0SnQPh{CbLIyGpa$e*Q8DUyj8rk&P;RfR64=d0A?cNYJx7 zUkEkSV)+k+*8?vqXQylaWc|ZyO-Al*&AT=k8a*@;irvZilbK%%9Rjo4$%97sESx-O z@Q@*moLJS>iYEK`RP{mS%z*wV)omBBwJSdtBdZOGW-7+_tO_tE4Z_*UptX4Eoakz0 zP0b5{xCjxr36Ww+8H;XDcMYHvy4wBlB!z@hG^vDZ(8$z=So#k2yK>#Xlkp1ODef#! zxi($}8u;L@1};QBb@HTJvC^{Da65pI+>0FpPLMJLLv_<(4kg^aLKWcJfl63t0@8SQ z=qV?O^f?=FYYT@c2$66fbK9h?BdJ|4uYr>tMdjT-#y_|*z&!YDkPn?> z?4R~yCDCw#r2@!*{`~nOj0G4G*;}{HF6O4EUiCR@Nsj{8>57_u7<)@FQS0trv-Ot5 zqzb#iomZYpdFX+-IBuSwoCD~Jad^#7`B@ft?f&NnZ@vo{!PwC7I!`dA{Ml2QV(v1$ z@-?-z(Ckx>g7Ittpi&> z6x6>1qkLaq*hAsq$2WPF6V1ls)5@2ovd02umP6gFtC9Ev9S0t!Z+NCzGnQiObJBqQ z)$L&dt-H*2!;j+zmep+;3~_A6sD~L{lD{R!CAEi20RYpT=Hl zgH8mj)D>EpcTVK*&^>pkn!y^VJq6OF#d2T8gBq>`E%B>aV6Urofd?$XB7epVSyiQ` zLojV_GOK5D!;Up|8py=G)$KT}-un?=m2U=R5p%k9d4mhV9JaNrPHKiAW;CoN5 zA$I#J_$IG9$c_v>^o)P6;4FJ5P;iK$l7uz!DI2B|$M1%#F&pPFfrr%U7X$tUNg60v zN8@FQ;i+~j+q;ia%V&5=^Mb~B6%_aq13a^r@5FiY8yp);tPolWi^p&a73DB!TqY)- zCTh-OV_YwUJRB6~w3N1hY8(R$p-iePY}rtqcuq(92IoGuXtb24Qmad^-?%WQ)myig z#!jptZTj)E5X0zi-o1YxXCAHkod<}|4V@LOj4-i+qhPVY+Cx!$%7b|ub!Y3xb^cGt zD#b;<>}9fxK3YuTCPsUhjvA^t>t6}6QbgtWuzKZ59Q~KJk{cNqsPo%5QtBxRB6z=9 zBfz8uhY$O|Kkmu)>t@3}6&an1rs!X5sF{@KAJCmJ-5DI3PB;XGH;$DLJVR))J_fk@KT5)j((aNLKXts$x4;>;Mc0>Uph z$vBIOvcSUhrOuZkyX;r5S%Xt4BP+cxQ%rX>GMAt-Hq=jEv;1^|Lr2!15l3a0bqg)F zmy|))z2(20^(GX9pM!b1=(ChDWHpY_>;1Ia=V8bSz(6e3qN40e(+?W={Q0r9b+V7o zS5nroqd@=m_uJn;bKKo4NTxtWHB5It@x5>`6s$)>oPe>;8?&1J*mtDNW4-EB@EGzs zoZ2J`)Ol49Zo}vu?>=%D$@j9+n0GMLM&(HF|@^+7sbtu=(}n7 zjvv1hWU&~Tv8k#2wTvr@;lG5Cb*_Ebi*N{J>t8^`pkl{*@Ww_Begj%pUOtT)Eer&$ zmQZV2IToNp{*6J27EY&aRr?%NfXB7<(y;6H44H4d{!C{~fH<>#S+I1Mqk@L$3WNlq~y<#fV2(B^7;$GJVvsHFkEt<}Fh)scvQWaMLs)Mf8;yp^f zB0i)Qr0LKa^PUobhp-=dDDq=YU*9I)8)Sz1EUH~v;sa7SC{uCD<(JfA-tbW*2Erb- zu`sJTa)b-aBPn`JZo`EY3xF~sE^2IP!P*nFWt2K?bYIZgeh* z!+zT_A$GIe(sWQgn-7I64)#3`YE|vapKN>7*4*qpb?R#@U)FpH!3mwk8!jdD)?oEZ zc=RIu@iOFAikokQt6PsAdP*NE8$XOxH+Xg~@!f&H()S^=CMPDM6iXkITfB-dA~npy zcHelqBd@4+6cORC`B&aLj9R_@Z?$x?HWS|D!tsJ`w9RhO=!+`$n*J8=5GpwTh)1=xSsSyU z)Ql~gB_3un_egbNsmOq$_llcTd+8^QA3-$+74FCpz*!iHh-_xf0x3+?bXb{`b~`U8 z_1^1DyLFm61HDQ^oHJX-`rdFKu+sNW#|+2w>&rB|f=~JSRbNZ%`1<0o13aEi_Ur5Sw{7HgZg1CMS9><7NwWgb>@ra-W#5Dv||q>m)h6 zGzCg_jDs$1;qW&m?N1|3{tj7w&@C(?;@5Yl*O*2zzldI&a{^DOU1=?@%x*bzkL1ryeJr)9g{!rJdvPVVb2YC`b-py0o`s#QMS2r90dQ zD>)2q3SxxS&E@?3=~EN66v43)N9K=5Kdyx7Mtm%z$5W8S69u`yZK*YPz!Xk_+ zNMgTwnEcqoIXV9SNek=Tu4P4Wq(jllpKP$hNG zG-uA{25Yp6>iFB@|H2VdE)Uh?-L~*Q|o3@5p?1mA#{0t`uuC5DU>Df zr#5Ys^{|>d`R$gDxwU6-aQi3YHbt69fmjsTr%r!#(gI+#(wSBJ5JiNIt37zTsk;6G zVg8&J)}3boV`F+J3>JUQw*4%bZ01hY4k(;TXgo#ql1Mm4WW&3%|Uh zZUq+7nSuWgosm*5)@0XqPbyY#$PfjSxP{UY$68&`%8VW@dHtwUwnMb2ZNgBi@dj1; zJ1?-lnf(J@7%vuI#HtrfNFFcuHNidA#XswxPd_5I-m0yub6;Yq3bPp)@x>{+1=Qy0 zp8X4xRz9)a{``AwV1t}g3+FI&Ft&=>#HKsyD^kB+<+|lCjeddM)HTbPWiJ!QuQzbc zICwc~Z_p*+a&X2KDBs${IIO3g4v;C5m%H#h=)jaMuSt&M@u61nQhoUB*`fn!b>V?+ zns>h&x}_!tLqV2ULFWQ@Lw-&4Xbq%Y2`ct-#=UN99TWc)DizBSycEc3>#bHQ$oKCB zu$2wo6r_?phlxt}U3xT@K7Fx7P-npPT7gP_aisZVs&RB!;>NU)uin4E3>*&%q@Z^P)6#nUIqF) zifmGljd_yrA;W3SX^5dgYQYmBAl>lF+g^w~5p*l~jK;gJmL#)s&Px;_U# z0g}0q^}D?{E(tKV|NCqCLhH1`DoNKC-0YHbDr`Mc z8bI2M75vCf%juu!9?OjR20J!cLaD8uxIi<^$1+E8G_W18%9^nLFE zc9{yQ`?9ih*#cdW2V`~H#0}j7eg$c(SJEZCTsA^gnSz?JBnHUh(tVWZVuX#Ml$BpOS-=er@^@W9Kj}VG`k-%Ls!ujUL?l_G!F$5z{VF%-7mGXP;J zsSr4^yf_Z8mpK2k+B~7O)Y0ulpy4xT{dC=Rn`@(VozNbYyv1+@#Nae_HM@WVp3Z3T zIZgog)|r6MbR1MYxEqu1-qUpVX3;4q>H216b`{=`0Acj-*SW0FJ9}=o>)ay(u7HMe z+O&GE6l2ZM(6wvV(p^hzF8BYz5SjIj^^u~wy0zVU6+O%;tbO}iecmu6HYblUEo#%i4e0J2_ zz#IshesP+?6GOXzoDM5-x~AaCKM=AWxOoA#;nV7j4+>G=#W9Cjz8q(f_Y~Qb3@}0@ z6c?H9ctU>;f&&(cLn1dH^O1MyZheD;f{GE{hlYlRgbY$1_3rg*fKD-k1y*~gA=B3P z)a|35YuoqarAvavHUElNXF6~ok{!d?iKaWqf&kogZw5e;O@}hHIxCQ>Ln+D3!onpq zTtm(^D(dRGm&ray$?J66zHa_}eEQ>p#~fDSGw>?0mF3{W*J<^&6`;8DJ9=Un*m9#6 zb46&Z*KTckH`egs?ErH#39*gQtvc5AQwH@G32{i+dAQff*oPZVitp_sEtR-;w3{1x z>JvFRVwM>qMS6xkkMAz^BN+I;qdchnn5mr&U^#zSwXGNK>ZXgclPE}4C~d#U;^ z&zKY5EJ?Tu4gI7^%bQ9>YxQcTZTAs+pKKT|9%o)a$F z9R4V3@%7h!8c%(HdNzh8=a3mb_kD}}`X(Hv{f@bfdV8u%KdkUzki+YFcjlU0t~g}* zPyK|W4|{e?RE_#Es$@c^jV_Ak7ymOCFqm~ND=RuD59=@&H*WuBOqZFajiy|ib%7}{ zxEik3(Qoib7dgvW7DcF`S0sxuY0D5S4YZaGHZch(tf$j~^=EKJ9EBm~7a+`#_5?tV z@(;-}{Ja21$5KGdUjTv`-%Z^{DfyZsw``Ht@oU1AT;*^x#z6p~7%9TS1R!ftbxm#I zZU+@-U4wx0H+SvZ2NOE3$Rzr;$*c<&h@UCdaD|#MZd{y>|3~L@nS*R)=gH_0ZvcO{ z__$tq$#yJ>39%Ze2g>D@+8m zQOxVsQo9w@#l)B$sg7AC7p+i>ectTIDh(6&K^a?QW%qtNoEh1`u3!=XxlFg0x=b$> z%RMuF4X2Z1b=-#?7eTR!*e=Nh(HCC%Nah7Y!q!%3@JOGPd{2*9^Iu|#$Txbdf2(#j zzka=n!dh_B&NovXEjnEMtCsgF@_U@J3r>YO1T%?r-aUiCAb=H;YJeO&ZbgoK4mYU% zEHJre(uvOflKO}kK79KH2$ORTG07-KjW$MYNJl&hPIF*_8$E-yRb0%AIyU`G2!wv6n5ZZCdn$>O+z5DVHH*D`(bJ^$U=p1KmH(u9%#4(&~ zd-rN{K=1~iKkrLVZfh&ImD#NL`{Y&+FN+DM2sXPp%+z$#-73cw2O5IrotpXX)`87- zh$tW6?vF@;nc2tZqb``*Z0gd^ByYn9-O@H4-Lo$8`&^<+rc^)Iuv*W?7g?{XyB0d` zQpM=;adYA?B}RH)KXN*A&u`_DZbN06J$KN=8eZ&mz4!Xek%P)KzI-ci3ozHXps#dP z`_DJ{%c9@LQ$SIp5c_+I;6-8@Y&_J-NOX^xRBvbNUL;m-jJ4vjpfGxZ+r)Y`Qp{W_C&zjlu` zQWhyuiy749t+(IOE&g)1%d4uEPQTt__7a6oM}}P1X;{}=IXMTj9&h)iRefK7@N_?X z%~o_O5D8V@_{F^%iqb;mXQgIq6?9DkGoWA;EgFmIrjXa2ks-`81#iLnmnaKp(P+Ay zgF*qsX^iR$=9bayH+eg5P8Q~;rGDHs4v?y3bb7`=JrtS9d zLCsO;rXQy)?mA%@>Iw0%)#u4nGrvzIkuGL*8C|j(mFo+FC(D}a&e!%oMQlO&jT>7o z^f>x3d3uDz53m^>H8me!-(uKbI52prN^YB1>zY+xmzTbxIu_=hQ?`iMJ?!TmKX_ok z>=Q(ZLpv4)w1p>c)!9pE_l|3#9U>i*6`W{v;GSY^X>{SHn=Sj0cwV_On}l~v zTZ=>WMvSNjji2U%`(Rj?a8gaWQNeEuN7|4kxfLDCCq9Xdwp~r9O3NACH)ZGE!7$-Lq+5(1=4Aw|X zza}&NosHF#wL_xbbWR%a)WbbFD7Z_c)rstcTx3a zV@=B@`{IRSF-=3FevD-zn?EW>MC4Ep#ZJP^t{`Q-al;jRXe#MzvP04&G}gQrHcGi; z-e5(y3F=1c{wS>My1sk;k{6;@PhYuRTTt;2TOH3#{2k#==jG9?mQ0U3)guW9#-_N0 zlGsWG8^zAr>cj89i*Z6>qtP=CsNo5NG*0%ZzLN@ynFmLLXfsHLGt)#lDNAkhd`-s(zaO5Aw0z^4_(r2op9GUj_&vv6 zsiS1sv2EKW)O968Lzga1+O%o$qD8;)S;XZw>xR+|k>AU$_mY+&({X5nON^t-#1gM< zW4tE=`(uJah32|Xnh#k{-F zZ;<`0;Tk{PoBz`EDZYGsxXyIlYc++ycK9^V**P@o)({Dm?Ez>2fr*A~dO$z`=mt!3 z!R(BulbWkP?^O8uF}b@(Ebnhqz}j~{EV>%phshir4Z>=s1XgrBD_7e6?s^bds3E_q z>)@8km%4_wy{rsN)GEfEk_k&46YT4UCnMyYg~{*I9O05RK`yATmr-@s|AQt!+qIw* zs#L2bj-HfEA=fKCl%*C)DOH8_sml8turYd=q8<3UU!KVPfBN@-#dp4bF>CNBx)j=@ zwJ$TK(n%y%-g2Bh^pWmT$ER0*T75PA=YEg{w>u%zC-7S!qltbx z?P~%BpBs!+y_gBqr6eHvWP8oLxU!et1{xWL97N90Wgwg_C-}|Ch;S@GcKPz1Emaba zYB}m6zg=tu{~7?N%Ee_ve0rFV!EnLZp7Z2^Wx~WsK~I((($E?18dq-Je4OZY%lBRDDXA;%9s!* zKWw|IKS|tBzv7SaG*25$7|!1*--vG?7$E$4y8K@AMb*(UOPEOI%-jBbFK3+1WuAe*E}cety#aJ0Y26&+F?) zT&Yg4d?~G`@-@yUO}YUwm)>zLhhDfo4qZQ4mvB2*15MlY!@0o1`V096+K)tjOKjF$ zu|+*})fITo*lMY#>=J*zZGW1ibR~diz{8p+C27w$gQtNbu7zwwD=sZ%Jo-WUyVv65 zqHnecDl1xDqP>LSGa+90N5l6N+9&GQQ?K&9jN9J)vs$u#c1{BK1T+Z*tO(Tc$P}nc z2VMm?D7!(2Z48}To%8n>NtX(; zwY&c&m1$Pmu6RSL@jL2kc3b_zv!JaI{6IQ|DQ5Ttnlb3nTzF_tnPL@2{f!J*!SFZO zVmBKBXEEgAiqh^T5=-W8Dw4&vjM|*SaNPLuQi1zo`!xEW!ZQ>Q1VI5VN|shuTTGm! zt<&ARSjhqp%4J&I3kk&l8VJG&pol{|Mic;247x8Q&(Ia_(7CJn%6*vnPnn%}0xevf zPHy`8b!Enwj4fL}!Df(?4GYyf7)j9;my^fF7eX@J^``aEd6d6E6U7BeV`V+89kmb%w3ez-{)w?rt+{x$4fn{d$egR;VI+F zAad!`kjPG1T|Zh^>$)#@B6Y9(7q)tr-^p9Hw{M?w0}l7-Yxv4h+tR*zfP=Ywen02J zGrI;%e#@S{ioVQ!;mtWp?@G@%u4?!$GUhLVo*h{bw2^Pqc8Fe+mrC6B3qmi$UbG_2 zKEN2mmKJQX$W3T2q;!PR@gIs?z%GOpN79X$*jvF7Xy_mllbKf|N9~f@+wS+pO4J^@ z_|{wd*SyI;^=iW6+X|<2-A&V757b{+@y}&@#lF%a8n_%YriaH2ZtC|BAQr5w*^u_M zss&F(I1rmN?r<363@1II3%JWr5{f#@ z`}~Obvu?+PqH3&OH_lH2VD9`85nCzX8*x|lEp~)MdOuTa!#fLDFSg7>qX6T=zs{p6c7I$uH0Z1pPbi zoNih#IiuVDrwDC}MDWp;Id#U2vAUnCN8Jsw5EGMs5I?ckj!pFoH>ISAYd@fRQXI*_ zFyTLM?wPdcix#~Xf4V8t|5uUv2fPsWquuPIeB0q}?S{URQ9&#adic@a+*?dZ71Gkz zHJ&5mfp>`h-K?H{U}v`5?$)-$hpQg^eN*AQuuINDNk5}M!@=w6F`lEU=Met~V-D)q zpbU|wyDgoHJ@Jq++!Nh4v7%;Y+n8Hoiz1!PZ<87+p*-4~&Z|B|#m4ffZ!saH3sAd& ztZX(Ma!l%!CG_i+`|DML&mriK@!R3fA1ah$6A0M!R+Uuu53qCUx^`A#&Z|&sYA)KY(E0E_jSLMrno%^Ju7Af! z1)V~Ho^ij{-d<;L24!Y?`BIF9zOG2ilKw0GAjop&V#p>C+|cD7`C9STYo$xWAqZWe zB6TMYJRf*rP}1t@??cAkvSk2@sjQlqKQ4xOvN0R~HB;xQU(XLH^!AlLV%NNP{bBJ)jXX_7D{XlU z?w~`Gl7~=@S%*ME#6sr40r3g3p^`WH>Ap`(b!s|Z6Ohz8?trFL=lEJ(c<@_SPWkFK z!EB#W)mRS?$eSZLT;1FfdZo!`r6M^KJ)8106KZ$KG~F2tp$r(P`D_l>T);bXvSR%Kf!-%Ki1e-0ccx9H3H(+Wx?bksmnJ6MĖRGP-dR}k zKEBV!u7Yc2osVU9{l;sarrCI)WavSIEwVjVIXFZoB)A0!x3kL=tu;c+7JWEWzYxCC zlAs`bF8FFfQW7;2l@o4j`PyQuDBx*TnS?=TAQwY?vdt{tY9oW8OFPv9Zuc9xvWjsL zHxjQ`RzcxgS65McuB$@hmI0W_GYM*azkkGXR}(ed_PE)8uI#O~z&H5&5tTDb7G2j~ zcgpt=-K?pEt7u}O_bB<6OHL;rBMX3c?e`fS)7N>sihb8ESu$jUPQg0qJ(ByyHyB*d zAyG)+4Nngb3NkivH8Zk34&!!K3gjja51Fne=mW_)=#Cjy!0!MXtC|U!x}U|qi!Jmy`Y`9ga^)B6MMR=SJX{T;gbzAjGwgp}q? zl^fSuIz^6R$9~M`pVO;gP^E2BH>j@i!wC~oeJ^Qg2T#>miYz!R{UH2|>zOg`w4|lJ zr)b(J`C&I_*POS>($ekK9E37QV$=3gKkSTJbHqk-?yz?zs>aX@kQRaxigm5wZrLzJ)S(9Foq{%8F?0@V=v zb>1EMQPE_XTZMYCv1Tgk>Ar&GAD`&YnsV`UUf!eTbQ~e`T-5|Lzbrw4ds>>ypI902z(GJbDhkE>n-8k;N{*2?%?lKtGOuZ+V#E