diff --git a/docs/docs/concepts/persistence.md b/docs/docs/concepts/persistence.md index 678b66a25..8b9981611 100644 --- a/docs/docs/concepts/persistence.md +++ b/docs/docs/concepts/persistence.md @@ -4,6 +4,10 @@ LangGraph has a built-in persistence layer, implemented through checkpointers. W  +!!! info "LangGraph API handles checkpointing automatically" + + When using the LangGraph API, you don't need to implement or configure checkpointers manually. The API handles all persistence infrastructure for you behind the scenes. + ## Threads A thread is a unique ID or [thread identifier](#threads) assigned to each checkpoint saved by a checkpointer. When invoking graph with a checkpointer, you **must** specify a `thread_id` as part of the `configurable` portion of the config: @@ -26,7 +30,7 @@ Let's see what checkpoints are saved when a simple graph is invoked as follows: ```python from langgraph.graph import StateGraph, START, END -from langgraph.checkpoint.memory import MemorySaver +from langgraph.checkpoint.memory import InMemorySaver from typing import Annotated from typing_extensions import TypedDict from operator import add @@ -49,7 +53,7 @@ workflow.add_edge(START, "node_a") workflow.add_edge("node_a", "node_b") workflow.add_edge("node_b", END) -checkpointer = MemorySaver() +checkpointer = InMemorySaver() graph = workflow.compile(checkpointer=checkpointer) config = {"configurable": {"thread_id": "1"}} @@ -223,6 +227,10 @@ But, what if we want to retain some information *across threads*? Consider the c With checkpointers alone, we cannot share information across threads. This motivates the need for the [`Store`](../reference/store.md#langgraph.store.base.BaseStore) 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 with our new `in_memory_store` variable. +!!! info "LangGraph API handles stores automatically" + + When using the LangGraph API, you don't need to implement or configure stores manually. The API handles all storage infrastructure for you behind the scenes. + ### Basic Usage First, let's showcase this in isolation without using LangGraph. @@ -324,10 +332,10 @@ store.put( 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 `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 +from langgraph.checkpoint.memory import InMemorySaver # We need this because we want to enable threads (conversations) -checkpointer = MemorySaver() +checkpointer = InMemorySaver() # ... Define the graph ... @@ -440,6 +448,7 @@ Under the hood, checkpointing is powered by checkpointer objects that conform to * `langgraph-checkpoint-sqlite`: An implementation of LangGraph checkpointer that uses SQLite database ([SqliteSaver][langgraph.checkpoint.sqlite.SqliteSaver] / [AsyncSqliteSaver][langgraph.checkpoint.sqlite.aio.AsyncSqliteSaver]). Ideal for experimentation and local workflows. Needs to be installed separately. * `langgraph-checkpoint-postgres`: An advanced checkpointer that uses Postgres database ([PostgresSaver][langgraph.checkpoint.postgres.PostgresSaver] / [AsyncPostgresSaver][langgraph.checkpoint.postgres.aio.AsyncPostgresSaver]), used in LangGraph Cloud. Ideal for using in production. Needs to be installed separately. + ### Checkpointer interface Each checkpointer conforms to [BaseCheckpointSaver][langgraph.checkpoint.base.BaseCheckpointSaver] interface and implements the following methods: @@ -452,7 +461,7 @@ Each checkpointer conforms to [BaseCheckpointSaver][langgraph.checkpoint.base.Ba If the checkpointer is used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), asynchronous versions of the above methods will be used (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`). !!! note Note - For running your graph asynchronously, you can use `MemorySaver`, or async versions of Sqlite/Postgres checkpointers -- `AsyncSqliteSaver` / `AsyncPostgresSaver` checkpointers. + For running your graph asynchronously, you can use `InMemorySaver`, or async versions of Sqlite/Postgres checkpointers -- `AsyncSqliteSaver` / `AsyncPostgresSaver` checkpointers. ### Serializer diff --git a/docs/docs/how-tos/persistence-functional.ipynb b/docs/docs/how-tos/persistence-functional.ipynb index 7b9181992..91c1f786d 100644 --- a/docs/docs/how-tos/persistence-functional.ipynb +++ b/docs/docs/how-tos/persistence-functional.ipynb @@ -16,6 +16,10 @@ " - [Memory](../../concepts/memory/)\n", " - [Chat Models](https://python.langchain.com/docs/concepts/chat_models/)\n", "\n", + "!!! info \"Not needed for LangGraph API users\"\n", + "\n", + " If you're using the LangGraph API, you needn't manually implement a checkpointer. The API automatically handles checkpointing for you. This guide is relevant when implementing LangGraph in your own custom server.\n", + "\n", "Many AI applications need memory to share context across multiple interactions on the same [thread](../../concepts/persistence#threads) (e.g., multiple turns of a conversation). In LangGraph functional API, this kind of memory can be added to any [entrypoint()][langgraph.func.entrypoint] workflow using [thread-level persistence](https://langchain-ai.github.io/langgraph/concepts/persistence).\n", "\n", "When creating a LangGraph workflow, you can set it up to persist its results by using a [checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver):\n", diff --git a/docs/docs/how-tos/persistence.ipynb b/docs/docs/how-tos/persistence.ipynb index 4bbcfee9a..608add87c 100644 --- a/docs/docs/how-tos/persistence.ipynb +++ b/docs/docs/how-tos/persistence.ipynb @@ -31,6 +31,10 @@ "
\n", " \n", "\n", + "!!! info \"Not needed for LangGraph API users\"\n", + "\n", + " If you're using the LangGraph API, you needn't manually implement a checkpointer. The API automatically handles checkpointing for you. This guide is relevant when implementing LangGraph in your own custom server.\n", + "\n", "Many AI applications need memory to share context across multiple interactions. In LangGraph, this kind of memory can be added to any [StateGraph](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph) using [thread-level persistence](https://langchain-ai.github.io/langgraph/concepts/persistence) .\n", "\n", "When creating any LangGraph graph, you can set it up to persist its state by adding a [checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#basecheckpointsaver) when compiling the graph:\n", diff --git a/docs/docs/how-tos/persistence_postgres.ipynb b/docs/docs/how-tos/persistence_postgres.ipynb index 332c98cae..e32f1d2d1 100644 --- a/docs/docs/how-tos/persistence_postgres.ipynb +++ b/docs/docs/how-tos/persistence_postgres.ipynb @@ -26,6 +26,10 @@ " \n", " \n", "\n", + "!!! info \"Not needed for LangGraph API users\"\n", + "\n", + " If you're using the LangGraph API, you needn't manually implement a checkpointer. The API automatically handles checkpointing for you. This guide is relevant when implementing LangGraph in your own custom server.\n", + "\n", "When creating LangGraph agents, you can also set them up so that they persist their state. This allows you to do things like interact with an agent multiple times and have it remember previous interactions.\n", "\n", "This how-to guide shows how to use `Postgres` as the backend for persisting checkpoint state using the [`langgraph-checkpoint-postgres`](https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres) library.\n", @@ -44,7 +48,7 @@ "...\n", "```\n", "\n", - "!!! info \"Setup\"", + "!!! info \"Setup\"\n", "\n", " You need to run `.setup()` once on your checkpointer to initialize the database before you can use it." ] diff --git a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py index 28e1696f1..863048240 100644 --- a/libs/checkpoint/langgraph/checkpoint/memory/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/memory/__init__.py @@ -38,6 +38,8 @@ class InMemorySaver( Only use `InMemorySaver` for debugging or testing purposes. For production use cases we recommend installing [langgraph-checkpoint-postgres](https://pypi.org/project/langgraph-checkpoint-postgres/) and using `PostgresSaver` / `AsyncPostgresSaver`. + If you are using the LangGraph Platform, no checkpointer needs to be specified. The correct managed checkpointer will be used automatically. + Args: serde (Optional[SerializerProtocol]): The serializer to use for serializing and deserializing checkpoints. Defaults to None. diff --git a/libs/langgraph/bench/__main__.py b/libs/langgraph/bench/__main__.py index 033b5034b..966e25d1d 100644 --- a/libs/langgraph/bench/__main__.py +++ b/libs/langgraph/bench/__main__.py @@ -9,6 +9,7 @@ from bench.fanout_to_subgraph import fanout_to_subgraph, fanout_to_subgraph_sync from bench.pydantic_state import pydantic_state from bench.react_agent import react_agent from bench.sequential import create_sequential +from bench.wide_dict import wide_dict from bench.wide_state import wide_state from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import StateGraph @@ -251,6 +252,102 @@ benchmarks = ( ] }, ), + ( + "wide_dict_25x300", + wide_dict(300).compile(checkpointer=None), + wide_dict(300).compile(checkpointer=None), + { + "messages": [ + { + str(i) * 10: { + str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5 + for j in range(5) + } + for i in range(5) + } + ] + }, + ), + ( + "wide_dict_25x300_checkpoint", + wide_dict(300).compile(checkpointer=MemorySaver()), + wide_dict(300).compile(checkpointer=MemorySaver()), + { + "messages": [ + { + str(i) * 10: { + str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5 + for j in range(5) + } + for i in range(5) + } + ] + }, + ), + ( + "wide_dict_15x600", + wide_dict(600).compile(checkpointer=None), + wide_dict(600).compile(checkpointer=None), + { + "messages": [ + { + str(i) * 10: { + str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5 + for j in range(5) + } + for i in range(3) + } + ] + }, + ), + ( + "wide_dict_15x600_checkpoint", + wide_dict(600).compile(checkpointer=MemorySaver()), + wide_dict(600).compile(checkpointer=MemorySaver()), + { + "messages": [ + { + str(i) * 10: { + str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5 + for j in range(5) + } + for i in range(3) + } + ] + }, + ), + ( + "wide_dict_9x1200", + wide_dict(1200).compile(checkpointer=None), + wide_dict(1200).compile(checkpointer=None), + { + "messages": [ + { + str(i) * 10: { + str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5 + for j in range(3) + } + for i in range(3) + } + ] + }, + ), + ( + "wide_dict_9x1200_checkpoint", + wide_dict(1200).compile(checkpointer=MemorySaver()), + wide_dict(1200).compile(checkpointer=MemorySaver()), + { + "messages": [ + { + str(i) * 10: { + str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5 + for j in range(3) + } + for i in range(3) + } + ] + }, + ), ( "sequential_10", create_sequential(10).compile(), diff --git a/libs/langgraph/bench/wide_dict.py b/libs/langgraph/bench/wide_dict.py new file mode 100644 index 000000000..2f0df75ca --- /dev/null +++ b/libs/langgraph/bench/wide_dict.py @@ -0,0 +1,153 @@ +import operator +from functools import partial +from random import choice +from typing import Annotated, Optional, Sequence + +from typing_extensions import TypedDict + +from langgraph.constants import END, START +from langgraph.graph.state import StateGraph + + +def wide_dict(n: int) -> StateGraph: + class State(TypedDict): + messages: Annotated[list, operator.add] + trigger_events: Annotated[list, operator.add] + """The external events that are converted by the graph.""" + primary_issue_medium: Annotated[str, lambda x, y: y or x] + autoresponse: Annotated[Optional[dict], lambda _, y: y] # Always overwrite + issue: Annotated[dict | None, lambda x, y: y if y else x] + relevant_rules: Optional[list[dict]] + """SOPs fetched from the rulebook that are relevant to the current conversation.""" + memory_docs: Optional[list[dict]] + """Memory docs fetched from the memory service that are relevant to the current conversation.""" + categorizations: Annotated[list[dict], operator.add] + """The issue categorizations auto-generated by the AI.""" + responses: Annotated[list[dict], operator.add] + """The draft responses recommended by the AI.""" + + user_info: Annotated[Optional[dict], lambda x, y: y if y is not None else x] + """The current user state (by email).""" + crm_info: Annotated[Optional[dict], lambda x, y: y if y is not None else x] + """The CRM information for organization the current user is from.""" + email_thread_id: Annotated[ + Optional[str], lambda x, y: y if y is not None else x + ] + """The current email thread ID.""" + slack_participants: Annotated[dict, operator.or_] + """The growing list of current slack participants.""" + bot_id: Optional[str] + """The ID of the bot user in the slack channel.""" + notified_assignees: Annotated[dict, operator.or_] + + list_fields = { + "messages", + "trigger_events", + "categorizations", + "responses", + "memory_docs", + "relevant_rules", + } + dict_fields = { + "user_info", + "crm_info", + "slack_participants", + "notified_assignees", + "autoresponse", + "issue", + } + + def read_write(read: str, write: Sequence[str], input: State) -> dict: + val = input.get(read) + val = {val: val} if isinstance(val, str) else val + val_single = val[-1] if isinstance(val, list) else val + val_list = val if isinstance(val, list) else [val] + return { + k: val_list + if k in list_fields + else val_single + if k in dict_fields + else "".join(choice("abcdefghijklmnopqrstuvwxyz") for _ in range(n)) + for k in write + } + + builder = StateGraph(State) + builder.add_edge(START, "one") + builder.add_node( + "one", + partial(read_write, "messages", ["trigger_events", "primary_issue_medium"]), + ) + builder.add_edge("one", "two") + builder.add_node( + "two", + partial(read_write, "trigger_events", ["autoresponse", "issue"]), + ) + builder.add_edge("two", "three") + builder.add_edge("two", "four") + builder.add_node( + "three", + partial(read_write, "autoresponse", ["relevant_rules"]), + ) + builder.add_node( + "four", + partial( + read_write, + "trigger_events", + ["categorizations", "responses", "memory_docs"], + ), + ) + builder.add_node( + "five", + partial( + read_write, + "categorizations", + [ + "user_info", + "crm_info", + "email_thread_id", + "slack_participants", + "bot_id", + "notified_assignees", + ], + ), + ) + builder.add_edge(["three", "four"], "five") + builder.add_edge("five", "six") + builder.add_node( + "six", + partial(read_write, "responses", ["messages"]), + ) + builder.add_conditional_edges( + "six", lambda state: END if len(state["messages"]) > n else "one" + ) + + return builder + + +if __name__ == "__main__": + import asyncio + + import uvloop + + from langgraph.checkpoint.memory import MemorySaver + + graph = wide_dict(1000).compile(checkpointer=MemorySaver()) + input = { + "messages": [ + { + str(i) * 10: { + str(j) * 10: ["hi?" * 10, True, 1, 6327816386138, None] * 5 + for j in range(50) + } + for i in range(50) + } + ] + } + config = {"configurable": {"thread_id": "1"}, "recursion_limit": 20000000000} + + async def run(): + async for c in graph.astream(input, config=config): + print(c.keys()) + + uvloop.install() + asyncio.run(run()) diff --git a/libs/langgraph/bench/wide_state.py b/libs/langgraph/bench/wide_state.py index ac6f190f3..b331be6ec 100644 --- a/libs/langgraph/bench/wide_state.py +++ b/libs/langgraph/bench/wide_state.py @@ -1,6 +1,7 @@ import operator from dataclasses import dataclass, field from functools import partial +from random import choice from typing import Annotated, Optional, Sequence from langgraph.constants import END, START @@ -49,12 +50,34 @@ def wide_state(n: int) -> StateGraph: """The ID of the bot user in the slack channel.""" notified_assignees: Annotated[dict, operator.or_] = field(default_factory=dict) + list_fields = { + "messages", + "trigger_events", + "categorizations", + "responses", + "memory_docs", + "relevant_rules", + } + dict_fields = { + "user_info", + "crm_info", + "slack_participants", + "notified_assignees", + "autoresponse", + "issue", + } + def read_write(read: str, write: Sequence[str], input: State) -> dict: val = getattr(input, read) + val = {val: val} if isinstance(val, str) else val val_single = val[-1] if isinstance(val, list) else val val_list = val if isinstance(val, list) else [val] return { - k: val_list if isinstance(getattr(input, k), list) else val_single + k: val_list + if k in list_fields + else val_single + if k in dict_fields + else "".join(choice("abcdefghijklmnopqrstuvwxyz") for _ in range(n)) for k in write } diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json index 3be321092..64ddcf047 100644 --- a/libs/sdk-js/package.json +++ b/libs/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-sdk", - "version": "0.0.62", + "version": "0.0.63", "description": "Client library for interacting with the LangGraph API", "type": "module", "packageManager": "yarn@1.22.19", diff --git a/libs/sdk-js/src/client.ts b/libs/sdk-js/src/client.ts index 54acd5232..f368da079 100644 --- a/libs/sdk-js/src/client.ts +++ b/libs/sdk-js/src/client.ts @@ -340,6 +340,7 @@ export class AssistantsClient extends BaseClient { assistantId?: string; ifExists?: OnConflictBehavior; name?: string; + description?: string; }): Promise