Merge configurable fields with previous checkpoint config before each run (#510)

* Merge configurable fields with previous checkpoint config.

* Update update_state() and aupdate_state() to merge configurable fields with previous checkpoint config.

* Update tests to verify that all checkpoint metadata contain the expected configurable field keys. This assertion is needed because a run can have an arbitrary number of steps based on the construction of the graph.
This commit is contained in:
Andrew Nguonly
2024-05-21 11:03:25 -07:00
committed by GitHub
parent d3cce1e245
commit 08b505e017
4 changed files with 433 additions and 6 deletions
+63
View File
@@ -1,6 +1,9 @@
import asyncio
from collections import defaultdict
from typing import Any, Optional
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.base import (
Checkpoint,
CheckpointMetadata,
@@ -46,3 +49,63 @@ class MemorySaverAssertImmutable(MemorySaver):
)
# call super to write checkpoint
return super().put(config, checkpoint, metadata)
class MemorySaverAssertCheckpointMetadata(MemorySaver):
"""This custom checkpointer is for verifying that a run's configurable
fields are merged with the previous checkpoint config for each step in
the run. This is the desired behavior. Because the checkpointer's (a)put()
method is called for each step, the implementation of this checkpointer
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,
checkpoint: Checkpoint,
metadata: Optional[CheckpointMetadata] = None,
) -> None:
"""The implementation of put() merges config["configurable"] (a run's
configurable fields) with the metadata field. The state of the
checkpoint metadata can be asserted to confirm that the run's
configurable fields were merged with the previous checkpoint config.
"""
configurable = config["configurable"].copy()
# remove thread_ts to make testing simpler
configurable.pop("thread_ts", None)
self.storage[config["configurable"]["thread_id"]].update(
{
checkpoint["id"]: (
self.serde.dumps(checkpoint),
# merge configurable fields and metadata
self.serde.dumps({**configurable, **metadata}),
)
}
)
return {
"configurable": {
"thread_id": config["configurable"]["thread_id"],
"thread_ts": checkpoint["id"],
}
}
async def aput(
self,
config: RunnableConfig,
checkpoint: Checkpoint,
metadata: CheckpointMetadata,
) -> RunnableConfig:
return await asyncio.get_running_loop().run_in_executor(
None, self.put, config, checkpoint, metadata
)