mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-26 19:45:00 +02:00
Shared state conceptual docs (#1958)
Co-authored-by: Harrison Chase <hw.chase.17@gmail.com> Co-authored-by: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com>
This commit is contained in:
co-authored by
Harrison Chase
William Fu-Hinthorn
parent
61798d09d2
commit
d5da547850
@@ -216,6 +216,148 @@ The final thing you can optionally specify when calling `update_state` is `as_no
|
||||
|
||||

|
||||
|
||||
## Memory Store
|
||||
|
||||

|
||||
|
||||
A [state schema](low_level.md#schema) specifies a set of keys that are populated as a graph is executed. As discussed above, state can be written by a checkpointer to a thread at each graph step, enabling state persistence.
|
||||
|
||||
But, what if we want to retrain some information *across threads*? Consider the case of a chatbot where we want to retain specific information about the user across *all* chat conversations (e.g., threads) with that user!
|
||||
|
||||
With checkpointers alone, we cannot share information across threads. This motivates the need for the `Store` interface. As an illustration, we can define an `InMemoryStore` to store information about a user across threads. We simply compile our graph with a checkpointer, as before, and will our new `in_memory_store`.
|
||||
First, let's showcase this in isolation without using LangGraph.
|
||||
|
||||
```python
|
||||
from langgraph.store.memory import InMemoryStore
|
||||
in_memory_store = InMemoryStore()
|
||||
```
|
||||
|
||||
Memories are namespaced by a `tuple`, which in this specific example will be `(<user_id>, "memories")`. The namespace can be any length and represent anything, does not have be user specific.
|
||||
|
||||
```python
|
||||
user_id = "1"
|
||||
namespace_for_memory = (user_id, "memories")
|
||||
```
|
||||
|
||||
We use the `store.put` to save memories to our namespace in the store. When we do this, we specify the namespace, as defined above, and a key-value pair for the memory: the key is simply a unique identifier for the memory (`memory_id`) and the value (a dictionary) is the memory itself.
|
||||
|
||||
```python
|
||||
memory_id = str(uuid.uuid4())
|
||||
memory = {"food_preference" : "I like pizza"}
|
||||
in_memory_store.put(namespace_for_memory, memory_id, memory)
|
||||
```
|
||||
|
||||
We can read out memories in our namespace using `store.search`, which will return all memories for a given user as a list. The most recent memory is the last in the list.
|
||||
|
||||
```python
|
||||
memories = in_memory_store.search(namespace_for_memory)
|
||||
memories[-1].dict()
|
||||
{'value': {'food_preference': 'I like pizza'},
|
||||
'key': '07e0caf4-1631-47b7-b15f-65515d4c1843',
|
||||
'namespace': ['1', 'memories'],
|
||||
'created_at': '2024-10-02T17:22:31.590602+00:00',
|
||||
'updated_at': '2024-10-02T17:22:31.590605+00:00'}
|
||||
```
|
||||
|
||||
Each memory type is a Python class with certain attributes. We can access it as a dictionary by converting via `.dict` as above.
|
||||
The attributes it has are:
|
||||
|
||||
- `value`: The value (itself a dictionary) of this memory
|
||||
- `key`: The UUID for this memory in this namespace
|
||||
- `namespace`: A list of strings, the namespace of this memory type
|
||||
- `created_at`: Timestamp for when this memory was created
|
||||
- `updated_at`: Timestamp for when this memory was updated
|
||||
|
||||
With this all in place, we use the `in_memory_store` in LangGraph. The `in_memory_store` works hand-in-hand with the checkpointer: the checkpointer saves state to threads, as discussed above, and the the `in_memory_store` allows us to store arbitrary information for access *across* threads. We compile the graph with both the checkpointer and the `in_memory_store` as follows.
|
||||
|
||||
```python
|
||||
from langgraph.checkpoint.memory import MemorySaver
|
||||
|
||||
# We need this because we want to enable threads (conversations)
|
||||
checkpointer = MemorySaver()
|
||||
|
||||
# ... Define the graph ...
|
||||
|
||||
# Compile the graph with the checkpointer and store
|
||||
graph = graph.compile(checkpointer=checkpointer, store=in_memory_store)
|
||||
```
|
||||
|
||||
We invoke the graph with a `thread_id`, as before, and also with a `user_id`, which we'll use to namespace our memories to this particular user as we showed above.
|
||||
|
||||
```python
|
||||
# Invoke the graph
|
||||
user_id = "1"
|
||||
config = {"configurable": {"thread_id": "1", "user_id": user_id}}
|
||||
|
||||
# First let's just say hi to the AI
|
||||
for update in graph.stream(
|
||||
{"messages": [{"role": "user", "content": "hi"}]}, config, stream_mode="updates"
|
||||
):
|
||||
print(update)
|
||||
```
|
||||
|
||||
We can access the `in_memory_store` and the `user_id` in *any node* by passing `store: BaseStore` and `config: RunnableConfig` as node arguments. Just as we saw above, simply use the `put` method to save memories to the store.
|
||||
|
||||
```python
|
||||
def update_memory(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
|
||||
|
||||
# Get the user id from the config
|
||||
user_id = config["configurable"]["user_id"]
|
||||
|
||||
# Namespace the memory
|
||||
namespace = (user_id, "memories")
|
||||
|
||||
# ... Analyze conversation and create a new memory
|
||||
|
||||
# Create a new memory ID
|
||||
memory_id = str(uuid.uuid4())
|
||||
|
||||
# We create a new memory
|
||||
store.put(namespace, memory_id, {"memory": memory})
|
||||
|
||||
```
|
||||
|
||||
As we showed above, we can also access the store in any node and use `search` to get memories. Recall the the memories are returned as a list of objects that can be converted to a dictionary.
|
||||
|
||||
```python
|
||||
memories[-1].dict()
|
||||
{'value': {'food_preference': 'I like pizza'},
|
||||
'key': '07e0caf4-1631-47b7-b15f-65515d4c1843',
|
||||
'namespace': ['1', 'memories'],
|
||||
'created_at': '2024-10-02T17:22:31.590602+00:00',
|
||||
'updated_at': '2024-10-02T17:22:31.590605+00:00'}
|
||||
```
|
||||
|
||||
We can access the memories and use them in our model call.
|
||||
|
||||
```python
|
||||
def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore):
|
||||
|
||||
# Get the user id from the config
|
||||
user_id = config["configurable"]["user_id"]
|
||||
|
||||
# Get the memories for the user from the store
|
||||
memories = store.search(("memories", user_id))
|
||||
info = "\n".join([d.value["memory"] for d in memories])
|
||||
|
||||
# ... Use memories in the model call
|
||||
```
|
||||
|
||||
If we create a new thread, we can still access the same memories so long as the `user_id` is the same.
|
||||
|
||||
```python
|
||||
# Invoke the graph
|
||||
config = {"configurable": {"thread_id": "2", "user_id": "1"}}
|
||||
|
||||
# Let's say hi again
|
||||
for update in graph.stream(
|
||||
{"messages": [{"role": "user", "content": "hi, tell me about my memories"}]}, config, stream_mode="updates"
|
||||
):
|
||||
print(update)
|
||||
```
|
||||
|
||||
When we use the LangGraph API, either locally (e.g., in LangGraph Studio) or with LangGraph Cloud, the memory store is available to use by default and does not need to be specified during graph compilation.
|
||||
|
||||
## Checkpointer libraries
|
||||
|
||||
Under the hood, checkpointing is powered by checkpointer objects that conform to [BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver] interface. LangGraph provides several checkpointer implementations, all implemented via standalone, installable libraries:
|
||||
|
||||
Reference in New Issue
Block a user