diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 050daa704..db20a88de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: - branches: [main] + branches: [main, v1] pull_request: permissions: diff --git a/docs/docs/agents/context.md b/docs/docs/agents/context.md index 542379a44..a84c28a4e 100644 --- a/docs/docs/agents/context.md +++ b/docs/docs/agents/context.md @@ -12,56 +12,64 @@ LangGraph provides **three** primary ways to supply context: | Type | Description | Mutable? | Lifetime | |------------------------------------------------------------------------------|-----------------------------------------------|----------|-------------------------| -| [**Config**](#config-static-context) | data passed at the start of a run | ❌ | per run | +| [**Runtime Context**](#runtime-context) | data passed at the start of a run | ❌ | per run | | [**Short-term memory (State)**](#short-term-memory-mutable-context) | dynamic data that can change during execution | ✅ | per run or conversation | | [**Long-term memory (Store)**](#long-term-memory-cross-conversation-context) | data that can be shared between conversations | ✅ | across conversations | -## Provide runtime context +### Runtime Context -### Config (static context) +!!! note "`config['configurable']` -> `runtime.context`" -Config is for immutable data like user metadata or API keys. Use -when you have values that don't change mid-run. + In LangGraph < v1.0, static runtime context was passed via the `config['configurable']` key, paired with a `config_schema` argument + to `StateGraph` or `Pregel`. This is now deprecated and will be removed in v2.0. -Specify configuration using a key called **"configurable"** which is reserved -for this purpose: + As of LangGraph v1.0, the Runtime object is recommended to access static context and runtime-specific information like the store and stream writer. + +Runtime context is for immutable data like user metadata or API keys. Use this when you have values that don't change mid-run. + +Specify static context via the `context` argument to `invoke` / `stream`, which is reserved for this purpose: ```python +@dataclass +class ContextSchema: + user_name: str + graph.invoke( # (1)! {"messages": [{"role": "user", "content": "hi!"}]}, # (2)! # highlight-next-line - config={"configurable": {"user_id": "user_123"}} # (3)! + context={"user_name": "John Smith"} # (3)! ) ``` 1. This is the invocation of the agent or graph. The `invoke` method runs the underlying graph with the provided input. 2. This example uses messages as an input, which is common, but your application may use different input structures. -3. This is where you pass the configuration data. The `config` parameter allows you to provide additional context that the agent can use during its execution. +3. This is where you pass the runtime data. The `context` parameter allows you to provide additional dependencies that the agent can use during its execution. === "Agent prompt" ```python from langchain_core.messages import AnyMessage - from langchain_core.runnables import RunnableConfig + from langgraph.runtime import get_runtime from langgraph.prebuilt.chat_agent_executor import AgentState from langgraph.prebuilt import create_react_agent # highlight-next-line - def prompt(state: AgentState, config: RunnableConfig) -> list[AnyMessage]: - user_name = config["configurable"].get("user_name") - system_msg = f"You are a helpful assistant. Address the user as {user_name}." + def prompt(state: AgentState) -> list[AnyMessage]: + runtime = get_runtime(ContextSchema) + system_msg = f"You are a helpful assistant. Address the user as {runtime.context.user_name}." return [{"role": "system", "content": system_msg}] + state["messages"] agent = create_react_agent( model="anthropic:claude-3-7-sonnet-latest", tools=[get_weather], - prompt=prompt + prompt=prompt, + context_schema=ContextSchema ) agent.invoke( {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, # highlight-next-line - config={"configurable": {"user_name": "John Smith"}} + context={"user_name": "John Smith"} ) ``` @@ -70,11 +78,11 @@ graph.invoke( # (1)! === "Workflow node" ```python - from langchain_core.runnables import RunnableConfig + from langgraph.runtime import Runtime # highlight-next-line - def node(state: State, config: RunnableConfig): - user_name = config["configurable"].get("user_name") + def node(state: State, config: Runtime[ContextSchema]): + user_name = runtime.context.user_name ... ``` @@ -83,14 +91,16 @@ graph.invoke( # (1)! === "In a tool" ```python - from langchain_core.runnables import RunnableConfig + from langgraph.runtime import get_runtime @tool # highlight-next-line - def get_user_info(config: RunnableConfig) -> str: + def get_user_email() -> str: """Retrieve user information based on user ID.""" - user_id = config["configurable"].get("user_id") - return "User is John Smith" if user_id == "user_123" else "Unknown user" + # simulate fetching user info from a database + runtime = get_runtime(ContextSchema) + email = get_user_email_from_db(runtime.context.user_name) + return email ``` See the [tool calling guide](../how-tos/tool-calling.md#configuration) for details. diff --git a/docs/docs/cloud/deployment/setup.md b/docs/docs/cloud/deployment/setup.md index 4fc52c7e5..91aecf989 100644 --- a/docs/docs/cloud/deployment/setup.md +++ b/docs/docs/cloud/deployment/setup.md @@ -108,11 +108,11 @@ 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 -# Define the config -class GraphConfig(TypedDict): +# Define the runtime context +class GraphContext(TypedDict): model_name: Literal["anthropic", "openai"] -workflow = StateGraph(AgentState, config_schema=GraphConfig) +workflow = StateGraph(AgentState, context_schema=GraphContext) workflow.add_node("agent", call_model) workflow.add_node("action", tool_node) workflow.add_edge(START, "agent") diff --git a/docs/docs/cloud/deployment/setup_pyproject.md b/docs/docs/cloud/deployment/setup_pyproject.md index 033ab7763..0b06149f6 100644 --- a/docs/docs/cloud/deployment/setup_pyproject.md +++ b/docs/docs/cloud/deployment/setup_pyproject.md @@ -121,11 +121,11 @@ 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 -# Define the config -class GraphConfig(TypedDict): +# Define the runtime context +class GraphContext(TypedDict): model_name: Literal["anthropic", "openai"] -workflow = StateGraph(AgentState, config_schema=GraphConfig) +workflow = StateGraph(AgentState, context_schema=GraphContext) workflow.add_node("agent", call_model) workflow.add_node("action", tool_node) workflow.add_edge(START, "agent") diff --git a/docs/docs/cloud/how-tos/add-human-in-the-loop.md b/docs/docs/cloud/how-tos/add-human-in-the-loop.md index 1df6c0c4e..c16e4db1e 100644 --- a/docs/docs/cloud/how-tos/add-human-in-the-loop.md +++ b/docs/docs/cloud/how-tos/add-human-in-the-loop.md @@ -30,9 +30,7 @@ To review, edit, and approve tool calls in an agent or workflow, use LangGraph's # > [ # > { # > 'value': {'text_to_revise': 'original text'}, - # > 'resumable': True, - # > 'ns': ['human_node:fc722478-2f21-0578-c572-d9fc4dd07c3b'], - # > 'when': 'during' + # > 'id': '...', # > } # > ] @@ -203,9 +201,7 @@ To review, edit, and approve tool calls in an agent or workflow, use LangGraph's # > [ # > { # > 'value': {'text_to_revise': 'original text'}, - # > 'resumable': True, - # > 'ns': ['human_node:fc722478-2f21-0578-c572-d9fc4dd07c3b'], - # > 'when': 'during' + # > 'id': '...', # > } # > ] diff --git a/docs/docs/cloud/how-tos/configuration_cloud.md b/docs/docs/cloud/how-tos/configuration_cloud.md index adc2fd841..1d1d62b61 100644 --- a/docs/docs/cloud/how-tos/configuration_cloud.md +++ b/docs/docs/cloud/how-tos/configuration_cloud.md @@ -2,21 +2,20 @@ In this guide we will show how to create, configure, and manage an [assistant](../../concepts/assistants.md). -First, as a brief refresher on the concept of configurations, consider the following simple `call_model` node and configuration schema. Observe that this node tries to read and use the `model_name` as defined by the `config` object's `configurable`. +First, as a brief refresher on the concept of runtime context, consider the following simple `call_model` node and context schema. Observe that this node tries to read and use the `model_provider` as defined by the `Runtime` object's `context` property. === "Python" ```python + @dataclass + class ContextSchema: + llm_provider: str = "anthropic" - class ConfigSchema(TypedDict): - model_name: str + builder = StateGraph(AgentState, context_schema=ContextSchema) - builder = StateGraph(AgentState, config_schema=ConfigSchema) - - def call_model(state, config): + def call_model(state, runtime: Runtime[ContextSchema]): messages = state["messages"] - model_name = config.get('configurable', {}).get("model_name", "anthropic") - model = _get_model(model_name) + model = _get_model(runtime.context.llm_provider) response = model.invoke(messages) # We return a list, because this will get added to the existing list return {"messages": [response]} @@ -44,7 +43,7 @@ First, as a brief refresher on the concept of configurations, consider the follo } ``` -For more information on configurations, [see here](../../concepts/low_level.md#configuration). +For more information on runtime context, [see here](../../concepts/low_level.md#runtime-context). ## Create an assistant diff --git a/docs/docs/concepts/assistants.md b/docs/docs/concepts/assistants.md index feb79641b..c43af6616 100644 --- a/docs/docs/concepts/assistants.md +++ b/docs/docs/concepts/assistants.md @@ -1,6 +1,6 @@ # Assistants -**Assistants** allow you to manage configurations (like prompts, LLM selection, tools) separately from your graph's core logic, enabling rapid changes that don't alter the graph architecture. It is a way to create multiple specialized versions of the same graph architecture, each optimized for different use cases through configuration variations rather than structural changes. +**Assistants** allow you to manage configurations (like prompts, LLM selection, tools) separately from your graph's core logic, enabling rapid changes that don't alter the graph architecture. It is a way to create multiple specialized versions of the same graph architecture, each optimized for different use cases through context/configuration variations rather than structural changes. For example, imagine a general-purpose writing agent built on a common graph architecture. While the structure remains the same, different writing styles—such as blog posts and tweets—require tailored configurations to optimize performance. To support these variations, you can create multiple assistants (e.g., one for blogs and another for tweets) that share the underlying graph but differ in model selection and system prompt. @@ -14,8 +14,8 @@ The LangGraph Cloud API provides several endpoints for creating and managing ass ## Configuration -Assistants build on the LangGraph open source concept of [configuration](low_level.md#configuration). -While configuration is available in the open source LangGraph library, assistants are only present in [LangGraph Platform](langgraph_platform.md). This is due to the fact that assistants are tightly coupled to your deployed graph. Upon deployment, LangGraph Server will automatically create a default assistant for each graph using the graph's default configuration settings. +Assistants build on the LangGraph open source concepts of configuration and [runtime context](low_level.md#runtime-context). +While these features are available in the open source LangGraph library, assistants are only present in [LangGraph Platform](langgraph_platform.md). This is due to the fact that assistants are tightly coupled to your deployed graph. Upon deployment, LangGraph Server will automatically create a default assistant for each graph using the graph's default context and configuration settings. In practice, an assistant is just an _instance_ of a graph with a specific configuration. Therefore, multiple assistants can reference the same graph but can contain different configurations (e.g. prompts, models, tools). The LangGraph Server API provides several endpoints for creating and managing assistants. See the [API reference](../cloud/reference/api/api_ref.html) and [this how-to](../cloud/how-tos/configuration_cloud.md) for more details on how to create assistants. @@ -26,6 +26,6 @@ Once you've created an assistant, subsequent edits to that assistant will create ## Execution -A **run** is an invocation of an assistant. Each run may have its own input, configuration, and metadata, which may affect execution and output of the underlying graph. A run can optionally be executed on a [thread](./persistence.md#threads). +A **run** is an invocation of an assistant. Each run may have its own input, configuration, context, and metadata, which may affect execution and output of the underlying graph. A run can optionally be executed on a [thread](./persistence.md#threads). The LangGraph Platform API provides several endpoints for creating and managing runs. See the [API reference](../cloud/reference/api/api_ref.html#tag/thread-runs/) for more details. diff --git a/docs/docs/concepts/functional_api.md b/docs/docs/concepts/functional_api.md index b2a3d97c7..b43cba9e5 100644 --- a/docs/docs/concepts/functional_api.md +++ b/docs/docs/concepts/functional_api.md @@ -79,51 +79,54 @@ def workflow(topic: str) -> dict: ```python import time import uuid - from langgraph.func import entrypoint, task from langgraph.types import interrupt from langgraph.checkpoint.memory import InMemorySaver + @task def write_essay(topic: str) -> str: """Write an essay about the given topic.""" - time.sleep(1) # This is a placeholder for a long-running task. + time.sleep(1) # This is a placeholder for a long-running task. return f"An essay about topic: {topic}" @entrypoint(checkpointer=InMemorySaver()) def workflow(topic: str) -> dict: """A simple workflow that writes an essay and asks for a review.""" essay = write_essay("cat").result() - is_approved = interrupt({ - # Any json-serializable payload provided to interrupt as argument. - # It will be surfaced on the client side as an Interrupt when streaming data - # from the workflow. - "essay": essay, # The essay we want reviewed. - # We can add any additional information that we need. - # For example, introduce a key called "action" with some instructions. - "action": "Please approve/reject the essay", - }) - + is_approved = interrupt( + { + # Any json-serializable payload provided to interrupt as argument. + # It will be surfaced on the client side as an Interrupt when streaming data + # from the workflow. + "essay": essay, # The essay we want reviewed. + # We can add any additional information that we need. + # For example, introduce a key called "action" with some instructions. + "action": "Please approve/reject the essay", + } + ) return { - "essay": essay, # The essay that was generated - "is_approved": is_approved, # Response from HIL + "essay": essay, # The essay that was generated + "is_approved": is_approved, # Response from HIL } + thread_id = str(uuid.uuid4()) - - config = { - "configurable": { - "thread_id": thread_id - } - } - + config = {"configurable": {"thread_id": thread_id}} for item in workflow.stream("cat", config): print(item) - ``` - - ```pycon - {'write_essay': 'An essay about topic: cat'} - {'__interrupt__': (Interrupt(value={'essay': 'An essay about topic: cat', 'action': 'Please approve/reject the essay'}, resumable=True, ns=['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], when='during'),)} + # > {'write_essay': 'An essay about topic: cat'} + # > { + # > '__interrupt__': ( + # > Interrupt( + # > value={ + # > 'essay': 'An essay about topic: cat', + # > 'action': 'Please approve/reject the essay' + # > }, + # > id='b9b2b9d788f482663ced6dc755c9e981' + # > ), + # > ) + # > } ``` An essay has been written and is ready for review. Once the review is provided, we can resume the workflow: diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index d47cc2116..15d5db4e7 100644 --- a/docs/docs/concepts/low_level.md +++ b/docs/docs/concepts/low_level.md @@ -192,35 +192,48 @@ class State(MessagesState): ## Nodes -In LangGraph, nodes are typically python functions (sync or async) where the **first** positional argument is the [state](#state), and (optionally), the **second** positional argument is a "config", containing optional [configurable parameters](#configuration) (such as a `thread_id`). +In LangGraph, nodes are Python functions (either synchronous or asynchronous) that accept the following arguments: + +1. `state`: The [state](#state) of the graph +2. `config`: A `RunnableConfig` object that contains configuration information like `thread_id` and tracing information like `tags` +3. `runtime`: A `Runtime` object that contains [runtime `context`](#runtime-context) and other information like `store` and `stream_writer` + Similar to `NetworkX`, you add these nodes to a graph using the [add_node][langgraph.graph.StateGraph.add_node] method: ```python +from dataclasses import dataclass from typing_extensions import TypedDict from langchain_core.runnables import RunnableConfig from langgraph.graph import StateGraph +from langgraph.runtime import Runtime class State(TypedDict): input: str results: str +@dataclass +class Context: + user_id: str + builder = StateGraph(State) +def plain_node(state: State): + return state -def my_node(state: State, config: RunnableConfig): - print("In node: ", config["configurable"]["user_id"]) +def node_with_runtime(state: State, runtime: Runtime[Context]): + print("In node: ", runtime.context.user_id) + return {"results": f"Hello, {state['input']}!"} + +def node_with_config(state: State, config: RunnableConfig): + print("In node with thread_id: ", config["configurable"]["thread_id"]) return {"results": f"Hello, {state['input']}!"} -# The second argument is optional -def my_other_node(state: State): - return state - - -builder.add_node("my_node", my_node) -builder.add_node("other_node", my_other_node) +builder.add_node("plain_node", plain_node) +builder.add_node("node_with_runtime", node_with_runtime) +builder.add_node("node_with_config", node_with_config) ... ``` @@ -459,33 +472,32 @@ LangGraph can easily handle migrations of graph definitions (nodes, edges, and s - State keys that are renamed lose their saved state in existing threads - State keys whose types change in incompatible ways could currently cause issues in threads with state from before the change -- if this is a blocker please reach out and we can prioritize a solution. -## Configuration +## Runtime Context -When creating a graph, you can also mark that certain parts of the graph are configurable. This is commonly done to enable easily switching between models or system prompts. This allows you to create a single "cognitive architecture" (the graph) but have multiple different instance of it. - -You can optionally specify a `config_schema` when creating a graph. +When creating a graph, you can specify a `context_schema` for runtime context passed to nodes. This is useful for passing +information to nodes that is not part of the graph state. For example, you might want to pass dependencies such as model name or a database connection. ```python -class ConfigSchema(TypedDict): - llm: str +@dataclass +class ContextSchema: + llm_provider: str = "openai" -graph = StateGraph(State, config_schema=ConfigSchema) +graph = StateGraph(State, context_schema=ContextSchema) ``` -You can then pass this configuration into the graph using the `configurable` config field. +You can then pass this context into the graph using the `context` parameter of the `invoke` method. ```python -config = {"configurable": {"llm": "anthropic"}} - -graph.invoke(inputs, config=config) +graph.invoke(inputs, context={"llm_provider": "anthropic"}) ``` -You can then access and use this configuration inside a node or conditional edge: +You can then access and use this context inside a node or conditional edge: ```python -def node_a(state, config): - llm_type = config.get("configurable", {}).get("llm", "openai") - llm = get_llm(llm_type) +from langgraph.runtime import Runtime + +def node_a(state: State, runtime: Runtime[ContextSchema]): + llm = get_llm(runtime.context.llm_provider) ... ``` @@ -496,7 +508,7 @@ See [this guide](../how-tos/graph-api.md#add-runtime-configuration) for a full b 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"}}) +graph.invoke(inputs, config={"recursion_limit": 5}, context={"llm": "anthropic"}) ``` Read [this how-to](https://langchain-ai.github.io/langgraph/how-tos/recursion-limit/) to learn more about how the recursion limit works. diff --git a/docs/docs/how-tos/graph-api.md b/docs/docs/how-tos/graph-api.md index 123b40399..f36ae584e 100644 --- a/docs/docs/how-tos/graph-api.md +++ b/docs/docs/how-tos/graph-api.md @@ -514,12 +514,12 @@ To add runtime configuration: See below for a simple example: ```python -from langchain_core.runnables import RunnableConfig from langgraph.graph import END, StateGraph, START +from langgraph.runtime import Runtime from typing_extensions import TypedDict # 1. Specify config schema -class ConfigSchema(TypedDict): +class ContextSchema(TypedDict): my_runtime_value: str # 2. Define a graph that accesses the config in a node @@ -527,18 +527,18 @@ class State(TypedDict): my_state_value: str # highlight-next-line -def node(state: State, config: RunnableConfig): +def node(state: State, runtime: Runtime[ContextSchema]): # highlight-next-line - if config["configurable"]["my_runtime_value"] == "a": + if runtime.context["my_runtime_value"] == "a": return {"my_state_value": 1} # highlight-next-line - elif config["configurable"]["my_runtime_value"] == "b": + elif runtime.context["my_runtime_value"] == "b": return {"my_state_value": 2} else: raise ValueError("Unknown values.") # highlight-next-line -builder = StateGraph(State, config_schema=ConfigSchema) +builder = StateGraph(State, context_schema=ContextSchema) builder.add_node(node) builder.add_edge(START, "node") builder.add_edge("node", END) @@ -547,9 +547,9 @@ graph = builder.compile() # 3. Pass in configuration at runtime: # highlight-next-line -print(graph.invoke({}, {"configurable": {"my_runtime_value": "a"}})) +print(graph.invoke({}, context={"my_runtime_value": "a"})) # highlight-next-line -print(graph.invoke({}, {"configurable": {"my_runtime_value": "b"}})) +print(graph.invoke({}, context={"my_runtime_value": "b"})) ``` ``` {'my_state_value': 1} @@ -560,27 +560,28 @@ print(graph.invoke({}, {"configurable": {"my_runtime_value": "b"}})) Below we demonstrate a practical example in which we configure what LLM to use at runtime. We will use both OpenAI and Anthropic models. ```python + from dataclasses import dataclass + from langchain.chat_models import init_chat_model - from langchain_core.runnables import RunnableConfig - from langgraph.graph import MessagesState - from langgraph.graph import END, StateGraph, START + from langgraph.graph import MessagesState, END, StateGraph, START + from langgraph.runtime import Runtime from typing_extensions import TypedDict - class ConfigSchema(TypedDict): - model: str + @dataclass + class ContextSchema: + model_provider: str = "anthropic" MODELS = { "anthropic": init_chat_model("anthropic:claude-3-5-haiku-latest"), "openai": init_chat_model("openai:gpt-4.1-mini"), } - def call_model(state: MessagesState, config: RunnableConfig): - model = config["configurable"].get("model", "anthropic") - model = MODELS[model] + def call_model(state: MessagesState, runtime: Runtime[ContextSchema]): + model = MODELS[runtime.context.model_provider] response = model.invoke(state["messages"]) return {"messages": [response]} - builder = StateGraph(MessagesState, config_schema=ConfigSchema) + builder = StateGraph(MessagesState, context_schema=ContextSchema) builder.add_node("model", call_model) builder.add_edge(START, "model") builder.add_edge("model", END) @@ -592,8 +593,7 @@ print(graph.invoke({}, {"configurable": {"my_runtime_value": "b"}})) # With no configuration, uses default (Anthropic) response_1 = graph.invoke({"messages": [input_message]})["messages"][-1] # Or, can set OpenAI - config = {"configurable": {"model": "openai"}} - response_2 = graph.invoke({"messages": [input_message]}, config=config)["messages"][-1] + response_2 = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai"})["messages"][-1] print(response_1.response_metadata["model_name"]) print(response_2.response_metadata["model_name"]) @@ -607,32 +607,33 @@ print(graph.invoke({}, {"configurable": {"my_runtime_value": "b"}})) Below we demonstrate a practical example in which we configure two parameters: the LLM and system message to use at runtime. ```python + from dataclasses import dataclass from typing import Optional from langchain.chat_models import init_chat_model from langchain_core.messages import SystemMessage - from langchain_core.runnables import RunnableConfig from langgraph.graph import END, MessagesState, StateGraph, START + from langgraph.runtime import Runtime from typing_extensions import TypedDict - class ConfigSchema(TypedDict): - model: Optional[str] - system_message: Optional[str] + @dataclass + class ContextSchema: + model_provider: str = "anthropic" + system_message: str | None = None MODELS = { "anthropic": init_chat_model("anthropic:claude-3-5-haiku-latest"), "openai": init_chat_model("openai:gpt-4.1-mini"), } - def call_model(state: MessagesState, config: RunnableConfig): - model = config["configurable"].get("model", "anthropic") - model = MODELS[model] + def call_model(state: MessagesState, runtime: Runtime[ContextSchema]): + model = MODELS[runtime.context.model_provider] messages = state["messages"] - if system_message := config["configurable"].get("system_message"): + if (system_message := runtime.context.system_message): messages = [SystemMessage(system_message)] + messages response = model.invoke(messages) return {"messages": [response]} - builder = StateGraph(MessagesState, config_schema=ConfigSchema) + builder = StateGraph(MessagesState, context_schema=ContextSchema) builder.add_node("model", call_model) builder.add_edge(START, "model") builder.add_edge("model", END) @@ -641,8 +642,7 @@ print(graph.invoke({}, {"configurable": {"my_runtime_value": "b"}})) # Usage input_message = {"role": "user", "content": "hi"} - config = {"configurable": {"model": "openai", "system_message": "Respond in Italian."}} - response = graph.invoke({"messages": [input_message]}, config) + response = graph.invoke({"messages": [input_message]}, context={"model_provider": "openai", "system_message": "Respond in Italian."}) for message in response["messages"]: message.pretty_print() ``` diff --git a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md index 55b39c514..e9d18ea4a 100644 --- a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md +++ b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md @@ -54,13 +54,7 @@ graph = graph_builder.compile(checkpointer=checkpointer) # (4)! config = {"configurable": {"thread_id": "some_id"}} result = graph.invoke({"some_text": "original text"}, config=config) # (5)! print(result['__interrupt__']) # (6)! -# > [ -# > Interrupt( -# > value={'text_to_revise': 'original text'}, -# > resumable=True, -# > ns=['human_node:6ce9e64f-edef-fe5d-f7dc-511fa9526960'] -# > ) -# > ] +# > [Interrupt(value={'text_to_revise': 'original text'}, id='a0d9dd40440ac7be2720dc5c20858627')] # highlight-next-line print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)! @@ -80,25 +74,27 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)! ```python from typing import TypedDict import uuid - from langgraph.checkpoint.memory import InMemorySaver from langgraph.constants import START from langgraph.graph import StateGraph + # highlight-next-line from langgraph.types import interrupt, Command + class State(TypedDict): some_text: str + def human_node(state: State): # highlight-next-line - value = interrupt( # (1)! + value = interrupt( # (1)! { - "text_to_revise": state["some_text"] # (2)! + "text_to_revise": state["some_text"] # (2)! } ) return { - "some_text": value # (3)! + "some_text": value # (3)! } @@ -106,25 +102,15 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)! graph_builder = StateGraph(State) graph_builder.add_node("human_node", human_node) graph_builder.add_edge(START, "human_node") - - checkpointer = InMemorySaver() # (4)! - + checkpointer = InMemorySaver() # (4)! graph = graph_builder.compile(checkpointer=checkpointer) - # Pass a thread ID to the graph to run it. config = {"configurable": {"thread_id": uuid.uuid4()}} - # Run the graph until the interrupt is hit. - result = graph.invoke({"some_text": "original text"}, config=config) # (5)! + result = graph.invoke({"some_text": "original text"}, config=config) # (5)! - print(result['__interrupt__']) # (6)! - # > [ - # > Interrupt( - # > value={'text_to_revise': 'original text'}, - # > resumable=True, - # > ns=['human_node:6ce9e64f-edef-fe5d-f7dc-511fa9526960'] - # > ) - # > ] + print(result["__interrupt__"]) # (6)! + # > [Interrupt(value={'text_to_revise': 'original text'}, id='6d7c4048049254c83195429a3659661d')] # highlight-next-line print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)! @@ -167,7 +153,7 @@ For example, once your graph has been interrupted (multiple times, theoretically ```python resume_map = { - i.interrupt_id: f"human input for prompt {i.value}" + i.id: f"human input for prompt {i.value}" for i in parent.get_state(thread_config).interrupts } @@ -388,14 +374,15 @@ graph.invoke( # Output interrupt payload print(result["__interrupt__"]) # Example output: - # Interrupt( - # value={ - # 'task': 'Please review and edit the generated summary if necessary.', - # 'generated_summary': 'The cat sat on the mat and looked at the stars.' - # }, - # resumable=True, - # ... - # ) + # > [ + # > Interrupt( + # > value={ + # > 'task': 'Please review and edit the generated summary if necessary.', + # > 'generated_summary': 'The cat sat on the mat and looked at the stars.' + # > }, + # > id='...' + # > ) + # > ] # Resume the graph with human-edited input edited_summary = "The cat lay on the rug, gazing peacefully at the night sky." @@ -1032,7 +1019,7 @@ def node_in_parent_graph(state: State): Entered `parent_node` a total of 1 times Entered `node_in_subgraph` a total of 1 times Entered human_node in sub-graph a total of 1 times - {'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['parent_node:4c3a0248-21f0-1287-eacf-3002bc304db4', 'human_node:2fe86d52-6f70-2a3f-6b2f-b1eededd6348'], when='during'),)} + {'__interrupt__': (Interrupt(value='what is your name?', id='...'),)} --- Resuming --- Entered `parent_node` a total of 2 times Entered human_node in sub-graph a total of 2 times @@ -1108,7 +1095,7 @@ To avoid issues, refrain from dynamically changing the node's structure between ``` ```pycon - {'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['human_node:3a007ef9-c30d-c357-1ec1-86a1a70d8fba'], when='during'),)} + {'__interrupt__': (Interrupt(value='what is your name?', id='...'),)} Name: N/A. Age: John {'human_node': {'age': 'John', 'name': 'N/A'}} ``` diff --git a/docs/docs/reference/constants.md b/docs/docs/reference/constants.md index f23e941fa..fe26ce727 100644 --- a/docs/docs/reference/constants.md +++ b/docs/docs/reference/constants.md @@ -2,5 +2,6 @@ options: members: - TAG_HIDDEN + - TAG_NOSTREAM - START - - END \ No newline at end of file + - END diff --git a/docs/docs/tutorials/tot/tot.ipynb b/docs/docs/tutorials/tot/tot.ipynb index 40f5216c5..f8b8fbd0f 100644 --- a/docs/docs/tutorials/tot/tot.ipynb +++ b/docs/docs/tutorials/tot/tot.ipynb @@ -272,7 +272,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -280,10 +280,10 @@ "from typing import Optional, Dict, Any\n", "from typing_extensions import Annotated, TypedDict\n", "from langgraph.graph import StateGraph\n", + "from langgraph.runtime import Runtime\n", "\n", - "from langchain_core.runnables import RunnableConfig\n", - "from langgraph.constants import Send\n", "from langgraph.checkpoint.memory import InMemorySaver\n", + "from langgraph.types import Send\n", "\n", "\n", "def update_candidates(\n", @@ -307,22 +307,27 @@ " depth: Annotated[int, operator.add]\n", "\n", "\n", - "class Configuration(TypedDict, total=False):\n", + "class Context(TypedDict, total=False):\n", " max_depth: int\n", " threshold: float\n", " k: int\n", " beam_size: int\n", "\n", "\n", - "def _ensure_configurable(config: RunnableConfig) -> Configuration:\n", + "class EnsuredContext(TypedDict):\n", + " max_depth: int\n", + " threshold: float\n", + " k: int\n", + " beam_size: int\n", + "\n", + "\n", + "def _ensure_context(ctx: Context) -> EnsuredContext:\n", " \"\"\"Get params that configure the search algorithm.\"\"\"\n", - " configurable = config.get(\"configurable\", {})\n", " return {\n", - " **configurable,\n", - " \"max_depth\": configurable.get(\"max_depth\", 10),\n", - " \"threshold\": config.get(\"threshold\", 0.9),\n", - " \"k\": configurable.get(\"k\", 5),\n", - " \"beam_size\": configurable.get(\"beam_size\", 3),\n", + " \"max_depth\": ctx.get(\"max_depth\", 10),\n", + " \"threshold\": ctx.get(\"threshold\", 0.9),\n", + " \"k\": ctx.get(\"k\", 5),\n", + " \"beam_size\": ctx.get(\"beam_size\", 3),\n", " }\n", "\n", "\n", @@ -330,9 +335,11 @@ " seed: Optional[Candidate]\n", "\n", "\n", - "def expand(state: ExpansionState, *, config: RunnableConfig) -> Dict[str, List[str]]:\n", + "def expand(\n", + " state: ExpansionState, *, runtime: Runtime[Context]\n", + ") -> Dict[str, List[Candidate]]:\n", " \"\"\"Generate the next state.\"\"\"\n", - " configurable = _ensure_configurable(config)\n", + " ctx = _ensure_context(runtime.context)\n", " if not state.get(\"seed\"):\n", " candidate_str = \"\"\n", " else:\n", @@ -342,9 +349,8 @@ " {\n", " \"problem\": state[\"problem\"],\n", " \"candidate\": candidate_str,\n", - " \"k\": configurable[\"k\"],\n", + " \"k\": ctx[\"k\"],\n", " },\n", - " config=config,\n", " )\n", " except Exception:\n", " return {\"candidates\": []}\n", @@ -354,7 +360,7 @@ " return {\"candidates\": new_candidates}\n", "\n", "\n", - "def score(state: ToTState) -> Dict[str, List[float]]:\n", + "def score(state: ToTState) -> Dict[str, Any]:\n", " \"\"\"Evaluate the candidate generations.\"\"\"\n", " candidates = state[\"candidates\"]\n", " scored = []\n", @@ -363,11 +369,9 @@ " return {\"scored_candidates\": scored, \"candidates\": \"clear\"}\n", "\n", "\n", - "def prune(\n", - " state: ToTState, *, config: RunnableConfig\n", - ") -> Dict[str, List[Dict[str, Any]]]:\n", + "def prune(state: ToTState, *, runtime: Runtime[Context]) -> Dict[str, Any]:\n", " scored_candidates = state[\"scored_candidates\"]\n", - " beam_size = _ensure_configurable(config)[\"beam_size\"]\n", + " beam_size = _ensure_context(runtime.context)[\"beam_size\"]\n", " organized = sorted(\n", " scored_candidates, key=lambda candidate: candidate[1], reverse=True\n", " )\n", @@ -383,11 +387,11 @@ "\n", "\n", "def should_terminate(\n", - " state: ToTState, config: RunnableConfig\n", + " state: ToTState, runtime: Runtime[Context]\n", ") -> Union[Literal[\"__end__\"], Send]:\n", - " configurable = _ensure_configurable(config)\n", - " solved = state[\"candidates\"][0].score >= configurable[\"threshold\"]\n", - " if solved or state[\"depth\"] >= configurable[\"max_depth\"]:\n", + " ctx = _ensure_context(runtime.context)\n", + " solved = state[\"candidates\"][0].score >= ctx[\"threshold\"]\n", + " if solved or state[\"depth\"] >= ctx[\"max_depth\"]:\n", " return \"__end__\"\n", " return [\n", " Send(\"expand\", {**state, \"somevalseed\": candidate})\n", @@ -396,7 +400,7 @@ "\n", "\n", "# Create the graph\n", - "builder = StateGraph(state_schema=ToTState, config_schema=Configuration)\n", + "builder = StateGraph(state_schema=ToTState, context_schema=Context)\n", "\n", "# Add nodes\n", "builder.add_node(expand)\n", @@ -467,13 +471,11 @@ } ], "source": [ - "config = {\n", - " \"configurable\": {\n", - " \"thread_id\": \"test_1\",\n", - " \"depth\": 10,\n", - " }\n", - "}\n", - "for step in graph.stream({\"problem\": puzzles[42]}, config):\n", + "for step in graph.stream(\n", + " {\"problem\": puzzles[42]},\n", + " config={\"configurable\": {\"thread_id\": \"test_1\"}},\n", + " context={\"depth\": 10},\n", + "):\n", " print(step)" ] }, @@ -491,7 +493,7 @@ } ], "source": [ - "final_state = graph.get_state(config)\n", + "final_state = graph.get_state({\"configurable\": {\"thread_id\": \"test_1\"}})\n", "winning_solution = final_state.values[\"candidates\"][0]\n", "search_depth = final_state.values[\"depth\"]\n", "if winning_solution[1] == 1:\n", diff --git a/docs/uv.lock b/docs/uv.lock index 9f90723ac..11eb9f695 100644 --- a/docs/uv.lock +++ b/docs/uv.lock @@ -15,16 +15,16 @@ name = "ag2" version = "0.9.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "asyncer" }, - { name = "diskcache" }, - { name = "docker" }, - { name = "httpx" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "termcolor" }, - { name = "tiktoken" }, + { name = "anyio", marker = "python_full_version < '3.13'" }, + { name = "asyncer", marker = "python_full_version < '3.13'" }, + { name = "diskcache", marker = "python_full_version < '3.13'" }, + { name = "docker", marker = "python_full_version < '3.13'" }, + { name = "httpx", marker = "python_full_version < '3.13'" }, + { name = "packaging", marker = "python_full_version < '3.13'" }, + { name = "pydantic", marker = "python_full_version < '3.13'" }, + { name = "python-dotenv", marker = "python_full_version < '3.13'" }, + { name = "termcolor", marker = "python_full_version < '3.13'" }, + { name = "tiktoken", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ee/15/edfbbf217e19ea647225b3ab72a6e3755d2677665f1a7f8e5108da3feabd/ag2-0.9.6.tar.gz", hash = "sha256:d6f7812b1a49654d14113fa3c13ccb593115dee1193744ca428d7178d2b32090", size = 3356270, upload-time = "2025-07-08T14:56:21.63Z" } wheels = [ @@ -267,7 +267,7 @@ name = "asyncer" version = "0.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, + { name = "anyio", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ff/67/7ea59c3e69eaeee42e7fc91a5be67ca5849c8979acac2b920249760c6af2/asyncer-0.0.8.tar.gz", hash = "sha256:a589d980f57e20efb07ed91d0dbe67f1d2fd343e7142c66d3a099f05c620739c", size = 18217, upload-time = "2024-08-24T23:15:36.449Z" } wheels = [ @@ -288,7 +288,7 @@ name = "autogen" version = "0.9.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ag2" }, + { name = "ag2", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/b9/dc958031b7e08ee50e3d40f5991f4c0bc21538df8d53aa3e9a9f2e2f7818/autogen-0.9.6.tar.gz", hash = "sha256:dc2efbeef61002608983afb120e62f8a109815eb741bcbc9ef398dcff7424a30", size = 43422, upload-time = "2025-07-08T14:56:17.6Z" } wheels = [ @@ -914,9 +914,9 @@ name = "docker" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "requests" }, - { name = "urllib3" }, + { name = "pywin32", marker = "python_full_version < '3.13' and sys_platform == 'win32'" }, + { name = "requests", marker = "python_full_version < '3.13'" }, + { name = "urllib3", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" } wheels = [ @@ -2337,7 +2337,7 @@ wheels = [ [[package]] name = "langgraph" -version = "0.5.2" +version = "0.6.0a1" source = { editable = "../libs/langgraph" } dependencies = [ { name = "langchain-core" }, @@ -2365,7 +2365,7 @@ dev = [ { name = "langgraph-checkpoint", editable = "../libs/checkpoint" }, { name = "langgraph-checkpoint-postgres", editable = "../libs/checkpoint-postgres" }, { name = "langgraph-checkpoint-sqlite", editable = "../libs/checkpoint-sqlite" }, - { name = "langgraph-cli", extras = ["inmem"] }, + { name = "langgraph-cli", extras = ["inmem"], editable = "../libs/cli" }, { name = "langgraph-prebuilt", editable = "../libs/prebuilt" }, { name = "langgraph-sdk", editable = "../libs/sdk-py" }, { name = "mypy" }, @@ -2388,7 +2388,7 @@ dev = [ [[package]] name = "langgraph-checkpoint" -version = "2.1.0" +version = "2.1.1" source = { editable = "../libs/checkpoint" } dependencies = [ { name = "langchain-core" }, @@ -2433,7 +2433,7 @@ wheels = [ [[package]] name = "langgraph-checkpoint-postgres" -version = "2.0.21" +version = "2.0.23" source = { editable = "../libs/checkpoint-postgres" } dependencies = [ { name = "langgraph-checkpoint" }, @@ -2674,7 +2674,7 @@ dev = [ [[package]] name = "langgraph-sdk" -version = "0.1.72" +version = "0.2.0a1" source = { editable = "../libs/sdk-py" } dependencies = [ { name = "httpx" }, diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py index 242c4e5e8..de1d0b648 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/__init__.py @@ -284,10 +284,7 @@ class PostgresSaver(BasePostgresSaver): configurable = config["configurable"].copy() thread_id = configurable.pop("thread_id") checkpoint_ns = configurable.pop("checkpoint_ns") - checkpoint_id = configurable.pop( - "checkpoint_id", configurable.pop("thread_ts", None) - ) - + checkpoint_id = configurable.pop("checkpoint_id", None) copy = checkpoint.copy() copy["channel_values"] = copy["channel_values"].copy() next_config = { diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py index f23c779f8..e7c95c42b 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/aio.py @@ -240,9 +240,7 @@ class AsyncPostgresSaver(BasePostgresSaver): configurable = config["configurable"].copy() thread_id = configurable.pop("thread_id") checkpoint_ns = configurable.pop("checkpoint_ns") - checkpoint_id = configurable.pop( - "checkpoint_id", configurable.pop("thread_ts", None) - ) + checkpoint_id = configurable.pop("checkpoint_id", None) copy = checkpoint.copy() copy["channel_values"] = copy["channel_values"].copy() diff --git a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/shallow.py b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/shallow.py index 16f4094ae..6626332b5 100644 --- a/libs/checkpoint-postgres/langgraph/checkpoint/postgres/shallow.py +++ b/libs/checkpoint-postgres/langgraph/checkpoint/postgres/shallow.py @@ -191,7 +191,7 @@ class ShallowPostgresSaver(BasePostgresSaver): ) -> None: warnings.warn( "ShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. " - "Use PostgresSaver instead, and invoke the graph with `graph.invoke(..., checkpoint_during=False)`.", + "Use PostgresSaver instead, and invoke the graph with `graph.invoke(..., durability='exit')`.", DeprecationWarning, stacklevel=2, ) @@ -547,7 +547,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver): ) -> None: warnings.warn( "AsyncShallowPostgresSaver is deprecated as of version 2.0.20 and will be removed in 3.0.0. " - "Use AsyncPostgresSaver instead, and invoke the graph with `await graph.ainvoke(..., checkpoint_during=False)`.", + "Use AsyncPostgresSaver instead, and invoke the graph with `await graph.ainvoke(..., durability='exit')`.", DeprecationWarning, stacklevel=2, ) diff --git a/libs/checkpoint-postgres/tests/test_async.py b/libs/checkpoint-postgres/tests/test_async.py index 9027307f6..fb3af3317 100644 --- a/libs/checkpoint-postgres/tests/test_async.py +++ b/libs/checkpoint-postgres/tests/test_async.py @@ -161,8 +161,7 @@ def test_data(): config_1: RunnableConfig = { "configurable": { "thread_id": "thread-1", - # for backwards compatibility testing - "thread_ts": "1", + "checkpoint_id": "1", "checkpoint_ns": "", } } diff --git a/libs/checkpoint-postgres/tests/test_sync.py b/libs/checkpoint-postgres/tests/test_sync.py index 3c212135d..e6d2720e4 100644 --- a/libs/checkpoint-postgres/tests/test_sync.py +++ b/libs/checkpoint-postgres/tests/test_sync.py @@ -143,8 +143,7 @@ def test_data(): config_1: RunnableConfig = { "configurable": { "thread_id": "thread-1", - # for backwards compatibility testing - "thread_ts": "1", + "checkpoint_id": "1", "checkpoint_ns": "", } } diff --git a/libs/checkpoint-sqlite/tests/test_aiosqlite.py b/libs/checkpoint-sqlite/tests/test_aiosqlite.py index 5a471aef4..02dedd31a 100644 --- a/libs/checkpoint-sqlite/tests/test_aiosqlite.py +++ b/libs/checkpoint-sqlite/tests/test_aiosqlite.py @@ -19,8 +19,7 @@ class TestAsyncSqliteSaver: self.config_1: RunnableConfig = { "configurable": { "thread_id": "thread-1", - # for backwards compatibility testing - "thread_ts": "1", + "checkpoint_id": "1", "checkpoint_ns": "", } } diff --git a/libs/checkpoint-sqlite/tests/test_sqlite.py b/libs/checkpoint-sqlite/tests/test_sqlite.py index b672c54d9..d2159a5ea 100644 --- a/libs/checkpoint-sqlite/tests/test_sqlite.py +++ b/libs/checkpoint-sqlite/tests/test_sqlite.py @@ -21,7 +21,7 @@ class TestSqliteSaver: "configurable": { "thread_id": "thread-1", # for backwards compatibility testing - "thread_ts": "1", + "checkpoint_id": "1", "checkpoint_ns": "", } } diff --git a/libs/checkpoint/README.md b/libs/checkpoint/README.md index f3fd43836..e78a6da0e 100644 --- a/libs/checkpoint/README.md +++ b/libs/checkpoint/README.md @@ -36,7 +36,7 @@ Each checkpointer should conform to `langgraph.checkpoint.base.BaseCheckpointSav - `.put` - Store a checkpoint with its configuration and metadata. - `.put_writes` - Store intermediate writes linked to a checkpoint (i.e. pending writes). -- `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `thread_ts`). +- `.get_tuple` - Fetch a checkpoint tuple using for a given configuration (`thread_id` and `checkpoint_id`). - `.list` - List checkpoints that match a given configuration and filter criteria. If the checkpointer will be used with asynchronous graph execution (i.e. executing the graph via `.ainvoke`, `.astream`, `.abatch`), checkpointer must implement asynchronous versions of the above methods (`.aput`, `.aput_writes`, `.aget_tuple`, `.alist`). diff --git a/libs/checkpoint/langgraph/checkpoint/base/__init__.py b/libs/checkpoint/langgraph/checkpoint/base/__init__.py index 9719118d2..a5704e13f 100644 --- a/libs/checkpoint/langgraph/checkpoint/base/__init__.py +++ b/libs/checkpoint/langgraph/checkpoint/base/__init__.py @@ -375,10 +375,8 @@ class EmptyChannelError(Exception): def get_checkpoint_id(config: RunnableConfig) -> str | None: - """Get checkpoint ID in a backwards-compatible manner (fallback on thread_ts).""" - return config["configurable"].get( - "checkpoint_id", config["configurable"].get("thread_ts") - ) + """Get checkpoint ID.""" + return config["configurable"].get("checkpoint_id") def get_checkpoint_metadata( @@ -413,7 +411,6 @@ WRITES_IDX_MAP = {ERROR: -1, SCHEDULED: -2, INTERRUPT: -3, RESUME: -4} EXCLUDED_METADATA_KEYS = { "thread_id", - "thread_ts", "checkpoint_id", "checkpoint_ns", "checkpoint_map", diff --git a/libs/checkpoint/tests/test_memory.py b/libs/checkpoint/tests/test_memory.py index a39ddad5e..e3fc4aa58 100644 --- a/libs/checkpoint/tests/test_memory.py +++ b/libs/checkpoint/tests/test_memory.py @@ -22,8 +22,7 @@ class TestMemorySaver: "configurable": { "thread_id": "thread-1", "checkpoint_ns": "", - # for backwards compatibility testing - "thread_ts": "1", + "checkpoint_id": "1", } } self.config_2: RunnableConfig = { diff --git a/libs/cli/examples/graphs/agent.py b/libs/cli/examples/graphs/agent.py index cf874a2ae..f39df4cae 100644 --- a/libs/cli/examples/graphs/agent.py +++ b/libs/cli/examples/graphs/agent.py @@ -49,12 +49,12 @@ def call_model(state, config): tool_node = ToolNode(tools) -class ConfigSchema(TypedDict): +class ContextSchema(TypedDict): model: Literal["anthropic", "openai"] # Define a new graph -workflow = StateGraph(AgentState, config_schema=ConfigSchema) +workflow = StateGraph(AgentState, context_schema=ContextSchema) # Define the two nodes we will cycle between workflow.add_node("agent", call_model) diff --git a/libs/langgraph/bench/__main__.py b/libs/langgraph/bench/__main__.py index f860ede57..42dbadd44 100644 --- a/libs/langgraph/bench/__main__.py +++ b/libs/langgraph/bench/__main__.py @@ -26,7 +26,7 @@ async def arun(graph: Pregel, input: dict): "configurable": {"thread_id": str(uuid4())}, "recursion_limit": 1000000000, }, - checkpoint_during=False, + durability="exit", ) ] ) @@ -43,7 +43,7 @@ async def arun_first_event_latency(graph: Pregel, input: dict) -> None: "configurable": {"thread_id": str(uuid4())}, "recursion_limit": 1000000000, }, - checkpoint_during=False, + durability="exit", ) try: @@ -63,7 +63,7 @@ def run(graph: Pregel, input: dict): "configurable": {"thread_id": str(uuid4())}, "recursion_limit": 1000000000, }, - checkpoint_during=False, + durability="exit", ) ] ) @@ -80,7 +80,7 @@ def run_first_event_latency(graph: Pregel, input: dict) -> None: "configurable": {"thread_id": str(uuid4())}, "recursion_limit": 1000000000, }, - checkpoint_during=False, + durability="exit", ) try: diff --git a/libs/langgraph/bench/fanout_to_subgraph.py b/libs/langgraph/bench/fanout_to_subgraph.py index e6ebc001a..42014c2b8 100644 --- a/libs/langgraph/bench/fanout_to_subgraph.py +++ b/libs/langgraph/bench/fanout_to_subgraph.py @@ -3,8 +3,9 @@ from typing import Annotated from typing_extensions import TypedDict -from langgraph.constants import END, START, Send +from langgraph.constants import END, START from langgraph.graph.state import StateGraph +from langgraph.types import Send def fanout_to_subgraph() -> StateGraph: diff --git a/libs/langgraph/bench/sequential.py b/libs/langgraph/bench/sequential.py index bfdad823e..886f51ba3 100644 --- a/libs/langgraph/bench/sequential.py +++ b/libs/langgraph/bench/sequential.py @@ -1,7 +1,7 @@ """Create a sequential no-op graph consisting of a few hundred nodes.""" +from langgraph._internal._runnable import RunnableCallable from langgraph.graph import MessagesState, StateGraph -from langgraph.utils.runnable import RunnableCallable def create_sequential(number_nodes: int) -> StateGraph: diff --git a/libs/langgraph/langgraph/_internal/__init__.py b/libs/langgraph/langgraph/_internal/__init__.py new file mode 100644 index 000000000..2e71cdc23 --- /dev/null +++ b/libs/langgraph/langgraph/_internal/__init__.py @@ -0,0 +1,4 @@ +"""Internal modules for LangGraph. + +This module is not part of the public API, and thus stability is not guaranteed. +""" diff --git a/libs/langgraph/langgraph/utils/cache.py b/libs/langgraph/langgraph/_internal/_cache.py similarity index 100% rename from libs/langgraph/langgraph/utils/cache.py rename to libs/langgraph/langgraph/_internal/_cache.py diff --git a/libs/langgraph/langgraph/utils/config.py b/libs/langgraph/langgraph/_internal/_config.py similarity index 98% rename from libs/langgraph/langgraph/utils/config.py rename to libs/langgraph/langgraph/_internal/_config.py index ace98cd91..0b4739c98 100644 --- a/libs/langgraph/langgraph/utils/config.py +++ b/libs/langgraph/langgraph/_internal/_config.py @@ -18,9 +18,7 @@ from langchain_core.runnables.config import ( var_child_runnable_config, ) -from langgraph.checkpoint.base import CheckpointMetadata -from langgraph.config import get_config, get_store, get_stream_writer # noqa -from langgraph.constants import ( +from langgraph._internal._constants import ( CONF, CONFIG_KEY_CHECKPOINT_ID, CONFIG_KEY_CHECKPOINT_MAP, @@ -28,6 +26,7 @@ from langgraph.constants import ( NS_END, NS_SEP, ) +from langgraph.checkpoint.base import CheckpointMetadata DEFAULT_RECURSION_LIMIT = int(getenv("LANGGRAPH_DEFAULT_RECURSION_LIMIT", "25")) diff --git a/libs/langgraph/langgraph/_internal/_constants.py b/libs/langgraph/langgraph/_internal/_constants.py new file mode 100644 index 000000000..abab56295 --- /dev/null +++ b/libs/langgraph/langgraph/_internal/_constants.py @@ -0,0 +1,110 @@ +"""Constants used for Pregel operations.""" + +import sys +from typing import Literal, cast + +# --- Reserved write keys --- +INPUT = sys.intern("__input__") +# for values passed as input to the graph +INTERRUPT = sys.intern("__interrupt__") +# for dynamic interrupts raised by nodes +RESUME = sys.intern("__resume__") +# for values passed to resume a node after an interrupt +ERROR = sys.intern("__error__") +# for errors raised by nodes +NO_WRITES = sys.intern("__no_writes__") +# marker to signal node didn't write anything +TASKS = sys.intern("__pregel_tasks") +# for Send objects returned by nodes/edges, corresponds to PUSH below +RETURN = sys.intern("__return__") +# for writes of a task where we simply record the return value +PREVIOUS = sys.intern("__previous__") +# the implicit branch that handles each node's Control values + + +# --- Reserved cache namespaces --- +CACHE_NS_WRITES = sys.intern("__pregel_ns_writes") +# cache namespace for node writes + +# --- Reserved config.configurable keys --- +CONFIG_KEY_SEND = sys.intern("__pregel_send") +# holds the `write` function that accepts writes to state/edges/reserved keys +CONFIG_KEY_READ = sys.intern("__pregel_read") +# holds the `read` function that returns a copy of the current state +CONFIG_KEY_CALL = sys.intern("__pregel_call") +# holds the `call` function that accepts a node/func, args and returns a future +CONFIG_KEY_CHECKPOINTER = sys.intern("__pregel_checkpointer") +# holds a `BaseCheckpointSaver` passed from parent graph to child graphs +CONFIG_KEY_STREAM = sys.intern("__pregel_stream") +# holds a `StreamProtocol` passed from parent graph to child graphs +CONFIG_KEY_CACHE = sys.intern("__pregel_cache") +# holds a `BaseCache` made available to subgraphs +CONFIG_KEY_RESUMING = sys.intern("__pregel_resuming") +# holds a boolean indicating if subgraphs should resume from a previous checkpoint +CONFIG_KEY_TASK_ID = sys.intern("__pregel_task_id") +# holds the task ID for the current task +CONFIG_KEY_THREAD_ID = sys.intern("thread_id") +# holds the thread ID for the current invocation +CONFIG_KEY_CHECKPOINT_MAP = sys.intern("checkpoint_map") +# holds a mapping of checkpoint_ns -> checkpoint_id for parent graphs +CONFIG_KEY_CHECKPOINT_ID = sys.intern("checkpoint_id") +# holds the current checkpoint_id, if any +CONFIG_KEY_CHECKPOINT_NS = sys.intern("checkpoint_ns") +# holds the current checkpoint_ns, "" for root graph +CONFIG_KEY_NODE_FINISHED = sys.intern("__pregel_node_finished") +# holds a callback to be called when a node is finished +CONFIG_KEY_SCRATCHPAD = sys.intern("__pregel_scratchpad") +# holds a mutable dict for temporary storage scoped to the current task +CONFIG_KEY_RUNNER_SUBMIT = sys.intern("__pregel_runner_submit") +# holds a function that receives tasks from runner, executes them and returns results +CONFIG_KEY_DURABILITY = sys.intern("__pregel_durability") +# holds the durability mode, one of "sync", "async", or "exit" +CONFIG_KEY_RUNTIME = sys.intern("__pregel_runtime") +# holds a `Runtime` instance with context, store, stream writer, etc. +CONFIG_KEY_RESUME_MAP = sys.intern("__pregel_resume_map") +# holds a mapping of task ns -> resume value for resuming tasks + +# --- Other constants --- +PUSH = sys.intern("__pregel_push") +# denotes push-style tasks, ie. those created by Send objects +PULL = sys.intern("__pregel_pull") +# denotes pull-style tasks, ie. those triggered by edges +NS_SEP = sys.intern("|") +# for checkpoint_ns, separates each level (ie. graph|subgraph|subsubgraph) +NS_END = sys.intern(":") +# for checkpoint_ns, for each level, separates the namespace from the task_id +CONF = cast(Literal["configurable"], sys.intern("configurable")) +# key for the configurable dict in RunnableConfig +NULL_TASK_ID = sys.intern("00000000-0000-0000-0000-000000000000") +# the task_id to use for writes that are not associated with a task + +# redefined to avoid circular import with langgraph.constants +_TAG_HIDDEN = sys.intern("langsmith:hidden") + +RESERVED = { + _TAG_HIDDEN, + # reserved write keys + INPUT, + INTERRUPT, + RESUME, + ERROR, + NO_WRITES, + # reserved config.configurable keys + CONFIG_KEY_SEND, + CONFIG_KEY_READ, + CONFIG_KEY_CHECKPOINTER, + CONFIG_KEY_STREAM, + CONFIG_KEY_CHECKPOINT_MAP, + CONFIG_KEY_RESUMING, + CONFIG_KEY_TASK_ID, + CONFIG_KEY_CHECKPOINT_MAP, + CONFIG_KEY_CHECKPOINT_ID, + CONFIG_KEY_CHECKPOINT_NS, + CONFIG_KEY_RESUME_MAP, + # other constants + PUSH, + PULL, + NS_SEP, + NS_END, + CONF, +} diff --git a/libs/langgraph/langgraph/utils/fields.py b/libs/langgraph/langgraph/_internal/_fields.py similarity index 98% rename from libs/langgraph/langgraph/utils/fields.py rename to libs/langgraph/langgraph/_internal/_fields.py index 5b9c8dca6..6979678d1 100644 --- a/libs/langgraph/langgraph/utils/fields.py +++ b/libs/langgraph/langgraph/_internal/_fields.py @@ -9,9 +9,7 @@ from typing import Annotated, Any, Optional, Union, get_type_hints from pydantic import BaseModel from typing_extensions import NotRequired, ReadOnly, Required, get_origin -# NOTE: this is redefined here separately from langgraph.constants -# to avoid a circular import -MISSING = object() +from langgraph._internal._typing import MISSING def _is_optional_type(type_: Any) -> bool: diff --git a/libs/langgraph/langgraph/utils/future.py b/libs/langgraph/langgraph/_internal/_future.py similarity index 100% rename from libs/langgraph/langgraph/utils/future.py rename to libs/langgraph/langgraph/_internal/_future.py diff --git a/libs/langgraph/langgraph/utils/pydantic.py b/libs/langgraph/langgraph/_internal/_pydantic.py similarity index 100% rename from libs/langgraph/langgraph/utils/pydantic.py rename to libs/langgraph/langgraph/_internal/_pydantic.py diff --git a/libs/langgraph/langgraph/utils/queue.py b/libs/langgraph/langgraph/_internal/_queue.py similarity index 99% rename from libs/langgraph/langgraph/utils/queue.py rename to libs/langgraph/langgraph/_internal/_queue.py index c0717fe34..b495e15c7 100644 --- a/libs/langgraph/langgraph/utils/queue.py +++ b/libs/langgraph/langgraph/_internal/_queue.py @@ -128,6 +128,3 @@ class SyncQueue: return len(self._queue) __class_getitem__ = classmethod(types.GenericAlias) - - -__all__ = ["AsyncQueue", "SyncQueue"] diff --git a/libs/langgraph/langgraph/_internal/_retry.py b/libs/langgraph/langgraph/_internal/_retry.py new file mode 100644 index 000000000..8d4e41fd7 --- /dev/null +++ b/libs/langgraph/langgraph/_internal/_retry.py @@ -0,0 +1,29 @@ +def default_retry_on(exc: Exception) -> bool: + import httpx + import requests + + if isinstance(exc, ConnectionError): + return True + if isinstance(exc, httpx.HTTPStatusError): + return 500 <= exc.response.status_code < 600 + if isinstance(exc, requests.HTTPError): + return 500 <= exc.response.status_code < 600 if exc.response else True + if isinstance( + exc, + ( + ValueError, + TypeError, + ArithmeticError, + ImportError, + LookupError, + NameError, + SyntaxError, + RuntimeError, + ReferenceError, + StopIteration, + StopAsyncIteration, + OSError, + ), + ): + return False + return True diff --git a/libs/langgraph/langgraph/utils/runnable.py b/libs/langgraph/langgraph/_internal/_runnable.py similarity index 88% rename from libs/langgraph/langgraph/utils/runnable.py rename to libs/langgraph/langgraph/_internal/_runnable.py index 4274c8134..efa0dc825 100644 --- a/libs/langgraph/langgraph/utils/runnable.py +++ b/libs/langgraph/langgraph/_internal/_runnable.py @@ -42,20 +42,19 @@ from langchain_core.runnables.utils import Input, Output from langchain_core.tracers.langchain import LangChainTracer from typing_extensions import TypeGuard -from langgraph.constants import ( - CONF, - CONFIG_KEY_PREVIOUS, - CONFIG_KEY_STORE, - CONFIG_KEY_STREAM_WRITER, -) -from langgraph.store.base import BaseStore -from langgraph.types import StreamWriter -from langgraph.utils.config import ( +from langgraph._internal._config import ( ensure_config, get_async_callback_manager_for_config, get_callback_manager_for_config, patch_config, ) +from langgraph._internal._constants import ( + CONF, + CONFIG_KEY_RUNTIME, +) +from langgraph._internal._typing import MISSING +from langgraph.store.base import BaseStore +from langgraph.types import StreamWriter try: from langchain_core.tracers._streaming import _StreamingCallbackHandler @@ -128,45 +127,52 @@ ANY_TYPE = object() ASYNCIO_ACCEPTS_CONTEXT = sys.version_info >= (3, 11) -# List of keyword arguments that can be injected at runtime from the config object. +# List of keyword arguments that can be injected into nodes / tasks / tools at runtime. # A named argument may appear multiple times if it appears with distinct types. KWARGS_CONFIG_KEYS: tuple[tuple[str, tuple[Any, ...], str, Any], ...] = ( ( - sys.intern("writer"), + "config", + (RunnableConfig, "RunnableConfig", inspect.Parameter.empty), + # for now, use config directly, eventually, will pop off of Runtime + "N/A", + inspect.Parameter.empty, + ), + ( + "writer", (StreamWriter, "StreamWriter", inspect.Parameter.empty), - CONFIG_KEY_STREAM_WRITER, + "stream_writer", lambda _: None, ), ( - # Covers store that is not optional (will raise an error if a store - # cannot be injected). - sys.intern("store"), + "store", ( BaseStore, "BaseStore", inspect.Parameter.empty, ), - CONFIG_KEY_STORE, + "store", inspect.Parameter.empty, ), ( - # Covers store that is optional. Will set to None if not found in config. - sys.intern("store"), + "store", ( Optional[BaseStore], - # Best effort to catch some forward references. - # This will not work for cases like `"Union[None, BaseStore]"`, - # we'll need to re-write logic to use get_type_hints() - # to resolve forward references. "Optional[BaseStore]", ), - CONFIG_KEY_STORE, + "store", None, ), ( - sys.intern("previous"), + "previous", (ANY_TYPE,), - CONFIG_KEY_PREVIOUS, + "previous", + inspect.Parameter.empty, + ), + ( + "runtime", + (ANY_TYPE,), + # we never hit this block, we just inject runtime directly + "N/A", inspect.Parameter.empty, ), ) @@ -174,7 +180,7 @@ KWARGS_CONFIG_KEYS: tuple[tuple[str, tuple[Any, ...], str, Any], ...] = ( config keys, default values and type annotations. Used to configure keyword arguments that can be injected at runtime -from the config object as kwargs to `invoke`, `ainvoke`, `stream` and `astream`. +from the `Runtime` object as kwargs to `invoke`, `ainvoke`, `stream` and `astream`. For a keyword to be injected from the config object, the function signature must contain a kwarg with the same name and a matching type annotation. @@ -182,8 +188,10 @@ must contain a kwarg with the same name and a matching type annotation. Each tuple contains: - the name of the kwarg in the function signature - the type annotation(s) for the kwarg -- the config key to look for the value in -- the default value for the kwarg +- the `Runtime` attribute for fetching the value (N/A if not applicable) + +This is fully internal and should be further refactored to use `get_type_hints` +to resolve forward references and optional types formatted like BaseStore | None. """ VALID_KINDS = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY) @@ -250,7 +258,6 @@ class RunnableCallable(Runnable): trace: bool = True, recurse: bool = True, explode_args: bool = False, - func_accepts_config: bool | None = None, **kwargs: Any, ) -> None: self.name = name @@ -277,31 +284,23 @@ class RunnableCallable(Runnable): if func is None and afunc is None: raise ValueError("At least one of func or afunc must be provided.") - if func_accepts_config is not None: - self.func_accepts_config = func_accepts_config - self.func_accepts: dict[str, tuple[str, Any]] = {} - else: - params = inspect.signature(cast(Callable, func or afunc)).parameters + self.func_accepts: dict[str, tuple[str, Any]] = {} + params = inspect.signature(cast(Callable, func or afunc)).parameters - self.func_accepts_config = "config" in params - # Mapping from kwarg name to (config key, default value) to be used. - # The default value is used if the config key is not found in the config. - self.func_accepts = {} + for kw, typ, runtime_key, default in KWARGS_CONFIG_KEYS: + p = params.get(kw) - for kw, typ, config_key, default in KWARGS_CONFIG_KEYS: - p = params.get(kw) + if p is None or p.kind not in VALID_KINDS: + # If parameter is not found or is not a valid kind, skip + continue - if p is None or p.kind not in VALID_KINDS: - # If parameter is not found or is not a valid kind, skip - continue + if typ != (ANY_TYPE,) and p.annotation not in typ: + # A specific type is required, but the function annotation does + # not match the expected type. + continue - if typ != (ANY_TYPE,) and p.annotation not in typ: - # A specific type is required, but the function annotation does - # not match the expected type. - continue - - # If the kwarg is accepted by the function, store the default value - self.func_accepts[kw] = (config_key, default) + # If the kwarg is accepted by the function, store the key / runtime attribute to inject + self.func_accepts[kw] = (runtime_key, default) def __repr__(self) -> str: repr_args = { @@ -328,25 +327,33 @@ class RunnableCallable(Runnable): else: args = (input,) kwargs = {**self.kwargs, **kwargs} - if self.func_accepts_config: - kwargs["config"] = config - _conf = config[CONF] - for kw, (config_key, default_value) in self.func_accepts.items(): + runtime = config[CONF].get(CONFIG_KEY_RUNTIME) + + for kw, (runtime_key, default) in self.func_accepts.items(): # If the kwarg is already set, use the set value if kw in kwargs: continue - if ( - # If the kwarg is requested, but isn't in the config AND has no - # default value, raise an error - config_key not in _conf and default_value is inspect.Parameter.empty - ): - raise ValueError( - f"Missing required config key '{config_key}' for '{self.name}'." - ) + kw_value: Any = MISSING + if kw == "config": + kw_value = config + elif runtime: + if kw == "runtime": + kw_value = runtime + else: + try: + kw_value = getattr(runtime, runtime_key) + except AttributeError: + pass - kwargs[kw] = _conf.get(config_key, default_value) + if kw_value is MISSING: + if default is inspect.Parameter.empty: + raise ValueError( + f"Missing required config key '{runtime_key}' for '{self.name}'." + ) + kw_value = default + kwargs[kw] = kw_value if self.trace: callback_manager = get_callback_manager_for_config(config, self.tags) @@ -392,23 +399,33 @@ class RunnableCallable(Runnable): else: args = (input,) kwargs = {**self.kwargs, **kwargs} - if self.func_accepts_config: - kwargs["config"] = config - _conf = config[CONF] - for kw, (config_key, default_value) in self.func_accepts.items(): + + runtime = config[CONF].get(CONFIG_KEY_RUNTIME) + + for kw, (runtime_key, default) in self.func_accepts.items(): # If the kwarg has already been set, use the set value if kw in kwargs: continue - if ( - # If the kwarg is requested, but isn't in the config AND has no - # default value, raise an error - config_key not in _conf and default_value is inspect.Parameter.empty - ): - raise ValueError( - f"Missing required config key '{config_key}' for '{self.name}'." - ) - kwargs[kw] = _conf.get(config_key, default_value) + kw_value: Any = MISSING + if kw == "config": + kw_value = config + elif runtime: + if kw == "runtime": + kw_value = runtime + else: + try: + kw_value = getattr(runtime, runtime_key) + except AttributeError: + pass + if kw_value is MISSING: + if default is inspect.Parameter.empty: + raise ValueError( + f"Missing required config key '{runtime_key}' for '{self.name}'." + ) + kw_value = default + kwargs[kw] = kw_value + if self.trace: callback_manager = get_async_callback_manager_for_config(config, self.tags) run_manager = await callback_manager.on_chain_start( diff --git a/libs/langgraph/langgraph/_typing.py b/libs/langgraph/langgraph/_internal/_typing.py similarity index 92% rename from libs/langgraph/langgraph/_typing.py rename to libs/langgraph/langgraph/_internal/_typing.py index 79b5478d0..02adb3364 100644 --- a/libs/langgraph/langgraph/_typing.py +++ b/libs/langgraph/langgraph/_internal/_typing.py @@ -42,13 +42,13 @@ It can either be a `TypedDict`, `dataclass`, or Pydantic `BaseModel`. Note: we cannot use either `TypedDict` or `dataclass` directly due to limitations in type checking. """ - -class Unset: - """A sentinel value to represent an unset type.""" - - -UNSET: Unset = Unset() +MISSING = object() +"""Unset sentinel value.""" class DeprecatedKwargs(TypedDict): """TypedDict to use for extra keyword arguments, enabling type checking warnings for deprecated arguments.""" + + +EMPTY_SEQ: tuple[str, ...] = tuple() +"""An empty sequence of strings.""" diff --git a/libs/langgraph/langgraph/channels/__init__.py b/libs/langgraph/langgraph/channels/__init__.py index cdb193484..a69c230b5 100644 --- a/libs/langgraph/langgraph/channels/__init__.py +++ b/libs/langgraph/langgraph/channels/__init__.py @@ -1,15 +1,27 @@ from langgraph.channels.any_value import AnyValue +from langgraph.channels.base import BaseChannel from langgraph.channels.binop import BinaryOperatorAggregate from langgraph.channels.ephemeral_value import EphemeralValue -from langgraph.channels.last_value import LastValue +from langgraph.channels.last_value import LastValue, LastValueAfterFinish +from langgraph.channels.named_barrier_value import ( + NamedBarrierValue, + NamedBarrierValueAfterFinish, +) from langgraph.channels.topic import Topic from langgraph.channels.untracked_value import UntrackedValue -__all__ = [ +__all__ = ( + # base + "BaseChannel", + # value types + "AnyValue", "LastValue", - "Topic", - "BinaryOperatorAggregate", + "LastValueAfterFinish", "UntrackedValue", "EphemeralValue", - "AnyValue", -] + "BinaryOperatorAggregate", + "NamedBarrierValue", + "NamedBarrierValueAfterFinish", + # topics + "Topic", +) diff --git a/libs/langgraph/langgraph/channels/any_value.py b/libs/langgraph/langgraph/channels/any_value.py index ec597dacb..9ba255574 100644 --- a/libs/langgraph/langgraph/channels/any_value.py +++ b/libs/langgraph/langgraph/channels/any_value.py @@ -1,12 +1,16 @@ +from __future__ import annotations + from collections.abc import Sequence from typing import Any, Generic from typing_extensions import Self +from langgraph._internal._typing import MISSING from langgraph.channels.base import BaseChannel, Value -from langgraph.constants import MISSING from langgraph.errors import EmptyChannelError +__all__ = ("AnyValue",) + class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]): """Stores the last value received, assumes that if multiple values are @@ -14,6 +18,8 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]): __slots__ = ("typ", "value") + value: Value | Any + def __init__(self, typ: Any, key: str = "") -> None: super().__init__(typ, key) self.value = MISSING diff --git a/libs/langgraph/langgraph/channels/base.py b/libs/langgraph/langgraph/channels/base.py index 4d6335bc1..2d00da64f 100644 --- a/libs/langgraph/langgraph/channels/base.py +++ b/libs/langgraph/langgraph/channels/base.py @@ -1,18 +1,22 @@ +from __future__ import annotations + from abc import ABC, abstractmethod from collections.abc import Sequence from typing import Any, Generic, TypeVar from typing_extensions import Self -from langgraph.constants import MISSING -from langgraph.errors import EmptyChannelError, InvalidUpdateError +from langgraph._internal._typing import MISSING +from langgraph.errors import EmptyChannelError Value = TypeVar("Value") Update = TypeVar("Update") -C = TypeVar("C") +Checkpoint = TypeVar("Checkpoint") + +__all__ = ("BaseChannel",) -class BaseChannel(Generic[Value, Update, C], ABC): +class BaseChannel(Generic[Value, Update, Checkpoint], ABC): """Base class for all channels.""" __slots__ = ("key", "typ") @@ -39,7 +43,7 @@ class BaseChannel(Generic[Value, Update, C], ABC): Subclasses can override this method with a more efficient implementation.""" return self.from_checkpoint(self.checkpoint()) - def checkpoint(self) -> C: + def checkpoint(self) -> Checkpoint | Any: """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.""" @@ -49,7 +53,7 @@ class BaseChannel(Generic[Value, Update, C], ABC): return MISSING @abstractmethod - def from_checkpoint(self, checkpoint: C) -> Self: + def from_checkpoint(self, checkpoint: Checkpoint | Any) -> Self: """Return a new identical channel, optionally initialized from a checkpoint. If the checkpoint contains complex data structures, they should be copied.""" @@ -99,10 +103,3 @@ class BaseChannel(Generic[Value, Update, C], ABC): Returns True if the channel was updated, False otherwise. """ return False - - -__all__ = [ - "BaseChannel", - "EmptyChannelError", - "InvalidUpdateError", -] diff --git a/libs/langgraph/langgraph/channels/binop.py b/libs/langgraph/langgraph/channels/binop.py index e974c5fba..d47c4e049 100644 --- a/libs/langgraph/langgraph/channels/binop.py +++ b/libs/langgraph/langgraph/channels/binop.py @@ -4,10 +4,12 @@ from typing import Callable, Generic from typing_extensions import NotRequired, Required, Self +from langgraph._internal._typing import MISSING from langgraph.channels.base import BaseChannel, Value -from langgraph.constants import MISSING from langgraph.errors import EmptyChannelError +__all__ = ("BinaryOperatorAggregate",) + # Adapted from typing_extensions def _strip_extras(t): # type: ignore[no-untyped-def] diff --git a/libs/langgraph/langgraph/channels/ephemeral_value.py b/libs/langgraph/langgraph/channels/ephemeral_value.py index 7448be106..108588d0b 100644 --- a/libs/langgraph/langgraph/channels/ephemeral_value.py +++ b/libs/langgraph/langgraph/channels/ephemeral_value.py @@ -1,18 +1,25 @@ +from __future__ import annotations + from collections.abc import Sequence from typing import Any, Generic from typing_extensions import Self +from langgraph._internal._typing import MISSING from langgraph.channels.base import BaseChannel, Value -from langgraph.constants import MISSING from langgraph.errors import EmptyChannelError, InvalidUpdateError +__all__ = ("EphemeralValue",) + class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]): """Stores the value received in the step immediately preceding, clears after.""" __slots__ = ("value", "guard") + value: Value | Any + guard: bool + def __init__(self, typ: Any, guard: bool = True) -> None: super().__init__(typ) self.guard = guard diff --git a/libs/langgraph/langgraph/channels/last_value.py b/libs/langgraph/langgraph/channels/last_value.py index 59c8d3c1b..54caac758 100644 --- a/libs/langgraph/langgraph/channels/last_value.py +++ b/libs/langgraph/langgraph/channels/last_value.py @@ -1,10 +1,12 @@ +from __future__ import annotations + from collections.abc import Sequence from typing import Any, Generic from typing_extensions import Self +from langgraph._internal._typing import MISSING from langgraph.channels.base import BaseChannel, Value -from langgraph.constants import MISSING from langgraph.errors import ( EmptyChannelError, ErrorCode, @@ -12,12 +14,16 @@ from langgraph.errors import ( create_error_message, ) +__all__ = ("LastValue", "LastValueAfterFinish") + class LastValue(Generic[Value], BaseChannel[Value, Value, Value]): """Stores the last value received, can receive at most one value per step.""" __slots__ = ("value",) + value: Value | Any + def __init__(self, typ: Any, key: str = "") -> None: super().__init__(typ, key) self.value = MISSING @@ -80,6 +86,9 @@ class LastValueAfterFinish( __slots__ = ("value", "finished") + value: Value | Any + finished: bool + def __init__(self, typ: Any, key: str = "") -> None: super().__init__(typ, key) self.value = MISSING @@ -98,19 +107,19 @@ class LastValueAfterFinish( """The type of the update received by the channel.""" return self.typ - def checkpoint(self) -> tuple[Value, bool]: + def checkpoint(self) -> tuple[Value | Any, bool] | Any: if self.value is MISSING: return MISSING return (self.value, self.finished) - def from_checkpoint(self, checkpoint: tuple[Value, bool]) -> Self: + def from_checkpoint(self, checkpoint: tuple[Value | Any, bool] | Any) -> Self: empty = self.__class__(self.typ) empty.key = self.key if checkpoint is not MISSING: empty.value, empty.finished = checkpoint return empty - def update(self, values: Sequence[Value]) -> bool: + def update(self, values: Sequence[Value | Any]) -> bool: if len(values) == 0: return False diff --git a/libs/langgraph/langgraph/channels/named_barrier_value.py b/libs/langgraph/langgraph/channels/named_barrier_value.py index e5e96a7fb..d45644110 100644 --- a/libs/langgraph/langgraph/channels/named_barrier_value.py +++ b/libs/langgraph/langgraph/channels/named_barrier_value.py @@ -3,10 +3,12 @@ from typing import Generic from typing_extensions import Self +from langgraph._internal._typing import MISSING from langgraph.channels.base import BaseChannel, Value -from langgraph.constants import MISSING from langgraph.errors import EmptyChannelError, InvalidUpdateError +__all__ = ("NamedBarrierValue", "NamedBarrierValueAfterFinish") + class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]): """A channel that waits until all named values are received before making the value available.""" diff --git a/libs/langgraph/langgraph/channels/topic.py b/libs/langgraph/langgraph/channels/topic.py index fa96bcd59..917798ff2 100644 --- a/libs/langgraph/langgraph/channels/topic.py +++ b/libs/langgraph/langgraph/channels/topic.py @@ -5,12 +5,14 @@ from typing import Any, Generic, Union from typing_extensions import Self +from langgraph._internal._typing import MISSING from langgraph.channels.base import BaseChannel, Value -from langgraph.constants import MISSING from langgraph.errors import EmptyChannelError +__all__ = ("Topic",) -def flatten(values: Sequence[Value | list[Value]]) -> Iterator[Value]: + +def _flatten(values: Sequence[Value | list[Value]]) -> Iterator[Value]: for value in values: if isinstance(value, list): yield from value @@ -77,7 +79,7 @@ class Topic( if not self.accumulate: updated = bool(self.values) self.values = list[Value]() - if flat_values := tuple(flatten(values)): + if flat_values := tuple(_flatten(values)): updated = True self.values.extend(flat_values) return updated diff --git a/libs/langgraph/langgraph/channels/untracked_value.py b/libs/langgraph/langgraph/channels/untracked_value.py index e0c9cb676..bcd55186b 100644 --- a/libs/langgraph/langgraph/channels/untracked_value.py +++ b/libs/langgraph/langgraph/channels/untracked_value.py @@ -1,18 +1,25 @@ +from __future__ import annotations + from collections.abc import Sequence -from typing import Generic +from typing import Any, Generic from typing_extensions import Self +from langgraph._internal._typing import MISSING from langgraph.channels.base import BaseChannel, Value -from langgraph.constants import MISSING from langgraph.errors import EmptyChannelError, InvalidUpdateError +__all__ = ("UntrackedValue",) + class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]): """Stores the last value received, never checkpointed.""" __slots__ = ("value", "guard") + guard: bool + value: Value | Any + def __init__(self, typ: type[Value], guard: bool = True) -> None: super().__init__(typ) self.guard = guard @@ -38,7 +45,7 @@ class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]): empty.value = self.value return empty - def checkpoint(self) -> Value: + def checkpoint(self) -> Value | Any: return MISSING def from_checkpoint(self, checkpoint: Value) -> Self: diff --git a/libs/langgraph/langgraph/config.py b/libs/langgraph/langgraph/config.py index b2ef57cfb..660924e46 100644 --- a/libs/langgraph/langgraph/config.py +++ b/libs/langgraph/langgraph/config.py @@ -5,7 +5,7 @@ from typing import Any from langchain_core.runnables import RunnableConfig from langchain_core.runnables.config import var_child_runnable_config -from langgraph.constants import CONF, CONFIG_KEY_STORE, CONFIG_KEY_STREAM_WRITER +from langgraph._internal._constants import CONF, CONFIG_KEY_RUNTIME from langgraph.store.base import BaseStore from langgraph.types import StreamWriter @@ -114,8 +114,7 @@ def get_store() -> BaseStore: 3 ``` """ - config = get_config() - return config[CONF][CONFIG_KEY_STORE] + return get_config()[CONF][CONFIG_KEY_RUNTIME].store def get_stream_writer() -> StreamWriter: @@ -181,5 +180,5 @@ def get_stream_writer() -> StreamWriter: {'custom_data': 'Hello!'} ``` """ - config = get_config() - return config[CONF].get(CONFIG_KEY_STREAM_WRITER, _no_op_stream_writer) + runtime = get_config()[CONF][CONFIG_KEY_RUNTIME] + return runtime.stream_writer diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index f524de39f..5b7e52aae 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -1,133 +1,64 @@ import sys -from collections.abc import Mapping -from types import MappingProxyType -from typing import Any, Literal, cast +from typing import Any +from warnings import warn -from langgraph.types import Interrupt, Send # noqa: F401 +from langgraph._internal._constants import ( + CONF, + CONFIG_KEY_CHECKPOINTER, + TASKS, +) +from langgraph.warnings import LangGraphDeprecatedSinceV10 -# Interrupt, Send re-exported for backwards compatibility - - -# --- Empty read-only containers --- -EMPTY_MAP: Mapping[str, Any] = MappingProxyType({}) -EMPTY_SEQ: tuple[str, ...] = tuple() -MISSING = object() +__all__ = ( + "TAG_NOSTREAM", + "TAG_HIDDEN", + "START", + "END", + # retained for backwards compatibility (mostly langgraph-api), should be removed in v2 (or earlier) + "CONF", + "TASKS", + "CONFIG_KEY_CHECKPOINTER", +) # --- Public constants --- TAG_NOSTREAM = sys.intern("nostream") """Tag to disable streaming for a chat model.""" -TAG_NOSTREAM_ALT = sys.intern("langsmith:nostream") -"""Tag to disable streaming for a chat model. (Deprecated in favour of "nostream")""" TAG_HIDDEN = sys.intern("langsmith:hidden") """Tag to hide a node/edge from certain tracing/streaming environments.""" -START = sys.intern("__start__") -"""The first (maybe virtual) node in graph-style Pregel.""" END = sys.intern("__end__") """The last (maybe virtual) node in graph-style Pregel.""" -SELF = sys.intern("__self__") -"""The implicit branch that handles each node's Control values.""" -PREVIOUS = sys.intern("__previous__") +START = sys.intern("__start__") +"""The first (maybe virtual) node in graph-style Pregel.""" -# --- Reserved write keys --- -INPUT = sys.intern("__input__") -# for values passed as input to the graph -INTERRUPT = sys.intern("__interrupt__") -# for dynamic interrupts raised by nodes -RESUME = sys.intern("__resume__") -# for values passed to resume a node after an interrupt -ERROR = sys.intern("__error__") -# for errors raised by nodes -NO_WRITES = sys.intern("__no_writes__") -# marker to signal node didn't write anything -TASKS = sys.intern("__pregel_tasks") -# for Send objects returned by nodes/edges, corresponds to PUSH below -RETURN = sys.intern("__return__") -# for writes of a task where we simply record the return value -# --- Reserved cache namespaces --- -CACHE_NS_WRITES = sys.intern("__pregel_ns_writes") -# cache namespace for node writes +def __getattr__(name: str) -> Any: + if name in ["Send", "Interrupt"]: + warn( + f"Importing {name} from langgraph.constants is deprecated. " + f"Please use 'from langgraph.types import {name}' instead.", + LangGraphDeprecatedSinceV10, + stacklevel=2, + ) -# --- Reserved config.configurable keys --- -CONFIG_KEY_SEND = sys.intern("__pregel_send") -# holds the `write` function that accepts writes to state/edges/reserved keys -CONFIG_KEY_READ = sys.intern("__pregel_read") -# holds the `read` function that returns a copy of the current state -CONFIG_KEY_CALL = sys.intern("__pregel_call") -# holds the `call` function that accepts a node/func, args and returns a future -CONFIG_KEY_CHECKPOINTER = sys.intern("__pregel_checkpointer") -# holds a `BaseCheckpointSaver` passed from parent graph to child graphs -CONFIG_KEY_STREAM = sys.intern("__pregel_stream") -# holds a `StreamProtocol` passed from parent graph to child graphs -CONFIG_KEY_STREAM_WRITER = sys.intern("__pregel_stream_writer") -# holds a `StreamWriter` for stream_mode=custom -CONFIG_KEY_STORE = sys.intern("__pregel_store") -# holds a `BaseStore` made available to managed values -CONFIG_KEY_CACHE = sys.intern("__pregel_cache") -# holds a `BaseCache` made available to subgraphs -CONFIG_KEY_RESUMING = sys.intern("__pregel_resuming") -# holds a boolean indicating if subgraphs should resume from a previous checkpoint -CONFIG_KEY_TASK_ID = sys.intern("__pregel_task_id") -# holds the task ID for the current task -CONFIG_KEY_THREAD_ID = sys.intern("thread_id") -# holds the thread ID for the current invocation -CONFIG_KEY_CHECKPOINT_MAP = sys.intern("checkpoint_map") -# holds a mapping of checkpoint_ns -> checkpoint_id for parent graphs -CONFIG_KEY_CHECKPOINT_ID = sys.intern("checkpoint_id") -# holds the current checkpoint_id, if any -CONFIG_KEY_CHECKPOINT_NS = sys.intern("checkpoint_ns") -# holds the current checkpoint_ns, "" for root graph -CONFIG_KEY_NODE_FINISHED = sys.intern("__pregel_node_finished") -# holds a callback to be called when a node is finished -CONFIG_KEY_SCRATCHPAD = sys.intern("__pregel_scratchpad") -# holds a mutable dict for temporary storage scoped to the current task -CONFIG_KEY_PREVIOUS = sys.intern("__pregel_previous") -# holds the previous return value from a stateful Pregel graph. -CONFIG_KEY_RUNNER_SUBMIT = sys.intern("__pregel_runner_submit") -# holds a function that receives tasks from runner, executes them and returns results -CONFIG_KEY_CHECKPOINT_DURING = sys.intern("__pregel_checkpoint_during") -# holds a boolean indicating whether to checkpoint during the run (or only at the end) + from importlib import import_module -# --- Other constants --- -PUSH = sys.intern("__pregel_push") -# denotes push-style tasks, ie. those created by Send objects -PULL = sys.intern("__pregel_pull") -# denotes pull-style tasks, ie. those triggered by edges -NS_SEP = sys.intern("|") -# for checkpoint_ns, separates each level (ie. graph|subgraph|subsubgraph) -NS_END = sys.intern(":") -# for checkpoint_ns, for each level, separates the namespace from the task_id -CONF = cast(Literal["configurable"], sys.intern("configurable")) -# key for the configurable dict in RunnableConfig -NULL_TASK_ID = sys.intern("00000000-0000-0000-0000-000000000000") -# the task_id to use for writes that are not associated with a task -CONFIG_KEY_RESUME_MAP = sys.intern("__pregel_resume_map") -# holds a mapping of task ns -> resume value for resuming tasks + module = import_module("langgraph.types") + return getattr(module, name) -RESERVED = { - TAG_HIDDEN, - # reserved write keys - INPUT, - INTERRUPT, - RESUME, - ERROR, - NO_WRITES, - # reserved config.configurable keys - CONFIG_KEY_SEND, - CONFIG_KEY_READ, - CONFIG_KEY_CHECKPOINTER, - CONFIG_KEY_STREAM, - CONFIG_KEY_STREAM_WRITER, - CONFIG_KEY_STORE, - CONFIG_KEY_RESUMING, - CONFIG_KEY_TASK_ID, - CONFIG_KEY_CHECKPOINT_MAP, - CONFIG_KEY_CHECKPOINT_ID, - CONFIG_KEY_CHECKPOINT_NS, - # other constants - PUSH, - PULL, - NS_SEP, - NS_END, - CONF, -} + try: + from importlib import import_module + + private_constants = import_module("langgraph._internal._constants") + attr = getattr(private_constants, name) + warn( + f"Importing {name} from langgraph.constants is deprecated. " + f"This constant is now private and should not be used directly. " + "Please let the LangGraph team know if you need this value.", + LangGraphDeprecatedSinceV10, + stacklevel=2, + ) + return attr + except AttributeError: + pass + + raise AttributeError(f"module has no attribute '{name}'") diff --git a/libs/langgraph/langgraph/errors.py b/libs/langgraph/langgraph/errors.py index 6213ff68e..841abbaf7 100644 --- a/libs/langgraph/langgraph/errors.py +++ b/libs/langgraph/langgraph/errors.py @@ -1,11 +1,29 @@ +from __future__ import annotations + from collections.abc import Sequence from enum import Enum from typing import Any +from warnings import warn +from typing_extensions import deprecated + +# EmptyChannelError is re-exported from langgraph.channels.base from langgraph.checkpoint.base import EmptyChannelError # noqa: F401 from langgraph.types import Command, Interrupt +from langgraph.warnings import LangGraphDeprecatedSinceV10 -# EmptyChannelError re-exported for backwards compatibility +__all__ = ( + "EmptyChannelError", + "ErrorCode", + "GraphRecursionError", + "InvalidUpdateError", + "GraphBubbleUp", + "GraphInterrupt", + "NodeInterrupt", + "ParentCommand", + "EmptyInputError", + "TaskNotFound", +) class ErrorCode(Enum): @@ -71,11 +89,26 @@ class GraphInterrupt(GraphBubbleUp): super().__init__(interrupts) +@deprecated( + "NodeInterrupt is deprecated. Please use `langgraph.types.interrupt` instead.", + stacklevel=2, +) class NodeInterrupt(GraphInterrupt): - """Raised by a node to interrupt execution.""" + """Raised by a node to interrupt execution. - def __init__(self, value: Any) -> None: - super().__init__([Interrupt(value=value)]) + Deprecated in V1.0.0 in favor of [`interrupt`][langgraph.types.interrupt]. + """ + + def __init__(self, value: Any, id: str | None = None) -> None: + warn( + "NodeInterrupt is deprecated. Please use `langgraph.types.interrupt` instead.", + LangGraphDeprecatedSinceV10, + stacklevel=2, + ) + if id is None: + super().__init__([Interrupt(value=value)]) + else: + super().__init__([Interrupt(value=value, id=id)]) class ParentCommand(GraphBubbleUp): diff --git a/libs/langgraph/langgraph/func/__init__.py b/libs/langgraph/langgraph/func/__init__.py index 71879d571..24ec545f7 100644 --- a/libs/langgraph/langgraph/func/__init__.py +++ b/libs/langgraph/langgraph/func/__init__.py @@ -12,6 +12,7 @@ from typing import ( Callable, Generic, TypeVar, + cast, get_args, get_origin, overload, @@ -19,14 +20,15 @@ from typing import ( from typing_extensions import Unpack -from langgraph._typing import UNSET, DeprecatedKwargs +from langgraph._internal._constants import CACHE_NS_WRITES, PREVIOUS +from langgraph._internal._typing import MISSING, DeprecatedKwargs from langgraph.cache.base import BaseCache from langgraph.channels.ephemeral_value import EphemeralValue from langgraph.channels.last_value import LastValue from langgraph.checkpoint.base import BaseCheckpointSaver -from langgraph.constants import CACHE_NS_WRITES, END, PREVIOUS, START +from langgraph.constants import END, START from langgraph.pregel import Pregel -from langgraph.pregel.call import ( +from langgraph.pregel._call import ( P, SyncAsyncFuture, T, @@ -34,14 +36,17 @@ from langgraph.pregel.call import ( get_runnable_for_entrypoint, identifier, ) -from langgraph.pregel.read import PregelNode -from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry +from langgraph.pregel._read import PregelNode +from langgraph.pregel._write import ChannelWrite, ChannelWriteEntry from langgraph.store.base import BaseStore from langgraph.types import _DC_KWARGS, CachePolicy, RetryPolicy, StreamMode -from langgraph.warnings import LangGraphDeprecatedSinceV05 +from langgraph.typing import ContextT +from langgraph.warnings import LangGraphDeprecatedSinceV05, LangGraphDeprecatedSinceV10 + +__all__ = ("task", "entrypoint") -class TaskFunction(Generic[P, T]): +class _TaskFunction(Generic[P, T]): def __init__( self, func: Callable[P, T], @@ -97,14 +102,14 @@ def task( **kwargs: Unpack[DeprecatedKwargs], ) -> Callable[ [Callable[P, Awaitable[T]] | Callable[P, T]], - TaskFunction[P, T], + _TaskFunction[P, T], ]: ... @overload def task( __func_or_none__: Callable[P, Awaitable[T]] | Callable[P, T], -) -> TaskFunction[P, T]: ... +) -> _TaskFunction[P, T]: ... def task( @@ -115,8 +120,8 @@ def task( cache_policy: CachePolicy[Callable[P, str | bytes]] | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> ( - Callable[[Callable[P, Awaitable[T]] | Callable[P, T]], TaskFunction[P, T]] - | TaskFunction[P, T] + Callable[[Callable[P, Awaitable[T]] | Callable[P, T]], _TaskFunction[P, T]] + | _TaskFunction[P, T] ): """Define a LangGraph task using the `task` decorator. @@ -176,7 +181,7 @@ def task( await add_one.ainvoke([1, 2, 3]) # Returns [2, 3, 4] ``` """ - if (retry := kwargs.get("retry", UNSET)) is not UNSET: + if (retry := kwargs.get("retry", MISSING)) is not MISSING: warnings.warn( "`retry` is deprecated and will be removed. Please use `retry_policy` instead.", category=LangGraphDeprecatedSinceV05, @@ -196,7 +201,7 @@ def task( def decorator( func: Callable[P, Awaitable[T]] | Callable[P, T], ) -> Callable[P, concurrent.futures.Future[T]] | Callable[P, asyncio.Future[T]]: - return TaskFunction( + return _TaskFunction( func, retry_policy=retry_policies, cache_policy=cache_policy, name=name ) @@ -214,7 +219,7 @@ S = TypeVar("S") # In this form, the `final` attribute should play nicely with IDE autocompletion, # and type checking tools. # In addition, we'll be able to surface this information in the API Reference. -class entrypoint: +class entrypoint(Generic[ContextT]): """Define a LangGraph workflow using the `entrypoint` decorator. ### Function signature @@ -230,10 +235,9 @@ class entrypoint: | Parameter | Description | |------------------|----------------------------------------------------------------------------------------------------| - | **`store`** | An instance of [BaseStore][langgraph.store.base.BaseStore]. Useful for long-term memory. | - | **`writer`** | A [StreamWriter][langgraph.types.StreamWriter] instance for writing custom data to a stream. | | **`config`** | A configuration object (aka RunnableConfig) that holds run-time configuration values. | | **`previous`** | The previous return value for the given thread (available only when a checkpointer is provided). | + | **`runtime`** | A Runtime object that contains information about the current run, including context, store, writer | | The entrypoint decorator can be applied to sync functions or async functions. @@ -253,7 +257,7 @@ class entrypoint: store: A generalized key-value store. Some implementations may support semantic search capabilities through an optional `index` configuration. cache: A cache to use for caching the results of the workflow. - config_schema: Specifies the schema for the configuration object that will be + context_schema: Specifies the schema for the context object that will be passed to the workflow. cache_policy: A cache policy to use for caching the results of the workflow. retry_policy: A retry policy (or list of policies) to use for the workflow in case of a failure. @@ -375,27 +379,36 @@ class entrypoint: checkpointer: BaseCheckpointSaver | None = None, store: BaseStore | None = None, cache: BaseCache | None = None, - config_schema: type[Any] | None = None, + context_schema: type[ContextT] | None = None, cache_policy: CachePolicy | None = None, retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> None: """Initialize the entrypoint decorator.""" - if (retry := kwargs.get("retry", UNSET)) is not UNSET: + if (config_schema := kwargs.get("config_schema", MISSING)) is not MISSING: + warnings.warn( + "`config_schema` is deprecated and will be removed. Please use `context_schema` instead.", + category=LangGraphDeprecatedSinceV10, + stacklevel=2, + ) + if context_schema is None: + context_schema = cast(type[ContextT], config_schema) + + if (retry := kwargs.get("retry", MISSING)) is not MISSING: warnings.warn( "`retry` is deprecated and will be removed. Please use `retry_policy` instead.", category=LangGraphDeprecatedSinceV05, stacklevel=2, ) if retry_policy is None: - retry_policy = retry # type: ignore[assignment] + retry_policy = cast("RetryPolicy | Sequence[RetryPolicy]", retry) self.checkpointer = checkpointer self.store = store self.cache = cache self.cache_policy = cache_policy self.retry_policy = retry_policy - self.config_schema = config_schema + self.context_schema = context_schema @dataclass(**_DC_KWARGS) class final(Generic[R, S]): @@ -527,5 +540,5 @@ class entrypoint: cache=self.cache, cache_policy=self.cache_policy, retry_policy=self.retry_policy or (), - config_type=self.config_schema, + context_schema=self.context_schema, # type: ignore[arg-type] ) diff --git a/libs/langgraph/langgraph/func/py.typed b/libs/langgraph/langgraph/func/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/libs/langgraph/langgraph/graph/__init__.py b/libs/langgraph/langgraph/graph/__init__.py index 2581713c3..7bea3fc82 100644 --- a/libs/langgraph/langgraph/graph/__init__.py +++ b/libs/langgraph/langgraph/graph/__init__.py @@ -2,11 +2,11 @@ from langgraph.constants import END, START from langgraph.graph.message import MessageGraph, MessagesState, add_messages from langgraph.graph.state import StateGraph -__all__ = [ +__all__ = ( "END", "START", "StateGraph", - "MessageGraph", "add_messages", "MessagesState", -] + "MessageGraph", +) diff --git a/libs/langgraph/langgraph/graph/branch.py b/libs/langgraph/langgraph/graph/_branch.py similarity index 95% rename from libs/langgraph/langgraph/graph/branch.py rename to libs/langgraph/langgraph/graph/_branch.py index f120167d6..34ff58a61 100644 --- a/libs/langgraph/langgraph/graph/branch.py +++ b/libs/langgraph/langgraph/graph/_branch.py @@ -26,15 +26,15 @@ from langchain_core.runnables import ( RunnableLambda, ) -from langgraph.constants import END, START -from langgraph.errors import InvalidUpdateError -from langgraph.pregel.write import PASSTHROUGH, ChannelWrite, ChannelWriteEntry -from langgraph.types import Send -from langgraph.utils.runnable import ( +from langgraph._internal._runnable import ( RunnableCallable, ) +from langgraph.constants import END, START +from langgraph.errors import InvalidUpdateError +from langgraph.pregel._write import PASSTHROUGH, ChannelWrite, ChannelWriteEntry +from langgraph.types import Send -Writer = Callable[ +_Writer = Callable[ [Sequence[Union[str, Send]], bool], Sequence[Union[ChannelWriteEntry, Send]], ] @@ -82,7 +82,7 @@ def _get_branch_path_input_schema( return input -class Branch(NamedTuple): +class BranchSpec(NamedTuple): path: Runnable[Any, Hashable | list[Hashable]] ends: dict[Hashable, str] | None input_schema: type[Any] | None = None @@ -93,7 +93,7 @@ class Branch(NamedTuple): path: Runnable[Any, Hashable | list[Hashable]], path_map: dict[Hashable, str] | list[str] | None, infer_schema: bool = False, - ) -> Branch: + ) -> BranchSpec: # coerce path_map to a dictionary path_map_: dict[Hashable, str] | None = None try: @@ -123,7 +123,7 @@ class Branch(NamedTuple): def run( self, - writer: Writer, + writer: _Writer, reader: Callable[[RunnableConfig], Any] | None = None, ) -> RunnableCallable: return ChannelWrite.register_writer( @@ -134,7 +134,6 @@ class Branch(NamedTuple): reader=reader, name=None, trace=False, - func_accepts_config=True, ), list( zip_longest( @@ -152,7 +151,7 @@ class Branch(NamedTuple): config: RunnableConfig, *, reader: Callable[[RunnableConfig], Any] | None, - writer: Writer, + writer: _Writer, ) -> Runnable: if reader: value = reader(config) @@ -175,7 +174,7 @@ class Branch(NamedTuple): config: RunnableConfig, *, reader: Callable[[RunnableConfig], Any] | None, - writer: Writer, + writer: _Writer, ) -> Runnable: if reader: value = reader(config) @@ -194,7 +193,7 @@ class Branch(NamedTuple): def _finish( self, - writer: Writer, + writer: _Writer, input: Any, result: Any, config: RunnableConfig, diff --git a/libs/langgraph/langgraph/graph/_node.py b/libs/langgraph/langgraph/graph/_node.py new file mode 100644 index 000000000..a21f14de5 --- /dev/null +++ b/libs/langgraph/langgraph/graph/_node.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import sys +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Any, Generic, Protocol, Union + +from langchain_core.runnables import Runnable, RunnableConfig +from typing_extensions import TypeAlias + +from langgraph._internal._typing import EMPTY_SEQ +from langgraph.runtime import Runtime +from langgraph.store.base import BaseStore +from langgraph.types import CachePolicy, RetryPolicy, StreamWriter +from langgraph.typing import ContextT, NodeInputT, NodeInputT_contra + +_DC_SLOTS = {"slots": True} if sys.version_info >= (3, 10) else {} + + +class _Node(Protocol[NodeInputT_contra]): + def __call__(self, state: NodeInputT_contra) -> Any: ... + + +class _NodeWithConfig(Protocol[NodeInputT_contra]): + def __call__(self, state: NodeInputT_contra, config: RunnableConfig) -> Any: ... + + +class _NodeWithWriter(Protocol[NodeInputT_contra]): + def __call__(self, state: NodeInputT_contra, *, writer: StreamWriter) -> Any: ... + + +class _NodeWithStore(Protocol[NodeInputT_contra]): + def __call__(self, state: NodeInputT_contra, *, store: BaseStore) -> Any: ... + + +class _NodeWithWriterStore(Protocol[NodeInputT_contra]): + def __call__( + self, state: NodeInputT_contra, *, writer: StreamWriter, store: BaseStore + ) -> Any: ... + + +class _NodeWithConfigWriter(Protocol[NodeInputT_contra]): + def __call__( + self, state: NodeInputT_contra, *, config: RunnableConfig, writer: StreamWriter + ) -> Any: ... + + +class _NodeWithConfigStore(Protocol[NodeInputT_contra]): + def __call__( + self, state: NodeInputT_contra, *, config: RunnableConfig, store: BaseStore + ) -> Any: ... + + +class _NodeWithConfigWriterStore(Protocol[NodeInputT_contra]): + def __call__( + self, + state: NodeInputT_contra, + *, + config: RunnableConfig, + writer: StreamWriter, + store: BaseStore, + ) -> Any: ... + + +class _NodeWithRuntime(Protocol[NodeInputT_contra, ContextT]): + def __call__( + self, state: NodeInputT_contra, *, runtime: Runtime[ContextT] + ) -> Any: ... + + +# TODO: we probably don't want to explicitly support the config / store signatures once +# we move to adding a context arg. Maybe what we do is we add support for kwargs with param spec +# this is purely for typing purposes though, so can easily change in the coming weeks. +StateNode: TypeAlias = Union[ + _Node[NodeInputT], + _NodeWithConfig[NodeInputT], + _NodeWithWriter[NodeInputT], + _NodeWithStore[NodeInputT], + _NodeWithWriterStore[NodeInputT], + _NodeWithConfigWriter[NodeInputT], + _NodeWithConfigStore[NodeInputT], + _NodeWithConfigWriterStore[NodeInputT], + _NodeWithRuntime[NodeInputT, ContextT], + Runnable[NodeInputT, Any], +] + + +@dataclass(**_DC_SLOTS) +class StateNodeSpec(Generic[NodeInputT, ContextT]): + runnable: StateNode[NodeInputT, ContextT] + metadata: dict[str, Any] | None + input_schema: type[NodeInputT] + retry_policy: RetryPolicy | Sequence[RetryPolicy] | None + cache_policy: CachePolicy | None + ends: tuple[str, ...] | dict[str, str] | None = EMPTY_SEQ + defer: bool = False diff --git a/libs/langgraph/langgraph/graph/message.py b/libs/langgraph/langgraph/graph/message.py index e50bcfec2..e20c22185 100644 --- a/libs/langgraph/langgraph/graph/message.py +++ b/libs/langgraph/langgraph/graph/message.py @@ -24,9 +24,15 @@ from langchain_core.messages import ( ) from typing_extensions import TypedDict -from langgraph.constants import CONF, CONFIG_KEY_SEND +from langgraph._internal._constants import CONF, CONFIG_KEY_SEND, NS_SEP from langgraph.graph.state import StateGraph +__all__ = ( + "add_messages", + "MessagesState", + "MessageGraph", +) + Messages = Union[list[MessageLikeRepresentation], MessageLikeRepresentation] REMOVE_ALL_MESSAGES = "__remove_all__" @@ -314,8 +320,7 @@ def push_message( ) from langgraph.config import get_config - from langgraph.constants import NS_SEP - from langgraph.pregel.messages import StreamMessagesHandler + from langgraph.pregel._messages import StreamMessagesHandler config = get_config() message = next(x for x in convert_to_messages([message])) diff --git a/libs/langgraph/langgraph/graph/py.typed b/libs/langgraph/langgraph/graph/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 6e5b77b73..9e6d8552e 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -2,6 +2,7 @@ from __future__ import annotations import inspect import logging +import sys import typing import warnings from collections import defaultdict @@ -14,8 +15,6 @@ from typing import ( Callable, Generic, Literal, - NamedTuple, - Protocol, Union, cast, get_args, @@ -26,9 +25,22 @@ from typing import ( from langchain_core.runnables import Runnable, RunnableConfig from pydantic import BaseModel, TypeAdapter -from typing_extensions import Self, TypeAlias, Unpack, is_typeddict +from typing_extensions import Self, Unpack, is_typeddict -from langgraph._typing import UNSET, DeprecatedKwargs +from langgraph._internal._constants import ( + INTERRUPT, + NS_END, + NS_SEP, + TASKS, +) +from langgraph._internal._fields import ( + get_cached_annotated_keys, + get_field_default, + get_update_as_tuples, +) +from langgraph._internal._pydantic import create_model +from langgraph._internal._runnable import coerce_to_runnable +from langgraph._internal._typing import EMPTY_SEQ, MISSING, DeprecatedKwargs from langgraph.cache.base import BaseCache from langgraph.channels.base import BaseChannel from langgraph.channels.binop import BinaryOperatorAggregate @@ -39,31 +51,22 @@ from langgraph.channels.named_barrier_value import ( NamedBarrierValueAfterFinish, ) from langgraph.checkpoint.base import Checkpoint -from langgraph.constants import ( - EMPTY_SEQ, - END, - INTERRUPT, - MISSING, - NS_END, - NS_SEP, - START, - TAG_HIDDEN, - TASKS, -) +from langgraph.constants import END, START, TAG_HIDDEN from langgraph.errors import ( ErrorCode, InvalidUpdateError, ParentCommand, create_error_message, ) -from langgraph.graph.branch import Branch +from langgraph.graph._branch import BranchSpec +from langgraph.graph._node import StateNode, StateNodeSpec from langgraph.managed.base import ( ManagedValueSpec, is_managed_value, ) from langgraph.pregel import Pregel -from langgraph.pregel.read import ChannelRead, PregelNode -from langgraph.pregel.write import ( +from langgraph.pregel._read import ChannelRead, PregelNode +from langgraph.pregel._write import ( ChannelWrite, ChannelWriteEntry, ChannelWriteTupleEntry, @@ -76,20 +79,21 @@ from langgraph.types import ( Command, RetryPolicy, Send, - StreamWriter, ) -from langgraph.typing import InputT, OutputT, StateT, StateT_contra -from langgraph.utils.fields import ( - get_cached_annotated_keys, - get_field_default, - get_update_as_tuples, -) -from langgraph.utils.pydantic import create_model -from langgraph.utils.runnable import coerce_to_runnable -from langgraph.warnings import LangGraphDeprecatedSinceV05 +from langgraph.typing import ContextT, InputT, NodeInputT, OutputT, StateT +from langgraph.warnings import LangGraphDeprecatedSinceV05, LangGraphDeprecatedSinceV10 + +if sys.version_info < (3, 10): + NoneType = type(None) +else: + from types import NoneType as NoneType + +__all__ = ("StateGraph", "CompiledStateGraph") logger = logging.getLogger(__name__) +_CHANNEL_BRANCH_TO = "branch:to:{}" + def _warn_invalid_state_schema(schema: type[Any] | Any) -> None: if isinstance(schema, type): @@ -103,89 +107,14 @@ def _warn_invalid_state_schema(schema: type[Any] | Any) -> None: ) -class _StateNode(Protocol[StateT_contra]): - def __call__(self, state: StateT_contra) -> Any: ... - - -class _NodeWithConfig(Protocol[StateT_contra]): - def __call__(self, state: StateT_contra, config: RunnableConfig) -> Any: ... - - -class _NodeWithWriter(Protocol[StateT_contra]): - def __call__(self, state: StateT_contra, *, writer: StreamWriter) -> Any: ... - - -class _NodeWithStore(Protocol[StateT_contra]): - def __call__(self, state: StateT_contra, *, store: BaseStore) -> Any: ... - - -class _NodeWithWriterStore(Protocol[StateT_contra]): - def __call__( - self, state: StateT_contra, *, writer: StreamWriter, store: BaseStore - ) -> Any: ... - - -class _NodeWithConfigWriter(Protocol[StateT_contra]): - def __call__( - self, state: StateT_contra, *, config: RunnableConfig, writer: StreamWriter - ) -> Any: ... - - -class _NodeWithConfigStore(Protocol[StateT_contra]): - def __call__( - self, state: StateT_contra, *, config: RunnableConfig, store: BaseStore - ) -> Any: ... - - -class _NodeWithConfigWriterStore(Protocol[StateT_contra]): - def __call__( - self, - state: StateT_contra, - *, - config: RunnableConfig, - writer: StreamWriter, - store: BaseStore, - ) -> Any: ... - - -# TODO: we probably don't want to explicitly support the config / store signatures once -# we move to adding a context arg. Maybe what we do is we add support for kwargs with param spec -# this is purely for typing purposes though, so can easily change in the coming weeks. -StateNode: TypeAlias = Union[ - _StateNode[StateT_contra], - _NodeWithConfig[StateT_contra], - _NodeWithWriter[StateT_contra], - _NodeWithStore[StateT_contra], - _NodeWithWriterStore[StateT_contra], - _NodeWithConfigWriter[StateT_contra], - _NodeWithConfigStore[StateT_contra], - _NodeWithConfigWriterStore[StateT_contra], - Runnable[StateT_contra, Any], -] - - -def _get_node_name(node: StateNode) -> str: +def _get_node_name(node: StateNode[Any, ContextT]) -> str: try: return getattr(node, "__name__", node.__class__.__name__) except AttributeError: raise TypeError(f"Unsupported node type: {type(node)}") -class StateNodeSpec(NamedTuple): - # TODO: rename this callable, also move away from NamedTuple so that we can use - # a generic StateNode, so maybe a dataclass - runnable: StateNode - metadata: dict[str, Any] | None - # TODO: rename to input_schema, though we really just want to modify this structure to - # be a dataclass - input: type[Any] - retry_policy: RetryPolicy | Sequence[RetryPolicy] | None - cache_policy: CachePolicy | None - ends: tuple[str, ...] | dict[str, str] | None = EMPTY_SEQ - defer: bool = False - - -class StateGraph(Generic[StateT, InputT, OutputT]): +class StateGraph(Generic[StateT, ContextT, InputT, OutputT]): """A graph whose nodes communicate by reading and writing to a shared state. The signature of each node is State -> Partial. @@ -195,8 +124,10 @@ class StateGraph(Generic[StateT, InputT, OutputT]): Args: state_schema: The schema class that defines the state. - config_schema: The schema class that defines the configuration. - Use this to expose configurable parameters in your API. + context_schema: The schema class that defines the runtime context. + Use this to expose immutable context data to your nodes, like user_id, db_conn, etc. + input_schema: The schema class that defines the input to the graph. + output_schema: The schema class that defines the output from the graph. Example: ```python @@ -204,6 +135,7 @@ class StateGraph(Generic[StateT, InputT, OutputT]): from typing_extensions import Annotated, TypedDict from langgraph.checkpoint.memory import InMemorySaver from langgraph.graph import StateGraph + from langgraph.runtime import Runtime def reducer(a: list, b: int | None) -> list: if b is not None: @@ -213,13 +145,13 @@ class StateGraph(Generic[StateT, InputT, OutputT]): class State(TypedDict): x: Annotated[list, reducer] - class ConfigSchema(TypedDict): + class Context(TypedDict): r: float - graph = StateGraph(State, config_schema=ConfigSchema) + graph = StateGraph(state_schema=State, context_schema=Context) - def node(state: State, config: RunnableConfig) -> dict: - r = config["configurable"].get("r", 1.0) + def node(state: State, runtime: Runtime[Context]) -> dict: + r = runtie.context.get("r", 1.0) x = state["x"][-1] next_value = x * r * (1 - x) return {"x": next_value} @@ -229,17 +161,14 @@ class StateGraph(Generic[StateT, InputT, OutputT]): graph.set_finish_point("A") compiled = graph.compile() - print(compiled.config_specs) - # [ConfigurableFieldSpec(id='r', annotation=, name=None, description=None, default=None, is_shared=False, dependencies=None)] - - step1 = compiled.invoke({"x": 0.5}, {"configurable": {"r": 3.0}}) + step1 = compiled.invoke({"x": 0.5}, context={"r": 3.0}) # {'x': [0.5, 0.75]} ``` """ edges: set[tuple[str, str]] - nodes: dict[str, StateNodeSpec] - branches: defaultdict[str, dict[str, Branch]] + nodes: dict[str, StateNodeSpec[Any, ContextT]] + branches: defaultdict[str, dict[str, BranchSpec]] channels: dict[str, BaseChannel] managed: dict[str, ManagedValueSpec] schemas: dict[type[Any], dict[str, BaseChannel | ManagedValueSpec]] @@ -247,35 +176,45 @@ class StateGraph(Generic[StateT, InputT, OutputT]): compiled: bool state_schema: type[StateT] + context_schema: type[ContextT] | None input_schema: type[InputT] output_schema: type[OutputT] def __init__( self, state_schema: type[StateT], - config_schema: type[Any] | None = None, + context_schema: type[ContextT] | None = None, *, input_schema: type[InputT] | None = None, output_schema: type[OutputT] | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> None: - if (input_ := kwargs.get("input", UNSET)) is not UNSET: + if (config_schema := kwargs.get("config_schema", MISSING)) is not MISSING: + warnings.warn( + "`config_schema` is deprecated and will be removed. Please use `context_schema` instead.", + category=LangGraphDeprecatedSinceV10, + stacklevel=2, + ) + if context_schema is None: + context_schema = cast(type[ContextT], config_schema) + + if (input_ := kwargs.get("input", MISSING)) is not MISSING: warnings.warn( "`input` is deprecated and will be removed. Please use `input_schema` instead.", category=LangGraphDeprecatedSinceV05, stacklevel=2, ) if input_schema is None: - input_schema = cast(Union[type[InputT], None], input_) + input_schema = cast(type[InputT], input_) - if (output := kwargs.get("output", UNSET)) is not UNSET: + if (output := kwargs.get("output", MISSING)) is not MISSING: warnings.warn( "`output` is deprecated and will be removed. Please use `output_schema` instead.", category=LangGraphDeprecatedSinceV05, stacklevel=2, ) if output_schema is None: - output_schema = cast(Union[type[OutputT], None], output) + output_schema = cast(type[OutputT], output) self.nodes = {} self.edges = set() @@ -289,7 +228,7 @@ class StateGraph(Generic[StateT, InputT, OutputT]): self.state_schema = state_schema self.input_schema = cast(type[InputT], input_schema or state_schema) self.output_schema = cast(type[OutputT], output_schema or state_schema) - self.config_schema = config_schema + self.context_schema = context_schema self._add_schema(self.state_schema) self._add_schema(self.input_schema, allow_managed=False) @@ -336,17 +275,35 @@ class StateGraph(Generic[StateT, InputT, OutputT]): @overload def add_node( self, - node: StateNode[StateT], + node: StateNode[NodeInputT, ContextT], *, defer: bool = False, metadata: dict[str, Any] | None = None, - input_schema: type[Any] | None = None, + input_schema: None = None, retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None, cache_policy: CachePolicy | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: - """Add a new node to the state graph. + """Add a new node to the state graph, input schema is inferred as the state schema. + Will take the name of the function/runnable as the node name. + """ + ... + + @overload + def add_node( + self, + node: StateNode[NodeInputT, ContextT], + *, + defer: bool = False, + metadata: dict[str, Any] | None = None, + input_schema: type[NodeInputT], + retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None, + cache_policy: CachePolicy | None = None, + destinations: dict[str, str] | tuple[str, ...] | None = None, + **kwargs: Unpack[DeprecatedKwargs], + ) -> Self: + """Add a new node to the state graph, input schema is specified. Will take the name of the function/runnable as the node name. """ ... @@ -355,27 +312,44 @@ class StateGraph(Generic[StateT, InputT, OutputT]): def add_node( self, node: str, - action: StateNode[StateT], + action: StateNode[NodeInputT, ContextT], *, defer: bool = False, metadata: dict[str, Any] | None = None, - input_schema: type[Any] | None = None, + input_schema: None = None, retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None, cache_policy: CachePolicy | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, **kwargs: Unpack[DeprecatedKwargs], ) -> Self: - """Add a new node to the state graph.""" + """Add a new node to the state graph, input schema is inferred as the state schema.""" + ... + + @overload + def add_node( + self, + node: str | StateNode[NodeInputT, ContextT], + action: StateNode[NodeInputT, ContextT] | None = None, + *, + defer: bool = False, + metadata: dict[str, Any] | None = None, + input_schema: type[NodeInputT], + retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None, + cache_policy: CachePolicy | None = None, + destinations: dict[str, str] | tuple[str, ...] | None = None, + **kwargs: Unpack[DeprecatedKwargs], + ) -> Self: + """Add a new node to the state graph, input schema is specified.""" ... def add_node( self, - node: str | StateNode[StateT], - action: StateNode[StateT] | None = None, + node: str | StateNode[NodeInputT, ContextT], + action: StateNode[NodeInputT, ContextT] | None = None, *, defer: bool = False, metadata: dict[str, Any] | None = None, - input_schema: type[Any] | None = None, + input_schema: type[NodeInputT] | None = None, retry_policy: RetryPolicy | Sequence[RetryPolicy] | None = None, cache_policy: CachePolicy | None = None, destinations: dict[str, str] | tuple[str, ...] | None = None, @@ -434,7 +408,7 @@ class StateGraph(Generic[StateT, InputT, OutputT]): Returns: Self: The instance of the state graph, allowing for method chaining. """ - if (retry := kwargs.get("retry", UNSET)) is not UNSET: + if (retry := kwargs.get("retry", MISSING)) is not MISSING: warnings.warn( "`retry` is deprecated and will be removed. Please use `retry_policy` instead.", category=LangGraphDeprecatedSinceV05, @@ -442,13 +416,13 @@ class StateGraph(Generic[StateT, InputT, OutputT]): if retry_policy is None: retry_policy = retry # type: ignore[assignment] - if (input_ := kwargs.get("input", UNSET)) is not UNSET: + if (input_ := kwargs.get("input", MISSING)) is not MISSING: warnings.warn( "`input` is deprecated and will be removed. Please use `input_schema` instead.", category=LangGraphDeprecatedSinceV05, ) if input_schema is None: - input_schema = cast(Union[type[InputT], None], input_) + input_schema = cast(Union[type[NodeInputT], None], input_) if not isinstance(node, str): action = node @@ -485,6 +459,8 @@ class StateGraph(Generic[StateT, InputT, OutputT]): f"'{character}' is a reserved character and is not allowed in the node names." ) + inferred_input_schema = None + ends: tuple[str, ...] | dict[str, str] = EMPTY_SEQ try: if ( @@ -505,7 +481,7 @@ class StateGraph(Generic[StateT, InputT, OutputT]): ) if input_hint := hints.get(first_parameter_name): if isinstance(input_hint, type) and get_type_hints(input_hint): - input_schema = input_hint + inferred_input_schema = input_hint if rtn := hints.get("return"): # Handle Union types rtn_origin = get_origin(rtn) @@ -533,17 +509,41 @@ class StateGraph(Generic[StateT, InputT, OutputT]): if destinations is not None: ends = destinations + if input_schema is not None: + self.nodes[node] = StateNodeSpec[NodeInputT, ContextT]( + coerce_to_runnable(action, name=node, trace=False), # type: ignore[arg-type] + metadata, + input_schema=input_schema, + retry_policy=retry_policy, + cache_policy=cache_policy, + ends=ends, + defer=defer, + ) + elif inferred_input_schema is not None: + self.nodes[node] = StateNodeSpec( + coerce_to_runnable(action, name=node, trace=False), # type: ignore[arg-type] + metadata, + input_schema=inferred_input_schema, + retry_policy=retry_policy, + cache_policy=cache_policy, + ends=ends, + defer=defer, + ) + else: + self.nodes[node] = StateNodeSpec[StateT, ContextT]( + coerce_to_runnable(action, name=node, trace=False), # type: ignore[arg-type] + metadata, + input_schema=self.state_schema, + retry_policy=retry_policy, + cache_policy=cache_policy, + ends=ends, + defer=defer, + ) + + input_schema = input_schema or inferred_input_schema if input_schema is not None: self._add_schema(input_schema) - self.nodes[node] = StateNodeSpec( - coerce_to_runnable(action, name=node, trace=False), - metadata, - input=input_schema or self.state_schema, - retry_policy=retry_policy, - cache_policy=cache_policy, - ends=ends, - defer=defer, - ) + return self def add_edge(self, start_key: str | list[str], end_key: str) -> Self: @@ -641,14 +641,17 @@ class StateGraph(Generic[StateT, InputT, OutputT]): f"Branch with name `{path.name}` already exists for node `{source}`" ) # save it - self.branches[source][name] = Branch.from_path(path, path_map, True) + self.branches[source][name] = BranchSpec.from_path(path, path_map, True) if schema := self.branches[source][name].input_schema: self._add_schema(schema) return self def add_sequence( self, - nodes: Sequence[StateNode[StateT] | tuple[str, StateNode[StateT]]], + nodes: Sequence[ + StateNode[NodeInputT, ContextT] + | tuple[str, StateNode[NodeInputT, ContextT]] + ], ) -> Self: """Add a sequence of nodes that will be executed in the provided order. @@ -794,7 +797,7 @@ class StateGraph(Generic[StateT, InputT, OutputT]): interrupt_after: All | list[str] | None = None, debug: bool = False, name: str | None = None, - ) -> CompiledStateGraph[StateT, InputT, OutputT]: + ) -> CompiledStateGraph[StateT, ContextT, InputT, OutputT]: """Compiles the state graph into a `CompiledStateGraph` object. The compiled graph implements the `Runnable` interface and can be invoked, @@ -846,10 +849,10 @@ class StateGraph(Generic[StateT, InputT, OutputT]): ] ) - compiled = CompiledStateGraph[StateT, InputT, OutputT]( + compiled = CompiledStateGraph[StateT, ContextT, InputT, OutputT]( builder=self, schema_to_mapper={}, - config_type=self.config_schema, + context_schema=self.context_schema, nodes={}, channels={ **self.channels, @@ -888,15 +891,16 @@ class StateGraph(Generic[StateT, InputT, OutputT]): class CompiledStateGraph( - Pregel[StateT, InputT, OutputT], Generic[StateT, InputT, OutputT] + Pregel[StateT, ContextT, InputT, OutputT], + Generic[StateT, ContextT, InputT, OutputT], ): - builder: StateGraph[StateT, InputT, OutputT] + builder: StateGraph[StateT, ContextT, InputT, OutputT] schema_to_mapper: dict[type[Any], Callable[[Any], Any] | None] def __init__( self, *, - builder: StateGraph[StateT, InputT, OutputT], + builder: StateGraph[StateT, ContextT, InputT, OutputT], schema_to_mapper: dict[type[Any], Callable[[Any], Any] | None], **kwargs: Any, ) -> None: @@ -924,7 +928,7 @@ class CompiledStateGraph( name=self.get_name("Output"), ) - def attach_node(self, key: str, node: StateNodeSpec | None) -> None: + def attach_node(self, key: str, node: StateNodeSpec[Any, ContextT] | None) -> None: if key == START: output_keys = [ k @@ -996,7 +1000,7 @@ class CompiledStateGraph( writers=[ChannelWrite(write_entries)], ) elif node is not None: - input_schema = node.input if node else self.builder.state_schema + input_schema = node.input_schema if node else self.builder.state_schema input_channels = list(self.builder.schemas[input_schema]) is_single_input = len(input_channels) == 1 and "__root__" in input_channels if input_schema in self.schema_to_mapper: @@ -1005,7 +1009,7 @@ class CompiledStateGraph( mapper = _pick_mapper(input_channels, input_schema) self.schema_to_mapper[input_schema] = mapper - branch_channel = CHANNEL_BRANCH_TO.format(key) + branch_channel = _CHANNEL_BRANCH_TO.format(key) self.channels[branch_channel] = ( LastValueAfterFinish(Any) if node.defer @@ -1033,7 +1037,7 @@ class CompiledStateGraph( if end != END: self.nodes[starts].writers.append( ChannelWrite( - (ChannelWriteEntry(CHANNEL_BRANCH_TO.format(end), None),) + (ChannelWriteEntry(_CHANNEL_BRANCH_TO.format(end), None),) ) ) elif end != END: @@ -1054,7 +1058,7 @@ class CompiledStateGraph( ) def attach_branch( - self, start: str, name: str, branch: Branch, *, with_reader: bool = True + self, start: str, name: str, branch: BranchSpec, *, with_reader: bool = True ) -> None: def get_writes( packets: Sequence[str | Send], static: bool = False @@ -1062,7 +1066,7 @@ class CompiledStateGraph( writes = [ ( ChannelWriteEntry( - p if p == END else CHANNEL_BRANCH_TO.format(p), None + p if p == END else _CHANNEL_BRANCH_TO.format(p), None ) if not isinstance(p, Send) else p @@ -1077,7 +1081,7 @@ class CompiledStateGraph( if with_reader: # get schema schema = branch.input_schema or ( - self.builder.nodes[start].input + self.builder.nodes[start].input_schema if start in self.builder.nodes else self.builder.state_schema ) @@ -1247,7 +1251,7 @@ def _control_branch(value: Any) -> Sequence[tuple[str, Any]]: elif isinstance(go, str) and go != END: # END is a special case, it's not actually a node in a practical sense # but rather a special terminal node that we don't need to branch to - rtn.append((CHANNEL_BRANCH_TO.format(go), None)) + rtn.append((_CHANNEL_BRANCH_TO.format(go), None)) return rtn @@ -1256,12 +1260,12 @@ def _control_static( ) -> Sequence[tuple[str, Any, str | None]]: if isinstance(ends, dict): return [ - (k if k == END else CHANNEL_BRANCH_TO.format(k), None, label) + (k if k == END else _CHANNEL_BRANCH_TO.format(k), None, label) for k, label in ends.items() ] else: return [ - (e if e == END else CHANNEL_BRANCH_TO.format(e), None, None) for e in ends + (e if e == END else _CHANNEL_BRANCH_TO.format(e), None, None) for e in ends ] @@ -1420,6 +1424,3 @@ def _get_json_schema( if k in channels and isinstance(channels[k], BaseChannel) }, ).model_json_schema() - - -CHANNEL_BRANCH_TO = "branch:to:{}" diff --git a/libs/langgraph/langgraph/graph/ui.py b/libs/langgraph/langgraph/graph/ui.py index 181587740..f2fe5a1c2 100644 --- a/libs/langgraph/langgraph/graph/ui.py +++ b/libs/langgraph/langgraph/graph/ui.py @@ -6,8 +6,17 @@ from uuid import uuid4 from langchain_core.messages import AnyMessage from typing_extensions import TypedDict -from langgraph.constants import CONF, CONFIG_KEY_SEND -from langgraph.utils.config import get_config, get_stream_writer +from langgraph.config import get_config, get_stream_writer +from langgraph.constants import CONF + +__all__ = ( + "UIMessage", + "RemoveUIMessage", + "AnyUIMessage", + "push_ui_message", + "delete_ui_message", + "ui_message_reducer", +) class UIMessage(TypedDict): @@ -87,6 +96,8 @@ def push_ui_message( ) """ + from langgraph._internal._constants import CONFIG_KEY_SEND + writer = get_stream_writer() config = get_config() @@ -139,6 +150,8 @@ def delete_ui_message(id: str, *, state_key: str = "ui") -> RemoveUIMessage: delete_ui_message("message-123") """ + from langgraph._internal._constants import CONFIG_KEY_SEND + writer = get_stream_writer() config = get_config() diff --git a/libs/langgraph/langgraph/managed/__init__.py b/libs/langgraph/langgraph/managed/__init__.py index 966348e6f..2d50f323b 100644 --- a/libs/langgraph/langgraph/managed/__init__.py +++ b/libs/langgraph/langgraph/managed/__init__.py @@ -1,3 +1,3 @@ from langgraph.managed.is_last_step import IsLastStep, RemainingSteps -__all__ = ["IsLastStep", "RemainingSteps"] +__all__ = ("IsLastStep", "RemainingSteps") diff --git a/libs/langgraph/langgraph/managed/base.py b/libs/langgraph/langgraph/managed/base.py index aa8f507b6..3b5de24d5 100644 --- a/libs/langgraph/langgraph/managed/base.py +++ b/libs/langgraph/langgraph/managed/base.py @@ -8,11 +8,13 @@ from typing import ( from typing_extensions import TypeGuard -from langgraph.types import PregelScratchpad +from langgraph.pregel._scratchpad import PregelScratchpad V = TypeVar("V") U = TypeVar("U") +__all__ = ("ManagedValueSpec", "ManagedValueMapping") + class ManagedValue(ABC, Generic[V]): @staticmethod diff --git a/libs/langgraph/langgraph/managed/is_last_step.py b/libs/langgraph/langgraph/managed/is_last_step.py index ccfaea038..6ffa4df16 100644 --- a/libs/langgraph/langgraph/managed/is_last_step.py +++ b/libs/langgraph/langgraph/managed/is_last_step.py @@ -1,7 +1,9 @@ from typing import Annotated from langgraph.managed.base import ManagedValue -from langgraph.types import PregelScratchpad +from langgraph.pregel._scratchpad import PregelScratchpad + +__all__ = ("IsLastStep", "RemainingStepsManager") class IsLastStepManager(ManagedValue[bool]): diff --git a/libs/langgraph/langgraph/managed/py.typed b/libs/langgraph/langgraph/managed/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 21d4d593b..90eb44b85 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -1,3049 +1,3 @@ -from __future__ import annotations +from langgraph.pregel.main import NodeBuilder, Pregel -import asyncio -import concurrent -import concurrent.futures -import queue -import weakref -from collections import defaultdict, deque -from collections.abc import AsyncIterator, Iterator, Mapping, Sequence -from functools import partial -from typing import Any, Callable, Generic, Union, cast, get_type_hints -from uuid import UUID, uuid5 - -from langchain_core.globals import get_debug -from langchain_core.runnables import ( - RunnableSequence, -) -from langchain_core.runnables.base import Input, Output -from langchain_core.runnables.config import ( - RunnableConfig, - get_async_callback_manager_for_config, - get_callback_manager_for_config, -) -from langchain_core.runnables.graph import Graph -from pydantic import BaseModel -from typing_extensions import Self - -from langgraph.cache.base import BaseCache -from langgraph.channels.base import BaseChannel -from langgraph.channels.topic import Topic -from langgraph.checkpoint.base import ( - BaseCheckpointSaver, - Checkpoint, - CheckpointTuple, -) -from langgraph.config import get_config -from langgraph.constants import ( - CACHE_NS_WRITES, - CONF, - CONFIG_KEY_CACHE, - CONFIG_KEY_CHECKPOINT_DURING, - CONFIG_KEY_CHECKPOINT_ID, - CONFIG_KEY_CHECKPOINT_NS, - CONFIG_KEY_CHECKPOINTER, - CONFIG_KEY_NODE_FINISHED, - CONFIG_KEY_READ, - CONFIG_KEY_RUNNER_SUBMIT, - CONFIG_KEY_SEND, - CONFIG_KEY_STORE, - CONFIG_KEY_STREAM, - CONFIG_KEY_STREAM_WRITER, - CONFIG_KEY_TASK_ID, - CONFIG_KEY_THREAD_ID, - END, - ERROR, - INPUT, - INTERRUPT, - NS_END, - NS_SEP, - NULL_TASK_ID, - PUSH, - TASKS, -) -from langgraph.errors import ( - ErrorCode, - GraphRecursionError, - InvalidUpdateError, - create_error_message, -) -from langgraph.managed.base import ManagedValueSpec -from langgraph.pregel.algo import ( - PregelTaskWrites, - _scratchpad, - apply_writes, - local_read, - prepare_next_tasks, -) -from langgraph.pregel.call import identifier -from langgraph.pregel.checkpoint import ( - channels_from_checkpoint, - copy_checkpoint, - create_checkpoint, - empty_checkpoint, -) -from langgraph.pregel.debug import get_bolded_text, get_colored_text, tasks_w_writes -from langgraph.pregel.draw import draw_graph -from langgraph.pregel.io import map_input, read_channels -from langgraph.pregel.loop import AsyncPregelLoop, StreamProtocol, SyncPregelLoop -from langgraph.pregel.messages import StreamMessagesHandler -from langgraph.pregel.protocol import PregelProtocol -from langgraph.pregel.read import DEFAULT_BOUND, PregelNode -from langgraph.pregel.retry import RetryPolicy -from langgraph.pregel.runner import PregelRunner -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 -from langgraph.types import ( - All, - CachePolicy, - Checkpointer, - Command, - Interrupt, - Send, - StateSnapshot, - StateUpdate, - StreamChunk, - StreamMode, -) -from langgraph.typing import InputT, OutputT, StateT -from langgraph.utils.config import ( - ensure_config, - merge_configs, - patch_checkpoint_map, - patch_config, - patch_configurable, - recast_checkpoint_ns, -) -from langgraph.utils.pydantic import create_model -from langgraph.utils.queue import AsyncQueue, SyncQueue # type: ignore[attr-defined] -from langgraph.utils.runnable import ( - Runnable, - RunnableLike, - RunnableSeq, - coerce_to_runnable, -) - -try: - from langchain_core.tracers._streaming import _StreamingCallbackHandler -except ImportError: - _StreamingCallbackHandler = None # type: ignore - -WriteValue = Union[Callable[[Input], Output], Any] - - -class NodeBuilder: - __slots__ = ( - "_channels", - "_triggers", - "_tags", - "_metadata", - "_writes", - "_bound", - "_retry_policy", - "_cache_policy", - ) - - _channels: str | list[str] - _triggers: list[str] - _tags: list[str] - _metadata: dict[str, Any] - _writes: list[ChannelWriteEntry] - _bound: Runnable - _retry_policy: list[RetryPolicy] - _cache_policy: CachePolicy | None - - def __init__( - self, - ) -> None: - self._channels = [] - self._triggers = [] - self._tags = [] - self._metadata = {} - self._writes = [] - self._bound = DEFAULT_BOUND - self._retry_policy = [] - self._cache_policy = None - - def subscribe_only( - self, - channel: str, - ) -> Self: - """Subscribe to a single channel.""" - if not self._channels: - self._channels = channel - else: - raise ValueError( - "Cannot subscribe to single channels when other channels are already subscribed to" - ) - - self._triggers.append(channel) - - return self - - def subscribe_to( - self, - *channels: str, - read: bool = True, - ) -> Self: - """Add channels to subscribe to. Node will be invoked when any of these - channels are updated, with a dict of the channel values as input. - - Args: - channels: Channel name(s) to subscribe to - read: If True, the channels will be included in the input to the node. - Otherwise, they will trigger the node without being sent in input. - - Returns: - Self for chaining - """ - if isinstance(self._channels, str): - raise ValueError( - "Cannot subscribe to channels when subscribed to a single channel" - ) - if read: - if not self._channels: - self._channels = list(channels) - else: - self._channels.extend(channels) - - if isinstance(channels, str): - self._triggers.append(channels) - else: - self._triggers.extend(channels) - - return self - - def read_from( - self, - *channels: str, - ) -> Self: - """Adds the specified channels to read from, without subscribing to them.""" - assert isinstance(self._channels, list), ( - "Cannot read additional channels when subscribed to single channels" - ) - self._channels.extend(channels) - return self - - def do( - self, - node: RunnableLike, - ) -> Self: - """Adds the specified node.""" - if self._bound is not DEFAULT_BOUND: - self._bound = RunnableSeq( - self._bound, coerce_to_runnable(node, name=None, trace=True) - ) - else: - self._bound = coerce_to_runnable(node, name=None, trace=True) - return self - - def write_to( - self, - *channels: str | ChannelWriteEntry, - **kwargs: WriteValue, - ) -> Self: - """Add channel writes. - - Args: - *channels: Channel names to write to - **kwargs: Channel name and value mappings - - Returns: - Self for chaining - """ - self._writes.extend( - ChannelWriteEntry(c) if isinstance(c, str) else c for c in channels - ) - self._writes.extend( - ChannelWriteEntry(k, mapper=v) - if callable(v) - else ChannelWriteEntry(k, value=v) - for k, v in kwargs.items() - ) - - return self - - def meta(self, *tags: str, **metadata: Any) -> Self: - """Add tags or metadata to the node.""" - self._tags.extend(tags) - self._metadata.update(metadata) - return self - - def add_retry_policies(self, *policies: RetryPolicy) -> Self: - """Adds retry policies to the node.""" - self._retry_policy.extend(policies) - return self - - def add_cache_policy(self, policy: CachePolicy) -> Self: - """Adds cache policies to the node.""" - self._cache_policy = policy - return self - - def build(self) -> PregelNode: - """Builds the node.""" - return PregelNode( - channels=self._channels, - triggers=self._triggers, - tags=self._tags, - metadata=self._metadata, - writers=[ChannelWrite(self._writes)], - bound=self._bound, - retry_policy=self._retry_policy, - cache_policy=self._cache_policy, - ) - - -class Pregel(PregelProtocol[StateT, InputT, OutputT], Generic[StateT, InputT, OutputT]): - """Pregel manages the runtime behavior for LangGraph applications. - - ## Overview - - Pregel combines [**actors**](https://en.wikipedia.org/wiki/Actor_model) - and **channels** into a single application. - **Actors** read data from channels and write data to channels. - Pregel organizes the execution of the application into multiple steps, - following the **Pregel Algorithm**/**Bulk Synchronous Parallel** model. - - Each step consists of three phases: - - - **Plan**: Determine which **actors** to execute in this step. For example, - in the first step, select the **actors** that subscribe to the special - **input** channels; in subsequent steps, - select the **actors** that subscribe to channels updated in the previous step. - - **Execution**: Execute all selected **actors** in parallel, - until all complete, or one fails, or a timeout is reached. During this - phase, channel updates are invisible to actors until the next step. - - **Update**: Update the channels with the values written by the **actors** - in this step. - - Repeat until no **actors** are selected for execution, or a maximum number of - steps is reached. - - ## Actors - - An **actor** is a `PregelNode`. - It subscribes to channels, reads data from them, and writes data to them. - It can be thought of as an **actor** in the Pregel algorithm. - `PregelNodes` implement LangChain's - Runnable interface. - - ## Channels - - Channels are used to communicate between actors (`PregelNodes`). - Each channel has a value type, an update type, and an update function – which - takes a sequence of updates and - modifies the stored value. Channels can be used to send data from one chain to - another, or to send data from a chain to itself in a future step. LangGraph - provides a number of built-in channels: - - ### Basic channels: LastValue and Topic - - - `LastValue`: The default channel, stores the last value sent to the channel, - useful for input and output values, or for sending data from one step to the next - - `Topic`: A configurable PubSub Topic, useful for sending multiple values - between *actors*, or for accumulating output. Can be configured to deduplicate - values, and/or to accumulate values over the course of multiple steps. - - ### Advanced channels: Context and BinaryOperatorAggregate - - - `Context`: exposes the value of a context manager, managing its lifecycle. - Useful for accessing external resources that require setup and/or teardown. eg. - `client = Context(httpx.Client)` - - `BinaryOperatorAggregate`: stores a persistent value, updated by applying - a binary operator to the current value and each update - sent to the channel, useful for computing aggregates over multiple steps. eg. - `total = BinaryOperatorAggregate(int, operator.add)` - - ## Examples - - Most users will interact with Pregel via a - [StateGraph (Graph API)][langgraph.graph.StateGraph] or via an - [entrypoint (Functional API)][langgraph.func.entrypoint]. - - However, for **advanced** use cases, Pregel can be used directly. If you're - not sure whether you need to use Pregel directly, then the answer is probably no - – you should use the Graph API or Functional API instead. These are higher-level - interfaces that will compile down to Pregel under the hood. - - Here are some examples to give you a sense of how it works: - - Example: Single node application - ```python - from langgraph.channels import EphemeralValue - from langgraph.pregel import Pregel, NodeBuilder - - node1 = ( - NodeBuilder().subscribe_only("a") - .do(lambda x: x + x) - .write_to("b") - ) - - app = Pregel( - nodes={"node1": node1}, - channels={ - "a": EphemeralValue(str), - "b": EphemeralValue(str), - }, - input_channels=["a"], - output_channels=["b"], - ) - - app.invoke({"a": "foo"}) - ``` - - ```con - {'b': 'foofoo'} - ``` - - Example: Using multiple nodes and multiple output channels - ```python - from langgraph.channels import LastValue, EphemeralValue - from langgraph.pregel import Pregel, NodeBuilder - - node1 = ( - NodeBuilder().subscribe_only("a") - .do(lambda x: x + x) - .write_to("b") - ) - - node2 = ( - NodeBuilder().subscribe_to("b") - .do(lambda x: x["b"] + x["b"]) - .write_to("c") - ) - - - app = Pregel( - nodes={"node1": node1, "node2": node2}, - channels={ - "a": EphemeralValue(str), - "b": LastValue(str), - "c": EphemeralValue(str), - }, - input_channels=["a"], - output_channels=["b", "c"], - ) - - app.invoke({"a": "foo"}) - ``` - - ```con - {'b': 'foofoo', 'c': 'foofoofoofoo'} - ``` - - Example: Using a Topic channel - ```python - from langgraph.channels import LastValue, EphemeralValue, Topic - from langgraph.pregel import Pregel, NodeBuilder - - node1 = ( - NodeBuilder().subscribe_only("a") - .do(lambda x: x + x) - .write_to("b", "c") - ) - - node2 = ( - NodeBuilder().subscribe_only("b") - .do(lambda x: x + x) - .write_to("c") - ) - - - app = Pregel( - nodes={"node1": node1, "node2": node2}, - channels={ - "a": EphemeralValue(str), - "b": EphemeralValue(str), - "c": Topic(str, accumulate=True), - }, - input_channels=["a"], - output_channels=["c"], - ) - - app.invoke({"a": "foo"}) - ``` - - ```pycon - {'c': ['foofoo', 'foofoofoofoo']} - ``` - - Example: Using a BinaryOperatorAggregate channel - ```python - from langgraph.channels import EphemeralValue, BinaryOperatorAggregate - from langgraph.pregel import Pregel, NodeBuilder - - - node1 = ( - NodeBuilder().subscribe_only("a") - .do(lambda x: x + x) - .write_to("b", "c") - ) - - node2 = ( - NodeBuilder().subscribe_only("b") - .do(lambda x: x + x) - .write_to("c") - ) - - - def reducer(current, update): - if current: - return current + " | " + update - else: - return update - - app = Pregel( - nodes={"node1": node1, "node2": node2}, - channels={ - "a": EphemeralValue(str), - "b": EphemeralValue(str), - "c": BinaryOperatorAggregate(str, operator=reducer), - }, - input_channels=["a"], - output_channels=["c"] - ) - - app.invoke({"a": "foo"}) - ``` - - ```con - {'c': 'foofoo | foofoofoofoo'} - ``` - - Example: Introducing a cycle - This example demonstrates how to introduce a cycle in the graph, by having - a chain write to a channel it subscribes to. Execution will continue - until a None value is written to the channel. - - ```python - from langgraph.channels import EphemeralValue - from langgraph.pregel import Pregel, NodeBuilder, ChannelWriteEntry - - example_node = ( - NodeBuilder().subscribe_only("value") - .do(lambda x: x + x if len(x) < 10 else None) - .write_to(ChannelWriteEntry(channel="value", skip_none=True)) - ) - - app = Pregel( - nodes={"example_node": example_node}, - channels={ - "value": EphemeralValue(str), - }, - input_channels=["value"], - output_channels=["value"] - ) - - app.invoke({"value": "a"}) - ``` - - ```con - {'value': 'aaaaaaaaaaaaaaaa'} - ``` - """ - - nodes: dict[str, PregelNode] - - channels: dict[str, BaseChannel | ManagedValueSpec] - - stream_mode: StreamMode = "values" - """Mode to stream output, defaults to 'values'.""" - - stream_eager: bool = False - """Whether to force emitting stream events eagerly, automatically turned on - for stream_mode "messages" and "custom".""" - - output_channels: str | Sequence[str] - - stream_channels: str | Sequence[str] | None = None - """Channels to stream, defaults to all channels not in reserved channels""" - - interrupt_after_nodes: All | Sequence[str] - - interrupt_before_nodes: All | Sequence[str] - - input_channels: str | Sequence[str] - - step_timeout: float | None = None - """Maximum time to wait for a step to complete, in seconds. Defaults to None.""" - - debug: bool - """Whether to print debug information during execution. Defaults to False.""" - - checkpointer: Checkpointer = None - """Checkpointer used to save and load graph state. Defaults to None.""" - - store: BaseStore | None = None - """Memory store to use for SharedValues. Defaults to None.""" - - cache: BaseCache | None = None - """Cache to use for storing node results. Defaults to None.""" - - retry_policy: Sequence[RetryPolicy] = () - """Retry policies to use when running tasks. Empty set disables retries.""" - - cache_policy: CachePolicy | None = None - """Cache policy to use for all nodes. Can be overridden by individual nodes. - Defaults to None.""" - - config_type: type[Any] | None = None - - config: RunnableConfig | None = None - - name: str = "LangGraph" - - trigger_to_nodes: Mapping[str, Sequence[str]] - - def __init__( - self, - *, - nodes: dict[str, PregelNode | NodeBuilder], - channels: dict[str, BaseChannel | ManagedValueSpec] | None, - auto_validate: bool = True, - stream_mode: StreamMode = "values", - stream_eager: bool = False, - output_channels: str | Sequence[str], - stream_channels: str | Sequence[str] | None = None, - interrupt_after_nodes: All | Sequence[str] = (), - interrupt_before_nodes: All | Sequence[str] = (), - input_channels: str | Sequence[str], - step_timeout: float | None = None, - debug: bool | None = None, - checkpointer: BaseCheckpointSaver | None = None, - store: BaseStore | None = None, - cache: BaseCache | None = None, - retry_policy: RetryPolicy | Sequence[RetryPolicy] = (), - cache_policy: CachePolicy | None = None, - config_type: type[Any] | None = None, - config: RunnableConfig | None = None, - trigger_to_nodes: Mapping[str, Sequence[str]] | None = None, - name: str = "LangGraph", - ) -> None: - self.nodes = { - k: v.build() if isinstance(v, NodeBuilder) else v for k, v in nodes.items() - } - self.channels = channels or {} - if TASKS in self.channels and not isinstance(self.channels[TASKS], Topic): - raise ValueError( - f"Channel '{TASKS}' is reserved and cannot be used in the graph." - ) - else: - self.channels[TASKS] = Topic(Send, accumulate=False) - self.stream_mode = stream_mode - self.stream_eager = stream_eager - 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.cache = cache - self.retry_policy = ( - (retry_policy,) if isinstance(retry_policy, RetryPolicy) else retry_policy - ) - self.cache_policy = cache_policy - self.config_type = config_type - self.config = config - self.trigger_to_nodes = trigger_to_nodes or {} - self.name = name - if auto_validate: - self.validate() - - def get_graph( - self, config: RunnableConfig | None = None, *, xray: int | bool = False - ) -> Graph: - """Return a drawable representation of the computation graph.""" - # gather subgraphs - if xray: - subgraphs = { - k: v.get_graph( - config, - xray=xray if isinstance(xray, bool) or xray <= 0 else xray - 1, - ) - for k, v in self.get_subgraphs() - } - else: - subgraphs = {} - - return draw_graph( - merge_configs(self.config, config), - nodes=self.nodes, - specs=self.channels, - input_channels=self.input_channels, - interrupt_after_nodes=self.interrupt_after_nodes, - interrupt_before_nodes=self.interrupt_before_nodes, - trigger_to_nodes=self.trigger_to_nodes, - checkpointer=self.checkpointer, - subgraphs=subgraphs, - ) - - async def aget_graph( - self, config: RunnableConfig | None = None, *, xray: int | bool = False - ) -> Graph: - """Return a drawable representation of the computation graph.""" - - # gather subgraphs - if xray: - subpregels: dict[str, PregelProtocol] = { - k: v async for k, v in self.aget_subgraphs() - } - subgraphs = { - k: v - for k, v in zip( - subpregels, - await asyncio.gather( - *( - p.aget_graph( - config, - xray=xray - if isinstance(xray, bool) or xray <= 0 - else xray - 1, - ) - for p in subpregels.values() - ) - ), - ) - } - else: - subgraphs = {} - - return draw_graph( - merge_configs(self.config, config), - nodes=self.nodes, - specs=self.channels, - input_channels=self.input_channels, - interrupt_after_nodes=self.interrupt_after_nodes, - interrupt_before_nodes=self.interrupt_before_nodes, - trigger_to_nodes=self.trigger_to_nodes, - checkpointer=self.checkpointer, - subgraphs=subgraphs, - ) - - def _repr_mimebundle_(self, **kwargs: Any) -> dict[str, Any]: - """Mime bundle used by Jupyter to display the graph""" - return { - "text/plain": repr(self), - "image/png": self.get_graph().draw_mermaid_png(), - } - - def copy(self, update: dict[str, Any] | None = None) -> Self: - attrs = {k: v for k, v in self.__dict__.items() if k != "__orig_class__"} - attrs.update(update or {}) - return self.__class__(**attrs) - - def with_config(self, config: RunnableConfig | None = None, **kwargs: Any) -> Self: - """Create a copy of the Pregel object with an updated config.""" - return self.copy( - {"config": merge_configs(self.config, config, cast(RunnableConfig, kwargs))} - ) - - def validate(self) -> Self: - validate_graph( - self.nodes, - {k: v for k, v in self.channels.items() if isinstance(v, BaseChannel)}, - {k: v for k, v in self.channels.items() if not isinstance(v, BaseChannel)}, - self.input_channels, - self.output_channels, - self.stream_channels, - self.interrupt_after_nodes, - self.interrupt_before_nodes, - ) - self.trigger_to_nodes = _trigger_to_nodes(self.nodes) - return self - - def config_schema(self, *, include: Sequence[str] | None = None) -> type[BaseModel]: - include = include or [] - fields = { - **({"configurable": (self.config_type, None)} if self.config_type else {}), - **{ - field_name: (field_type, None) - for field_name, field_type in get_type_hints(RunnableConfig).items() - if field_name in [i for i in include if i != "configurable"] - }, - } - return create_model(self.get_name("Config"), field_definitions=fields) - - def get_config_jsonschema( - self, *, include: Sequence[str] | None = None - ) -> dict[str, Any]: - schema = self.config_schema(include=include) - return schema.model_json_schema() - - @property - def InputType(self) -> Any: - if isinstance(self.input_channels, str): - channel = self.channels[self.input_channels] - if isinstance(channel, BaseChannel): - return channel.UpdateType - - def get_input_schema(self, config: RunnableConfig | None = None) -> type[BaseModel]: - config = merge_configs(self.config, config) - if isinstance(self.input_channels, str): - return super().get_input_schema(config) - else: - return create_model( - self.get_name("Input"), - field_definitions={ - k: (c.UpdateType, None) - for k in self.input_channels or self.channels.keys() - if (c := self.channels[k]) and isinstance(c, BaseChannel) - }, - ) - - def get_input_jsonschema( - self, config: RunnableConfig | None = None - ) -> dict[str, Any]: - schema = self.get_input_schema(config) - return schema.model_json_schema() - - @property - def OutputType(self) -> Any: - if isinstance(self.output_channels, str): - channel = self.channels[self.output_channels] - if isinstance(channel, BaseChannel): - return channel.ValueType - - def get_output_schema( - self, config: RunnableConfig | None = None - ) -> type[BaseModel]: - config = merge_configs(self.config, config) - if isinstance(self.output_channels, str): - return super().get_output_schema(config) - else: - return create_model( - self.get_name("Output"), - field_definitions={ - k: (c.ValueType, None) - for k in self.output_channels - if (c := self.channels[k]) and isinstance(c, BaseChannel) - }, - ) - - def get_output_jsonschema( - self, config: RunnableConfig | None = None - ) -> dict[str, Any]: - schema = self.get_output_schema(config) - return schema.model_json_schema() - - @property - def stream_channels_list(self) -> Sequence[str]: - stream_channels = self.stream_channels_asis - return ( - [stream_channels] if isinstance(stream_channels, str) else stream_channels - ) - - @property - def stream_channels_asis(self) -> str | Sequence[str]: - return self.stream_channels or [ - k for k in self.channels if isinstance(self.channels[k], BaseChannel) - ] - - def get_subgraphs( - self, *, namespace: str | None = None, recurse: bool = False - ) -> Iterator[tuple[str, PregelProtocol]]: - """Get the subgraphs of the graph. - - Args: - namespace: The namespace to filter the subgraphs by. - recurse: Whether to recurse into the subgraphs. - If False, only the immediate subgraphs will be returned. - - Returns: - Iterator[tuple[str, PregelProtocol]]: An iterator of the (namespace, subgraph) pairs. - """ - for name, node in self.nodes.items(): - # filter by prefix - if namespace is not None: - if not namespace.startswith(name): - continue - - # find the subgraph, if any - graph = node.subgraphs[0] if node.subgraphs else None - - # if found, yield recursively - if graph: - if name == namespace: - yield name, graph - return # we found it, stop searching - if namespace is None: - yield name, graph - if recurse and isinstance(graph, Pregel): - if namespace is not None: - namespace = namespace[len(name) + 1 :] - yield from ( - (f"{name}{NS_SEP}{n}", s) - for n, s in graph.get_subgraphs( - namespace=namespace, recurse=recurse - ) - ) - - async def aget_subgraphs( - self, *, namespace: str | None = None, recurse: bool = False - ) -> AsyncIterator[tuple[str, PregelProtocol]]: - """Get the subgraphs of the graph. - - Args: - namespace: The namespace to filter the subgraphs by. - recurse: Whether to recurse into the subgraphs. - If False, only the immediate subgraphs will be returned. - - Returns: - AsyncIterator[tuple[str, PregelProtocol]]: An iterator of the (namespace, subgraph) pairs. - """ - for name, node in self.get_subgraphs(namespace=namespace, recurse=recurse): - yield name, node - - def _migrate_checkpoint(self, checkpoint: Checkpoint) -> None: - """Migrate a saved checkpoint to new channel layout.""" - if checkpoint["v"] < 4 and checkpoint.get("pending_sends"): - pending_sends: list[Send] = checkpoint.pop("pending_sends") - checkpoint["channel_values"][TASKS] = pending_sends - checkpoint["channel_versions"][TASKS] = max( - checkpoint["channel_versions"].values() - ) - - def _prepare_state_snapshot( - self, - config: RunnableConfig, - saved: CheckpointTuple | None, - recurse: BaseCheckpointSaver | None = None, - apply_pending_writes: bool = False, - ) -> StateSnapshot: - if not saved: - return StateSnapshot( - values={}, - next=(), - config=config, - metadata=None, - created_at=None, - parent_config=None, - tasks=(), - interrupts=(), - ) - - # migrate checkpoint if needed - self._migrate_checkpoint(saved.checkpoint) - - step = saved.metadata.get("step", -1) + 1 - stop = step + 2 - channels, managed = channels_from_checkpoint( - self.channels, - saved.checkpoint, - ) - # tasks for this checkpoint - next_tasks = prepare_next_tasks( - saved.checkpoint, - saved.pending_writes or [], - self.nodes, - channels, - managed, - saved.config, - step, - stop, - for_execution=True, - store=self.store, - checkpointer=( - self.checkpointer - if isinstance(self.checkpointer, BaseCheckpointSaver) - else None - ), - manager=None, - ) - # get the subgraphs - subgraphs = dict(self.get_subgraphs()) - parent_ns = saved.config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") - task_states: dict[str, RunnableConfig | StateSnapshot] = {} - for task in next_tasks.values(): - 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 = { - CONF: { - "thread_id": saved.config[CONF]["thread_id"], - CONFIG_KEY_CHECKPOINT_NS: task_ns, - } - } - task_states[task.id] = config - else: - # get the state of the subgraph - config = { - CONF: { - CONFIG_KEY_CHECKPOINTER: recurse, - "thread_id": saved.config[CONF]["thread_id"], - CONFIG_KEY_CHECKPOINT_NS: task_ns, - } - } - task_states[task.id] = subgraphs[task.name].get_state( - config, subgraphs=True - ) - # apply pending writes - if null_writes := [ - w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID - ]: - apply_writes( - saved.checkpoint, - channels, - [PregelTaskWrites((), INPUT, null_writes, [])], - None, - self.trigger_to_nodes, - ) - if apply_pending_writes and saved.pending_writes: - for tid, k, v in saved.pending_writes: - if k in (ERROR, INTERRUPT): - continue - if tid not in next_tasks: - continue - next_tasks[tid].writes.append((k, v)) - if tasks := [t for t in next_tasks.values() if t.writes]: - apply_writes( - saved.checkpoint, channels, tasks, None, self.trigger_to_nodes - ) - tasks_with_writes = tasks_w_writes( - next_tasks.values(), - saved.pending_writes, - task_states, - self.stream_channels_asis, - ) - # assemble the state snapshot - return StateSnapshot( - read_channels(channels, self.stream_channels_asis), - tuple(t.name for t in next_tasks.values() if not t.writes), - patch_checkpoint_map(saved.config, saved.metadata), - saved.metadata, - saved.checkpoint["ts"], - patch_checkpoint_map(saved.parent_config, saved.metadata), - tasks_with_writes, - tuple([i for task in tasks_with_writes for i in task.interrupts]), - ) - - async def _aprepare_state_snapshot( - self, - config: RunnableConfig, - saved: CheckpointTuple | None, - recurse: BaseCheckpointSaver | None = None, - apply_pending_writes: bool = False, - ) -> StateSnapshot: - if not saved: - return StateSnapshot( - values={}, - next=(), - config=config, - metadata=None, - created_at=None, - parent_config=None, - tasks=(), - interrupts=(), - ) - - # migrate checkpoint if needed - self._migrate_checkpoint(saved.checkpoint) - - step = saved.metadata.get("step", -1) + 1 - stop = step + 2 - channels, managed = channels_from_checkpoint( - self.channels, - saved.checkpoint, - ) - # tasks for this checkpoint - next_tasks = prepare_next_tasks( - saved.checkpoint, - saved.pending_writes or [], - self.nodes, - channels, - managed, - saved.config, - step, - stop, - for_execution=True, - store=self.store, - checkpointer=( - self.checkpointer - if isinstance(self.checkpointer, BaseCheckpointSaver) - else None - ), - manager=None, - ) - # get the subgraphs - subgraphs = {n: g async for n, g in self.aget_subgraphs()} - parent_ns = saved.config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") - task_states: dict[str, RunnableConfig | StateSnapshot] = {} - for task in next_tasks.values(): - 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 = { - CONF: { - "thread_id": saved.config[CONF]["thread_id"], - CONFIG_KEY_CHECKPOINT_NS: task_ns, - } - } - task_states[task.id] = config - else: - # get the state of the subgraph - config = { - CONF: { - CONFIG_KEY_CHECKPOINTER: recurse, - "thread_id": saved.config[CONF]["thread_id"], - CONFIG_KEY_CHECKPOINT_NS: task_ns, - } - } - task_states[task.id] = await subgraphs[task.name].aget_state( - config, subgraphs=True - ) - # apply pending writes - if null_writes := [ - w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID - ]: - apply_writes( - saved.checkpoint, - channels, - [PregelTaskWrites((), INPUT, null_writes, [])], - None, - self.trigger_to_nodes, - ) - if apply_pending_writes and saved.pending_writes: - for tid, k, v in saved.pending_writes: - if k in (ERROR, INTERRUPT): - continue - if tid not in next_tasks: - continue - next_tasks[tid].writes.append((k, v)) - if tasks := [t for t in next_tasks.values() if t.writes]: - apply_writes( - saved.checkpoint, channels, tasks, None, self.trigger_to_nodes - ) - - tasks_with_writes = tasks_w_writes( - next_tasks.values(), - saved.pending_writes, - task_states, - self.stream_channels_asis, - ) - # assemble the state snapshot - return StateSnapshot( - read_channels(channels, self.stream_channels_asis), - tuple(t.name for t in next_tasks.values() if not t.writes), - patch_checkpoint_map(saved.config, saved.metadata), - saved.metadata, - saved.checkpoint["ts"], - patch_checkpoint_map(saved.parent_config, saved.metadata), - tasks_with_writes, - tuple([i for task in tasks_with_writes for i in task.interrupts]), - ) - - def get_state( - self, config: RunnableConfig, *, subgraphs: bool = False - ) -> StateSnapshot: - """Get the current state of the graph.""" - checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( - CONFIG_KEY_CHECKPOINTER, self.checkpointer - ) - if not checkpointer: - raise ValueError("No checkpointer set") - - if ( - checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") - ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: - # remove task_ids from checkpoint_ns - recast = recast_checkpoint_ns(checkpoint_ns) - # find the subgraph with the matching name - for _, pregel in self.get_subgraphs(namespace=recast, recurse=True): - return pregel.get_state( - patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), - subgraphs=subgraphs, - ) - else: - raise ValueError(f"Subgraph {recast} not found") - - config = merge_configs(self.config, config) if self.config else config - if self.checkpointer is True: - ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS]) - config = merge_configs( - config, {CONF: {CONFIG_KEY_CHECKPOINT_NS: recast_checkpoint_ns(ns)}} - ) - thread_id = config[CONF][CONFIG_KEY_THREAD_ID] - if not isinstance(thread_id, str): - config[CONF][CONFIG_KEY_THREAD_ID] = str(thread_id) - - saved = checkpointer.get_tuple(config) - return self._prepare_state_snapshot( - config, - saved, - recurse=checkpointer if subgraphs else None, - apply_pending_writes=CONFIG_KEY_CHECKPOINT_ID not in config[CONF], - ) - - async def aget_state( - self, config: RunnableConfig, *, subgraphs: bool = False - ) -> StateSnapshot: - """Get the current state of the graph.""" - checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( - CONFIG_KEY_CHECKPOINTER, self.checkpointer - ) - if not checkpointer: - raise ValueError("No checkpointer set") - - if ( - checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") - ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: - # remove task_ids from checkpoint_ns - recast = recast_checkpoint_ns(checkpoint_ns) - # find the subgraph with the matching name - async for _, pregel in self.aget_subgraphs(namespace=recast, recurse=True): - return await pregel.aget_state( - patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), - subgraphs=subgraphs, - ) - else: - raise ValueError(f"Subgraph {recast} not found") - - config = merge_configs(self.config, config) if self.config else config - if self.checkpointer is True: - ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS]) - config = merge_configs( - config, {CONF: {CONFIG_KEY_CHECKPOINT_NS: recast_checkpoint_ns(ns)}} - ) - thread_id = config[CONF][CONFIG_KEY_THREAD_ID] - if not isinstance(thread_id, str): - config[CONF][CONFIG_KEY_THREAD_ID] = str(thread_id) - - saved = await checkpointer.aget_tuple(config) - return await self._aprepare_state_snapshot( - config, - saved, - recurse=checkpointer if subgraphs else None, - apply_pending_writes=CONFIG_KEY_CHECKPOINT_ID not in config[CONF], - ) - - def get_state_history( - self, - config: RunnableConfig, - *, - filter: dict[str, Any] | None = None, - before: RunnableConfig | None = None, - limit: int | None = None, - ) -> Iterator[StateSnapshot]: - """Get the history of the state of the graph.""" - config = ensure_config(config) - checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( - CONFIG_KEY_CHECKPOINTER, self.checkpointer - ) - if not checkpointer: - raise ValueError("No checkpointer set") - - if ( - checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") - ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: - # remove task_ids from checkpoint_ns - recast = recast_checkpoint_ns(checkpoint_ns) - # find the subgraph with the matching name - for _, pregel in self.get_subgraphs(namespace=recast, recurse=True): - yield from pregel.get_state_history( - patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), - filter=filter, - before=before, - limit=limit, - ) - return - else: - raise ValueError(f"Subgraph {recast} not found") - - config = merge_configs( - self.config, - config, - { - CONF: { - CONFIG_KEY_CHECKPOINT_NS: checkpoint_ns, - CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID]), - } - }, - ) - # eagerly consume list() to avoid holding up the db cursor - for checkpoint_tuple in list( - checkpointer.list(config, before=before, limit=limit, filter=filter) - ): - yield self._prepare_state_snapshot( - checkpoint_tuple.config, checkpoint_tuple - ) - - async def aget_state_history( - self, - config: RunnableConfig, - *, - filter: dict[str, Any] | None = None, - before: RunnableConfig | None = None, - limit: int | None = None, - ) -> AsyncIterator[StateSnapshot]: - """Asynchronously get the history of the state of the graph.""" - config = ensure_config(config) - checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( - CONFIG_KEY_CHECKPOINTER, self.checkpointer - ) - if not checkpointer: - raise ValueError("No checkpointer set") - - if ( - checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") - ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: - # remove task_ids from checkpoint_ns - recast = recast_checkpoint_ns(checkpoint_ns) - # find the subgraph with the matching name - async for _, pregel in self.aget_subgraphs(namespace=recast, recurse=True): - async for state in pregel.aget_state_history( - patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), - filter=filter, - before=before, - limit=limit, - ): - yield state - return - else: - raise ValueError(f"Subgraph {recast} not found") - - config = merge_configs( - self.config, - config, - { - CONF: { - CONFIG_KEY_CHECKPOINT_NS: checkpoint_ns, - CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID]), - } - }, - ) - # 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 - ) - - def bulk_update_state( - self, - config: RunnableConfig, - supersteps: Sequence[Sequence[StateUpdate]], - ) -> RunnableConfig: - """Apply updates to the graph state in bulk. Requires a checkpointer to be set. - - Args: - config: The config to apply the updates to. - supersteps: A list of supersteps, each including a list of updates to apply sequentially to a graph state. - Each update is a tuple of the form `(values, as_node, task_id)` where task_id is optional. - - Raises: - ValueError: If no checkpointer is set or no updates are provided. - InvalidUpdateError: If an invalid update is provided. - - Returns: - RunnableConfig: The updated config. - """ - - checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( - CONFIG_KEY_CHECKPOINTER, self.checkpointer - ) - if not checkpointer: - raise ValueError("No checkpointer set") - - if len(supersteps) == 0: - raise ValueError("No supersteps provided") - - if any(len(u) == 0 for u in supersteps): - raise ValueError("No updates provided") - - # delegate to subgraph - if ( - checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") - ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: - # remove task_ids from checkpoint_ns - recast = recast_checkpoint_ns(checkpoint_ns) - # find the subgraph with the matching name - for _, pregel in self.get_subgraphs(namespace=recast, recurse=True): - return pregel.bulk_update_state( - patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), - supersteps, - ) - else: - raise ValueError(f"Subgraph {recast} not found") - - def perform_superstep( - input_config: RunnableConfig, updates: Sequence[StateUpdate] - ) -> RunnableConfig: - # get last checkpoint - config = ensure_config(self.config, input_config) - saved = checkpointer.get_tuple(config) - if saved is not None: - self._migrate_checkpoint(saved.checkpoint) - checkpoint = ( - copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() - ) - checkpoint_previous_versions = ( - 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 - checkpoint_config = patch_configurable( - config, - { - CONFIG_KEY_CHECKPOINT_NS: config[CONF].get( - CONFIG_KEY_CHECKPOINT_NS, "" - ) - }, - ) - if saved: - checkpoint_config = patch_configurable(config, saved.config[CONF]) - channels, managed = channels_from_checkpoint( - self.channels, - checkpoint, - ) - values, as_node = updates[0][:2] - - # no values as END, just clear all tasks - if values is None and as_node == END: - if len(updates) > 1: - raise InvalidUpdateError( - "Cannot apply multiple updates when clearing state" - ) - - if saved is not None: - # tasks for this checkpoint - next_tasks = prepare_next_tasks( - checkpoint, - saved.pending_writes or [], - self.nodes, - channels, - managed, - saved.config, - step + 1, - step + 3, - for_execution=True, - store=self.store, - checkpointer=checkpointer, - manager=None, - ) - # apply null writes - if null_writes := [ - w[1:] - for w in saved.pending_writes or [] - if w[0] == NULL_TASK_ID - ]: - apply_writes( - checkpoint, - channels, - [PregelTaskWrites((), INPUT, null_writes, [])], - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - # apply writes from tasks that already ran - for tid, k, v in saved.pending_writes or []: - if k in (ERROR, INTERRUPT): - continue - if tid not in next_tasks: - continue - next_tasks[tid].writes.append((k, v)) - # clear all current tasks - apply_writes( - checkpoint, - channels, - next_tasks.values(), - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - # save checkpoint - next_config = checkpointer.put( - checkpoint_config, - create_checkpoint(checkpoint, channels, step), - { - "source": "update", - "step": step + 1, - "parents": saved.metadata.get("parents", {}) if saved else {}, - }, - get_new_channel_versions( - checkpoint_previous_versions, - checkpoint["channel_versions"], - ), - ) - return patch_checkpoint_map( - next_config, saved.metadata if saved else None - ) - - # act as an input - if as_node == INPUT: - if len(updates) > 1: - raise InvalidUpdateError( - "Cannot apply multiple updates when updating as input" - ) - - if input_writes := deque(map_input(self.input_channels, values)): - apply_writes( - checkpoint, - channels, - [PregelTaskWrites((), INPUT, input_writes, [])], - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - - # apply input write to channels - next_step = ( - step + 1 - if saved and saved.metadata.get("step") is not None - else -1 - ) - next_config = checkpointer.put( - checkpoint_config, - create_checkpoint(checkpoint, channels, next_step), - { - "source": "input", - "step": next_step, - "parents": saved.metadata.get("parents", {}) - if saved - else {}, - }, - get_new_channel_versions( - checkpoint_previous_versions, - checkpoint["channel_versions"], - ), - ) - - # store the writes - checkpointer.put_writes( - next_config, - input_writes, - str(uuid5(UUID(checkpoint["id"]), INPUT)), - ) - - return patch_checkpoint_map( - next_config, saved.metadata if saved else None - ) - else: - raise InvalidUpdateError( - f"Received no input writes for {self.input_channels}" - ) - - # copy checkpoint - if as_node == "__copy__": - if len(updates) > 1: - raise InvalidUpdateError( - "Cannot copy checkpoint with multiple updates" - ) - - if saved is None: - raise InvalidUpdateError("Cannot copy a non-existent checkpoint") - - next_checkpoint = create_checkpoint(checkpoint, None, step) - - # copy checkpoint - next_config = checkpointer.put( - saved.parent_config - or patch_configurable( - saved.config, {CONFIG_KEY_CHECKPOINT_ID: None} - ), - next_checkpoint, - { - "source": "fork", - "step": step + 1, - "parents": saved.metadata.get("parents", {}), - }, - {}, - ) - - # we want to both clone a checkpoint and update state in one go. - # reuse the same task ID if possible. - if isinstance(values, list) and len(values) > 0: - # figure out the task IDs for the next update checkpoint - next_tasks = prepare_next_tasks( - next_checkpoint, - saved.pending_writes or [], - self.nodes, - channels, - managed, - next_config, - step + 2, - step + 4, - for_execution=True, - store=self.store, - checkpointer=checkpointer, - manager=None, - ) - - tasks_group_by = defaultdict(list) - user_group_by: dict[str, list[StateUpdate]] = defaultdict(list) - - for task in next_tasks.values(): - tasks_group_by[task.name].append(task.id) - - for item in values: - if not isinstance(item, Sequence): - raise InvalidUpdateError( - f"Invalid update item: {item} when copying checkpoint" - ) - - values, as_node = item[:2] - - user_group = user_group_by[as_node] - tasks_group = tasks_group_by[as_node] - - target_idx = len(user_group) - task_id = ( - tasks_group[target_idx] - if target_idx < len(tasks_group) - else None - ) - - user_group_by[as_node].append( - StateUpdate(values=values, as_node=as_node, task_id=task_id) - ) - - return perform_superstep( - patch_checkpoint_map(next_config, saved.metadata), - [item for lst in user_group_by.values() for item in lst], - ) - - return patch_checkpoint_map(next_config, saved.metadata) - - # apply pending writes, if not on specific checkpoint - if ( - CONFIG_KEY_CHECKPOINT_ID not in config[CONF] - and saved is not None - and saved.pending_writes - ): - # tasks for this checkpoint - next_tasks = prepare_next_tasks( - checkpoint, - saved.pending_writes, - self.nodes, - channels, - managed, - saved.config, - step + 1, - step + 3, - for_execution=True, - store=self.store, - checkpointer=checkpointer, - manager=None, - ) - # apply null writes - if null_writes := [ - w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID - ]: - apply_writes( - checkpoint, - channels, - [PregelTaskWrites((), INPUT, null_writes, [])], - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - # apply writes - for tid, k, v in saved.pending_writes: - if k in (ERROR, INTERRUPT): - continue - if tid not in next_tasks: - continue - next_tasks[tid].writes.append((k, v)) - if tasks := [t for t in next_tasks.values() if t.writes]: - apply_writes( - checkpoint, - channels, - tasks, - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - valid_updates: list[tuple[str, dict[str, Any] | None, str | None]] = [] - if len(updates) == 1: - values, as_node, task_id = updates[0] - # find last node that updated the state, if not provided - if as_node is None and len(self.nodes) == 1: - as_node = tuple(self.nodes)[0] - elif as_node is None and not any( - v - for vv in checkpoint["versions_seen"].values() - for v in vv.values() - ): - if ( - isinstance(self.input_channels, str) - and self.input_channels in self.nodes - ): - as_node = self.input_channels - elif as_node is None: - last_seen_by_node = sorted( - (v, n) - for n, seen in checkpoint["versions_seen"].items() - if n in self.nodes - for v in seen.values() - ) - # if two nodes updated the state at the same time, it's ambiguous - if last_seen_by_node: - if len(last_seen_by_node) == 1: - as_node = last_seen_by_node[0][1] - elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]: - as_node = last_seen_by_node[-1][1] - if as_node is None: - raise InvalidUpdateError("Ambiguous update, specify as_node") - if as_node not in self.nodes: - raise InvalidUpdateError(f"Node {as_node} does not exist") - valid_updates.append((as_node, values, task_id)) - else: - for values, as_node, task_id in updates: - if as_node is None: - raise InvalidUpdateError( - "as_node is required when applying multiple updates" - ) - if as_node not in self.nodes: - raise InvalidUpdateError(f"Node {as_node} does not exist") - - valid_updates.append((as_node, values, task_id)) - - run_tasks: list[PregelTaskWrites] = [] - run_task_ids: list[str] = [] - - for as_node, values, provided_task_id in valid_updates: - # create task to run all writers of the chosen node - writers = self.nodes[as_node].flat_writers - if not writers: - raise InvalidUpdateError(f"Node {as_node} has no writers") - writes: deque[tuple[str, Any]] = deque() - task = PregelTaskWrites((), as_node, writes, [INTERRUPT]) - task_id = provided_task_id or str( - uuid5(UUID(checkpoint["id"]), INTERRUPT) - ) - run_tasks.append(task) - run_task_ids.append(task_id) - run = RunnableSequence(*writers) if len(writers) > 1 else writers[0] - # execute task - run.invoke( - values, - patch_config( - config, - run_name=self.name + "UpdateState", - configurable={ - # deque.extend is thread-safe - CONFIG_KEY_SEND: writes.extend, - CONFIG_KEY_TASK_ID: task_id, - CONFIG_KEY_READ: partial( - local_read, - _scratchpad( - None, - [], - task_id, - "", - None, - step, - step + 2, - ), - channels, - managed, - task, - ), - }, - ), - ) - # save task writes - for task_id, task in zip(run_task_ids, run_tasks): - # channel writes are saved to current checkpoint - channel_writes = [w for w in task.writes if w[0] != PUSH] - if saved and channel_writes: - checkpointer.put_writes(checkpoint_config, channel_writes, task_id) - # apply to checkpoint and save - apply_writes( - checkpoint, - channels, - run_tasks, - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - checkpoint = create_checkpoint(checkpoint, channels, step + 1) - next_config = checkpointer.put( - checkpoint_config, - checkpoint, - { - "source": "update", - "step": step + 1, - "parents": saved.metadata.get("parents", {}) if saved else {}, - }, - get_new_channel_versions( - checkpoint_previous_versions, checkpoint["channel_versions"] - ), - ) - for task_id, task in zip(run_task_ids, run_tasks): - # save push writes - if push_writes := [w for w in task.writes if w[0] == PUSH]: - checkpointer.put_writes(next_config, push_writes, task_id) - - return patch_checkpoint_map(next_config, saved.metadata if saved else None) - - current_config = patch_configurable( - config, {CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID])} - ) - for superstep in supersteps: - current_config = perform_superstep(current_config, superstep) - return current_config - - async def abulk_update_state( - self, - config: RunnableConfig, - supersteps: Sequence[Sequence[StateUpdate]], - ) -> RunnableConfig: - """Asynchronously apply updates to the graph state in bulk. Requires a checkpointer to be set. - - Args: - config: The config to apply the updates to. - supersteps: A list of supersteps, each including a list of updates to apply sequentially to a graph state. - Each update is a tuple of the form `(values, as_node, task_id)` where task_id is optional. - - Raises: - ValueError: If no checkpointer is set or no updates are provided. - InvalidUpdateError: If an invalid update is provided. - - Returns: - RunnableConfig: The updated config. - """ - - checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( - CONFIG_KEY_CHECKPOINTER, self.checkpointer - ) - if not checkpointer: - raise ValueError("No checkpointer set") - - if len(supersteps) == 0: - raise ValueError("No supersteps provided") - - if any(len(u) == 0 for u in supersteps): - raise ValueError("No updates provided") - - # delegate to subgraph - if ( - checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") - ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: - # remove task_ids from checkpoint_ns - recast = recast_checkpoint_ns(checkpoint_ns) - # find the subgraph with the matching name - async for _, pregel in self.aget_subgraphs(namespace=recast, recurse=True): - return await pregel.abulk_update_state( - patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), - supersteps, - ) - else: - raise ValueError(f"Subgraph {recast} not found") - - async def aperform_superstep( - input_config: RunnableConfig, updates: Sequence[StateUpdate] - ) -> RunnableConfig: - # get last checkpoint - config = ensure_config(self.config, input_config) - saved = await checkpointer.aget_tuple(config) - if saved is not None: - self._migrate_checkpoint(saved.checkpoint) - checkpoint = ( - copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() - ) - checkpoint_previous_versions = ( - 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 - checkpoint_config = patch_configurable( - config, - { - CONFIG_KEY_CHECKPOINT_NS: config[CONF].get( - CONFIG_KEY_CHECKPOINT_NS, "" - ) - }, - ) - if saved: - checkpoint_config = patch_configurable(config, saved.config[CONF]) - channels, managed = channels_from_checkpoint( - self.channels, - checkpoint, - ) - values, as_node = updates[0][:2] - # no values, just clear all tasks - if values is None and as_node == END: - if len(updates) > 1: - raise InvalidUpdateError( - "Cannot apply multiple updates when clearing state" - ) - if saved is not None: - # tasks for this checkpoint - next_tasks = prepare_next_tasks( - checkpoint, - saved.pending_writes or [], - self.nodes, - channels, - managed, - saved.config, - step + 1, - step + 3, - for_execution=True, - store=self.store, - checkpointer=checkpointer, - manager=None, - ) - # apply null writes - if null_writes := [ - w[1:] - for w in saved.pending_writes or [] - if w[0] == NULL_TASK_ID - ]: - apply_writes( - checkpoint, - channels, - [PregelTaskWrites((), INPUT, null_writes, [])], - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - # apply writes from tasks that already ran - for tid, k, v in saved.pending_writes or []: - if k in (ERROR, INTERRUPT): - continue - if tid not in next_tasks: - continue - next_tasks[tid].writes.append((k, v)) - # clear all current tasks - apply_writes( - checkpoint, - channels, - next_tasks.values(), - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - # save checkpoint - next_config = await checkpointer.aput( - checkpoint_config, - create_checkpoint(checkpoint, channels, step), - { - "source": "update", - "step": step + 1, - "parents": saved.metadata.get("parents", {}) if saved else {}, - }, - get_new_channel_versions( - checkpoint_previous_versions, checkpoint["channel_versions"] - ), - ) - return patch_checkpoint_map( - next_config, saved.metadata if saved else None - ) - - # act as an input - if as_node == INPUT: - if len(updates) > 1: - raise InvalidUpdateError( - "Cannot apply multiple updates when updating as input" - ) - - if input_writes := deque(map_input(self.input_channels, values)): - apply_writes( - checkpoint, - channels, - [PregelTaskWrites((), INPUT, input_writes, [])], - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - - # apply input write to channels - next_step = ( - step + 1 - if saved and saved.metadata.get("step") is not None - else -1 - ) - next_config = await checkpointer.aput( - checkpoint_config, - create_checkpoint(checkpoint, channels, next_step), - { - "source": "input", - "step": next_step, - "parents": saved.metadata.get("parents", {}) - if saved - else {}, - }, - get_new_channel_versions( - checkpoint_previous_versions, - checkpoint["channel_versions"], - ), - ) - - # store the writes - await checkpointer.aput_writes( - next_config, - input_writes, - str(uuid5(UUID(checkpoint["id"]), INPUT)), - ) - - return patch_checkpoint_map( - next_config, saved.metadata if saved else None - ) - else: - raise InvalidUpdateError( - f"Received no input writes for {self.input_channels}" - ) - - # no values, copy checkpoint - if as_node == "__copy__": - if len(updates) > 1: - raise InvalidUpdateError( - "Cannot copy checkpoint with multiple updates" - ) - - if saved is None: - raise InvalidUpdateError("Cannot copy a non-existent checkpoint") - - next_checkpoint = create_checkpoint(checkpoint, None, step) - - # copy checkpoint - next_config = await checkpointer.aput( - saved.parent_config - or patch_configurable( - saved.config, {CONFIG_KEY_CHECKPOINT_ID: None} - ), - next_checkpoint, - { - "source": "fork", - "step": step + 1, - "parents": saved.metadata.get("parents", {}), - }, - {}, - ) - - # we want to both clone a checkpoint and update state in one go. - # reuse the same task ID if possible. - if isinstance(values, list) and len(values) > 0: - # figure out the task IDs for the next update checkpoint - next_tasks = prepare_next_tasks( - next_checkpoint, - saved.pending_writes or [], - self.nodes, - channels, - managed, - next_config, - step + 2, - step + 4, - for_execution=True, - store=self.store, - checkpointer=checkpointer, - manager=None, - ) - - tasks_group_by = defaultdict(list) - user_group_by: dict[str, list[StateUpdate]] = defaultdict(list) - - for task in next_tasks.values(): - tasks_group_by[task.name].append(task.id) - - for item in values: - if not isinstance(item, Sequence): - raise InvalidUpdateError( - f"Invalid update item: {item} when copying checkpoint" - ) - - values, as_node = item[:2] - user_group = user_group_by[as_node] - tasks_group = tasks_group_by[as_node] - - target_idx = len(user_group) - task_id = ( - tasks_group[target_idx] - if target_idx < len(tasks_group) - else None - ) - - user_group_by[as_node].append( - StateUpdate(values=values, as_node=as_node, task_id=task_id) - ) - - return await aperform_superstep( - patch_checkpoint_map(next_config, saved.metadata), - [item for lst in user_group_by.values() for item in lst], - ) - - return patch_checkpoint_map( - next_config, saved.metadata if saved else None - ) - # apply pending writes, if not on specific checkpoint - if ( - CONFIG_KEY_CHECKPOINT_ID not in config[CONF] - and saved is not None - and saved.pending_writes - ): - # tasks for this checkpoint - next_tasks = prepare_next_tasks( - checkpoint, - saved.pending_writes, - self.nodes, - channels, - managed, - saved.config, - step + 1, - step + 3, - for_execution=True, - store=self.store, - checkpointer=checkpointer, - manager=None, - ) - # apply null writes - if null_writes := [ - w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID - ]: - apply_writes( - checkpoint, - channels, - [PregelTaskWrites((), INPUT, null_writes, [])], - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - for tid, k, v in saved.pending_writes: - if k in (ERROR, INTERRUPT): - continue - if tid not in next_tasks: - continue - next_tasks[tid].writes.append((k, v)) - if tasks := [t for t in next_tasks.values() if t.writes]: - apply_writes( - checkpoint, - channels, - tasks, - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - valid_updates: list[tuple[str, dict[str, Any] | None, str | None]] = [] - if len(updates) == 1: - values, as_node, task_id = updates[0] - # find last node that updated the state, if not provided - if as_node is None and len(self.nodes) == 1: - as_node = tuple(self.nodes)[0] - elif as_node is None and not saved: - if ( - isinstance(self.input_channels, str) - and self.input_channels in self.nodes - ): - as_node = self.input_channels - elif as_node is None: - last_seen_by_node = sorted( - (v, n) - for n, seen in checkpoint["versions_seen"].items() - if n in self.nodes - for v in seen.values() - ) - # if two nodes updated the state at the same time, it's ambiguous - if last_seen_by_node: - if len(last_seen_by_node) == 1: - as_node = last_seen_by_node[0][1] - elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]: - as_node = last_seen_by_node[-1][1] - if as_node is None: - raise InvalidUpdateError("Ambiguous update, specify as_node") - if as_node not in self.nodes: - raise InvalidUpdateError(f"Node {as_node} does not exist") - valid_updates.append((as_node, values, task_id)) - else: - for values, as_node, task_id in updates: - if as_node is None: - raise InvalidUpdateError( - "as_node is required when applying multiple updates" - ) - if as_node not in self.nodes: - raise InvalidUpdateError(f"Node {as_node} does not exist") - - valid_updates.append((as_node, values, task_id)) - - run_tasks: list[PregelTaskWrites] = [] - run_task_ids: list[str] = [] - - for as_node, values, provided_task_id in valid_updates: - # create task to run all writers of the chosen node - writers = self.nodes[as_node].flat_writers - if not writers: - raise InvalidUpdateError(f"Node {as_node} has no writers") - writes: deque[tuple[str, Any]] = deque() - task = PregelTaskWrites((), as_node, writes, [INTERRUPT]) - task_id = provided_task_id or str( - uuid5(UUID(checkpoint["id"]), INTERRUPT) - ) - run_tasks.append(task) - run_task_ids.append(task_id) - run = RunnableSequence(*writers) if len(writers) > 1 else writers[0] - # execute task - await run.ainvoke( - values, - patch_config( - config, - run_name=self.name + "UpdateState", - configurable={ - # deque.extend is thread-safe - CONFIG_KEY_SEND: writes.extend, - CONFIG_KEY_TASK_ID: task_id, - CONFIG_KEY_READ: partial( - local_read, - _scratchpad( - None, - [], - task_id, - "", - None, - step, - step + 2, - ), - channels, - managed, - task, - ), - }, - ), - ) - # save task writes - for task_id, task in zip(run_task_ids, run_tasks): - # channel writes are saved to current checkpoint - channel_writes = [w for w in task.writes if w[0] != PUSH] - if saved and channel_writes: - await checkpointer.aput_writes( - checkpoint_config, channel_writes, task_id - ) - # apply to checkpoint and save - apply_writes( - checkpoint, - channels, - run_tasks, - checkpointer.get_next_version, - self.trigger_to_nodes, - ) - checkpoint = create_checkpoint(checkpoint, channels, step + 1) - # save checkpoint, after applying writes - next_config = await checkpointer.aput( - checkpoint_config, - checkpoint, - { - "source": "update", - "step": step + 1, - "parents": saved.metadata.get("parents", {}) if saved else {}, - }, - get_new_channel_versions( - checkpoint_previous_versions, checkpoint["channel_versions"] - ), - ) - for task_id, task in zip(run_task_ids, run_tasks): - # save push writes - if push_writes := [w for w in task.writes if w[0] == PUSH]: - await checkpointer.aput_writes(next_config, push_writes, task_id) - return patch_checkpoint_map(next_config, saved.metadata if saved else None) - - current_config = patch_configurable( - config, {CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID])} - ) - for superstep in supersteps: - current_config = await aperform_superstep(current_config, superstep) - return current_config - - def update_state( - self, - config: RunnableConfig, - values: dict[str, Any] | Any | None, - as_node: str | None = None, - task_id: str | None = None, - ) -> RunnableConfig: - """Update the state of the graph with the given values, as if they came from - node `as_node`. If `as_node` is not provided, it will be set to the last node - that updated the state, if not ambiguous. - """ - return self.bulk_update_state(config, [[StateUpdate(values, as_node, task_id)]]) - - async def aupdate_state( - self, - config: RunnableConfig, - values: dict[str, Any] | Any, - as_node: str | None = None, - task_id: str | None = None, - ) -> RunnableConfig: - """Asynchronously update the state of the graph with the given values, as if they came from - node `as_node`. If `as_node` is not provided, it will be set to the last node - that updated the state, if not ambiguous. - """ - return await self.abulk_update_state( - config, [[StateUpdate(values, as_node, task_id)]] - ) - - def _defaults( - self, - config: RunnableConfig, - *, - stream_mode: StreamMode | Sequence[StreamMode], - print_mode: StreamMode | Sequence[StreamMode], - output_keys: str | Sequence[str] | None, - interrupt_before: All | Sequence[str] | None, - interrupt_after: All | Sequence[str] | None, - ) -> tuple[ - set[StreamMode], - str | Sequence[str], - All | Sequence[str], - All | Sequence[str], - BaseCheckpointSaver | None, - BaseStore | None, - BaseCache | None, - ]: - if config["recursion_limit"] < 1: - raise ValueError("recursion_limit must be at least 1") - if output_keys is None: - output_keys = self.stream_channels_asis - else: - validate_keys(output_keys, self.channels) - interrupt_before = interrupt_before or self.interrupt_before_nodes - interrupt_after = interrupt_after or self.interrupt_after_nodes - if not isinstance(stream_mode, list): - stream_modes = {stream_mode} - else: - stream_modes = set(stream_mode) - if isinstance(print_mode, str): - stream_modes.add(print_mode) - else: - stream_modes.update(print_mode) - if self.checkpointer is False: - checkpointer: BaseCheckpointSaver | None = None - elif CONFIG_KEY_CHECKPOINTER in config.get(CONF, {}): - checkpointer = config[CONF][CONFIG_KEY_CHECKPOINTER] - elif self.checkpointer is True: - raise RuntimeError("checkpointer=True cannot be used for root graphs.") - else: - checkpointer = self.checkpointer - if checkpointer and not config.get(CONF): - raise ValueError( - "Checkpointer requires one or more of the following 'configurable' " - "keys: thread_id, checkpoint_ns, checkpoint_id" - ) - if CONFIG_KEY_STORE in config.get(CONF, {}): - store: BaseStore | None = config[CONF][CONFIG_KEY_STORE] - else: - store = self.store - if CONFIG_KEY_CACHE in config.get(CONF, {}): - cache: BaseCache | None = config[CONF][CONFIG_KEY_CACHE] - else: - cache = self.cache - return ( - stream_modes, - output_keys, - interrupt_before, - interrupt_after, - checkpointer, - store, - cache, - ) - - def stream( - self, - input: InputT | Command | None, - config: RunnableConfig | None = None, - *, - stream_mode: StreamMode | Sequence[StreamMode] | None = None, - print_mode: StreamMode | Sequence[StreamMode] = (), - output_keys: str | Sequence[str] | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - checkpoint_during: bool | None = None, - debug: bool | None = None, - subgraphs: bool = False, - ) -> Iterator[dict[str, Any] | Any]: - """Stream graph steps for a single input. - - Args: - input: The input to the graph. - config: The configuration to use for the run. - stream_mode: The mode to stream output, defaults to `self.stream_mode`. - Options are: - - - `"values"`: Emit all values in the state after each step, including interrupts. - When used with functional API, values are emitted once at the end of the workflow. - - `"updates"`: Emit only the node or task names and updates returned by the nodes or tasks after each step. - If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately. - - `"custom"`: Emit custom data from inside nodes or tasks using `StreamWriter`. - - `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks. - Will be emitted as 2-tuples `(LLM token, metadata)`. - - `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by get_state(). - - `"tasks"`: Emit events when tasks start and finish, including their results and errors. - - You can pass a list as the `stream_mode` parameter to stream multiple modes at once. - The streamed outputs will be tuples of `(mode, data)`. - - See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details. - print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way. - output_keys: The keys to stream, defaults to all non-context channels. - interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph. - interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph. - checkpoint_during: Whether to checkpoint intermediate steps, defaults to False. If False, only the final checkpoint is saved. - subgraphs: Whether to stream events from inside subgraphs, defaults to False. - If True, the events will be emitted as tuples `(namespace, data)`, - or `(namespace, mode, data)` if `stream_mode` is a list, - where `namespace` is a tuple with the path to the node where a subgraph is invoked, - e.g. `("parent_node:", "child_node:")`. - - See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details. - - Yields: - The output of each step in the graph. The output shape depends on the stream_mode. - """ - - if stream_mode is None: - # if being called as a node in another graph, default to values mode - # but don't overwrite stream_mode arg if provided - stream_mode = ( - "values" - if config is not None and CONFIG_KEY_TASK_ID in config.get(CONF, {}) - else self.stream_mode - ) - if debug or self.debug: - print_mode = ["updates", "values"] - - stream = SyncQueue() - - config = ensure_config(self.config, config) - callback_manager = get_callback_manager_for_config(config) - run_manager = callback_manager.on_chain_start( - None, - input, - name=config.get("run_name", self.get_name()), - run_id=config.get("run_id"), - ) - try: - # assign defaults - ( - stream_modes, - output_keys, - interrupt_before_, - interrupt_after_, - checkpointer, - store, - cache, - ) = self._defaults( - config, - stream_mode=stream_mode, - print_mode=print_mode, - output_keys=output_keys, - interrupt_before=interrupt_before, - interrupt_after=interrupt_after, - ) - # set up subgraph checkpointing - if self.checkpointer is True: - ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS]) - config[CONF][CONFIG_KEY_CHECKPOINT_NS] = recast_checkpoint_ns(ns) - # set up messages stream mode - if "messages" in stream_modes: - run_manager.inheritable_handlers.append( - StreamMessagesHandler(stream.put, subgraphs) - ) - # set up custom stream mode - if "custom" in stream_modes: - config[CONF][CONFIG_KEY_STREAM_WRITER] = lambda c: stream.put( - ( - tuple( - get_config()[CONF][CONFIG_KEY_CHECKPOINT_NS].split(NS_SEP)[ - :-1 - ] - ), - "custom", - c, - ) - ) - elif ( - CONFIG_KEY_STREAM not in config[CONF] - and CONFIG_KEY_STREAM_WRITER in config[CONF] - ): - # remove parent graph stream writer if subgraph streaming not requested - del config[CONF][CONFIG_KEY_STREAM_WRITER] - # set checkpointing mode for subgraphs - if checkpoint_during is not None: - config[CONF][CONFIG_KEY_CHECKPOINT_DURING] = checkpoint_during - with SyncPregelLoop( - input, - stream=StreamProtocol(stream.put, stream_modes), - config=config, - store=store, - cache=cache, - checkpointer=checkpointer, - nodes=self.nodes, - specs=self.channels, - output_keys=output_keys, - input_keys=self.input_channels, - stream_keys=self.stream_channels_asis, - interrupt_before=interrupt_before_, - interrupt_after=interrupt_after_, - manager=run_manager, - checkpoint_during=checkpoint_during - if checkpoint_during is not None - else config[CONF].get(CONFIG_KEY_CHECKPOINT_DURING, True), - trigger_to_nodes=self.trigger_to_nodes, - migrate_checkpoint=self._migrate_checkpoint, - retry_policy=self.retry_policy, - cache_policy=self.cache_policy, - ) as loop: - # create runner - runner = PregelRunner( - submit=config[CONF].get( - CONFIG_KEY_RUNNER_SUBMIT, weakref.WeakMethod(loop.submit) - ), - put_writes=weakref.WeakMethod(loop.put_writes), - node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED), - ) - # enable subgraph streaming - if subgraphs: - loop.config[CONF][CONFIG_KEY_STREAM] = loop.stream - # enable concurrent streaming - if ( - self.stream_eager - or subgraphs - or "messages" in stream_modes - or "custom" in stream_modes - ): - # we are careful to have a single waiter live at any one time - # because on exit we increment semaphore count by exactly 1 - waiter: concurrent.futures.Future | None = None - # because sync futures cannot be cancelled, we instead - # release the stream semaphore on exit, which will cause - # a pending waiter to return immediately - loop.stack.callback(stream._count.release) - - def get_waiter() -> concurrent.futures.Future[None]: - nonlocal waiter - if waiter is None or waiter.done(): - waiter = loop.submit(stream.wait) - return waiter - else: - return waiter - - else: - get_waiter = None # type: ignore[assignment] - # 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 - # 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(): - for task in loop.match_cached_writes(): - loop.output_writes(task.id, task.writes, cached=True) - for _ in runner.tick( - [t for t in loop.tasks.values() if not t.writes], - timeout=self.step_timeout, - get_waiter=get_waiter, - schedule_task=loop.accept_push, - ): - # emit output - yield from _output( - stream_mode, print_mode, subgraphs, stream.get, queue.Empty - ) - loop.after_tick() - # emit output - yield from _output( - stream_mode, print_mode, subgraphs, stream.get, queue.Empty - ) - # handle exit - if loop.status == "out_of_steps": - msg = create_error_message( - message=( - 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." - ), - error_code=ErrorCode.GRAPH_RECURSION_LIMIT, - ) - raise GraphRecursionError(msg) - # 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 - - async def astream( - self, - input: InputT | Command | None, - config: RunnableConfig | None = None, - *, - stream_mode: StreamMode | Sequence[StreamMode] | None = None, - print_mode: StreamMode | Sequence[StreamMode] = (), - output_keys: str | Sequence[str] | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - checkpoint_during: bool | None = None, - debug: bool | None = None, - subgraphs: bool = False, - ) -> AsyncIterator[dict[str, Any] | Any]: - """Asynchronously stream graph steps for a single input. - - Args: - input: The input to the graph. - config: The configuration to use for the run. - stream_mode: The mode to stream output, defaults to `self.stream_mode`. - Options are: - - - `"values"`: Emit all values in the state after each step, including interrupts. - When used with functional API, values are emitted once at the end of the workflow. - - `"updates"`: Emit only the node or task names and updates returned by the nodes or tasks after each step. - If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately. - - `"custom"`: Emit custom data from inside nodes or tasks using `StreamWriter`. - - `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks. - Will be emitted as 2-tuples `(LLM token, metadata)`. - - `"debug"`: Emit debug events with as much information as possible for each step. - - You can pass a list as the `stream_mode` parameter to stream multiple modes at once. - The streamed outputs will be tuples of `(mode, data)`. - - See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details. - print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way. - output_keys: The keys to stream, defaults to all non-context channels. - interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph. - interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph. - checkpoint_during: Whether to checkpoint intermediate steps, defaults to False. If False, only the final checkpoint is saved. - subgraphs: Whether to stream events from inside subgraphs, defaults to False. - If True, the events will be emitted as tuples `(namespace, data)`, - or `(namespace, mode, data)` if `stream_mode` is a list, - where `namespace` is a tuple with the path to the node where a subgraph is invoked, - e.g. `("parent_node:", "child_node:")`. - - See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details. - - Yields: - The output of each step in the graph. The output shape depends on the stream_mode. - """ - - if stream_mode is None: - # if being called as a node in another graph, default to values mode - # but don't overwrite stream_mode arg if provided - stream_mode = ( - "values" - if config is not None and CONFIG_KEY_TASK_ID in config.get(CONF, {}) - else self.stream_mode - ) - if debug or self.debug: - print_mode = ["updates", "values"] - - stream = AsyncQueue() - aioloop = asyncio.get_running_loop() - stream_put = cast( - Callable[[StreamChunk], None], - partial(aioloop.call_soon_threadsafe, stream.put_nowait), - ) - - config = ensure_config(self.config, config) - callback_manager = get_async_callback_manager_for_config(config) - run_manager = await callback_manager.on_chain_start( - None, - input, - name=config.get("run_name", self.get_name()), - run_id=config.get("run_id"), - ) - # if running from astream_log() run each proc with streaming - do_stream = ( - next( - ( - True - for h in run_manager.handlers - if isinstance(h, _StreamingCallbackHandler) - and not isinstance(h, StreamMessagesHandler) - ), - False, - ) - if _StreamingCallbackHandler is not None - else False - ) - try: - # assign defaults - ( - stream_modes, - output_keys, - interrupt_before_, - interrupt_after_, - checkpointer, - store, - cache, - ) = self._defaults( - config, - stream_mode=stream_mode, - print_mode=print_mode, - output_keys=output_keys, - interrupt_before=interrupt_before, - interrupt_after=interrupt_after, - ) - # set up subgraph checkpointing - if self.checkpointer is True: - ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS]) - config[CONF][CONFIG_KEY_CHECKPOINT_NS] = recast_checkpoint_ns(ns) - # set up messages stream mode - if "messages" in stream_modes: - run_manager.inheritable_handlers.append( - StreamMessagesHandler(stream_put, subgraphs) - ) - # set up custom stream mode - if "custom" in stream_modes: - config[CONF][CONFIG_KEY_STREAM_WRITER] = ( - lambda c: aioloop.call_soon_threadsafe( - stream.put_nowait, - ( - tuple( - get_config()[CONF][CONFIG_KEY_CHECKPOINT_NS].split( - NS_SEP - )[:-1] - ), - "custom", - c, - ), - ) - ) - elif ( - CONFIG_KEY_STREAM not in config[CONF] - and CONFIG_KEY_STREAM_WRITER in config[CONF] - ): - # remove parent graph stream writer if subgraph streaming not requested - del config[CONF][CONFIG_KEY_STREAM_WRITER] - # set checkpointing mode for subgraphs - if checkpoint_during is not None: - config[CONF][CONFIG_KEY_CHECKPOINT_DURING] = checkpoint_during - async with AsyncPregelLoop( - input, - stream=StreamProtocol(stream.put_nowait, stream_modes), - config=config, - store=store, - cache=cache, - checkpointer=checkpointer, - nodes=self.nodes, - specs=self.channels, - output_keys=output_keys, - input_keys=self.input_channels, - stream_keys=self.stream_channels_asis, - interrupt_before=interrupt_before_, - interrupt_after=interrupt_after_, - manager=run_manager, - checkpoint_during=checkpoint_during - if checkpoint_during is not None - else config[CONF].get(CONFIG_KEY_CHECKPOINT_DURING, True), - trigger_to_nodes=self.trigger_to_nodes, - migrate_checkpoint=self._migrate_checkpoint, - retry_policy=self.retry_policy, - cache_policy=self.cache_policy, - ) as loop: - # create runner - runner = PregelRunner( - submit=config[CONF].get( - CONFIG_KEY_RUNNER_SUBMIT, weakref.WeakMethod(loop.submit) - ), - put_writes=weakref.WeakMethod(loop.put_writes), - use_astream=do_stream, - node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED), - ) - # enable subgraph streaming - if subgraphs: - loop.config[CONF][CONFIG_KEY_STREAM] = StreamProtocol( - stream_put, stream_modes - ) - # enable concurrent streaming - if ( - self.stream_eager - or subgraphs - or "messages" in stream_modes - or "custom" in stream_modes - ): - - def get_waiter() -> asyncio.Task[None]: - return aioloop.create_task(stream.wait()) - - else: - get_waiter = None # type: ignore[assignment] - # 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 - # 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(): - for task in await loop.amatch_cached_writes(): - loop.output_writes(task.id, task.writes, cached=True) - async for _ in runner.atick( - [t for t in loop.tasks.values() if not t.writes], - timeout=self.step_timeout, - get_waiter=get_waiter, - schedule_task=loop.aaccept_push, - ): - # emit output - for o in _output( - stream_mode, - print_mode, - subgraphs, - stream.get_nowait, - asyncio.QueueEmpty, - ): - yield o - loop.after_tick() - # emit output - for o in _output( - stream_mode, - print_mode, - subgraphs, - stream.get_nowait, - asyncio.QueueEmpty, - ): - yield o - # handle exit - if loop.status == "out_of_steps": - msg = create_error_message( - message=( - 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." - ), - error_code=ErrorCode.GRAPH_RECURSION_LIMIT, - ) - raise GraphRecursionError(msg) - # set final channel values as run output - await run_manager.on_chain_end(loop.output) - except BaseException as e: - await asyncio.shield(run_manager.on_chain_error(e)) - raise - - def invoke( - self, - input: InputT | Command | None, - config: RunnableConfig | None = None, - *, - stream_mode: StreamMode = "values", - print_mode: StreamMode | Sequence[StreamMode] = (), - output_keys: str | Sequence[str] | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - **kwargs: Any, - ) -> dict[str, Any] | Any: - """Run the graph with a single input and config. - - Args: - input: The input data for the graph. It can be a dictionary or any other type. - config: Optional. The configuration for the graph run. - stream_mode: Optional[str]. The stream mode for the graph run. Default is "values". - print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way. - output_keys: Optional. The output keys to retrieve from the graph run. - interrupt_before: Optional. The nodes to interrupt the graph run before. - interrupt_after: Optional. The nodes to interrupt the graph run after. - **kwargs: Additional keyword arguments to pass to the graph run. - - Returns: - The output of the graph run. If stream_mode is "values", it returns the latest output. - If stream_mode is not "values", it returns a list of output chunks. - """ - output_keys = output_keys if output_keys is not None else self.output_channels - - latest: dict[str, Any] | Any = None - chunks: list[dict[str, Any] | Any] = [] - interrupts: list[Interrupt] = [] - - for chunk in self.stream( - input, - config, - stream_mode=["updates", "values"] - if stream_mode == "values" - else stream_mode, - print_mode=print_mode, - output_keys=output_keys, - interrupt_before=interrupt_before, - interrupt_after=interrupt_after, - **kwargs, - ): - if stream_mode == "values": - if len(chunk) == 2: - mode, payload = cast(tuple[StreamMode, Any], chunk) - else: - _, mode, payload = cast( - tuple[tuple[str, ...], StreamMode, Any], chunk - ) - if ( - mode == "updates" - and isinstance(payload, dict) - and (ints := payload.get(INTERRUPT)) is not None - ): - interrupts.extend(ints) - elif mode == "values": - latest = payload - else: - chunks.append(chunk) - - if stream_mode == "values": - if interrupts: - return ( - {**latest, INTERRUPT: interrupts} - if isinstance(latest, dict) - else {INTERRUPT: interrupts} - ) - return latest - else: - return chunks - - async def ainvoke( - self, - input: InputT | Command | None, - config: RunnableConfig | None = None, - *, - stream_mode: StreamMode = "values", - print_mode: StreamMode | Sequence[StreamMode] = (), - output_keys: str | Sequence[str] | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - **kwargs: Any, - ) -> dict[str, Any] | Any: - """Asynchronously invoke the graph on a single input. - - Args: - input: The input data for the computation. It can be a dictionary or any other type. - config: Optional. The configuration for the computation. - stream_mode: Optional. The stream mode for the computation. Default is "values". - print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way. - output_keys: Optional. The output keys to include in the result. Default is None. - interrupt_before: Optional. The nodes to interrupt before. Default is None. - interrupt_after: Optional. The nodes to interrupt after. Default is None. - **kwargs: Additional keyword arguments. - - Returns: - The result of the computation. If stream_mode is "values", it returns the latest value. - If stream_mode is "chunks", it returns a list of chunks. - """ - - output_keys = output_keys if output_keys is not None else self.output_channels - - latest: dict[str, Any] | Any = None - chunks: list[dict[str, Any] | Any] = [] - interrupts: list[Interrupt] = [] - - async for chunk in self.astream( - input, - config, - stream_mode=["updates", "values"] - if stream_mode == "values" - else stream_mode, - print_mode=print_mode, - output_keys=output_keys, - interrupt_before=interrupt_before, - interrupt_after=interrupt_after, - **kwargs, - ): - if stream_mode == "values": - if len(chunk) == 2: - mode, payload = cast(tuple[StreamMode, Any], chunk) - else: - _, mode, payload = cast( - tuple[tuple[str, ...], StreamMode, Any], chunk - ) - if ( - mode == "updates" - and isinstance(payload, dict) - and (ints := payload.get(INTERRUPT)) is not None - ): - interrupts.extend(ints) - elif mode == "values": - latest = payload - else: - chunks.append(chunk) - - if stream_mode == "values": - if interrupts: - return ( - {**latest, INTERRUPT: interrupts} - if isinstance(latest, dict) - else {INTERRUPT: interrupts} - ) - return latest - else: - return chunks - - def clear_cache(self, nodes: Sequence[str] | None = None) -> None: - """Clear the cache for the given nodes.""" - if not self.cache: - raise ValueError("No cache is set for this graph. Cannot clear cache.") - nodes = nodes or self.nodes.keys() - # collect namespaces to clear - namespaces: list[tuple[str, ...]] = [] - for node in nodes: - if node in self.nodes: - namespaces.append( - ( - CACHE_NS_WRITES, - (identifier(self.nodes[node]) or "__dynamic__"), - node, - ), - ) - # clear cache - self.cache.clear(namespaces) - - async def aclear_cache(self, nodes: Sequence[str] | None = None) -> None: - """Asynchronously clear the cache for the given nodes.""" - if not self.cache: - raise ValueError("No cache is set for this graph. Cannot clear cache.") - nodes = nodes or self.nodes.keys() - # collect namespaces to clear - namespaces: list[tuple[str, ...]] = [] - for node in nodes: - if node in self.nodes: - namespaces.append( - ( - CACHE_NS_WRITES, - (identifier(self.nodes[node]) or "__dynamic__"), - node, - ), - ) - # clear cache - await self.cache.aclear(namespaces) - - -def _trigger_to_nodes(nodes: dict[str, PregelNode]) -> Mapping[str, Sequence[str]]: - """Index from a trigger to nodes that depend on it.""" - trigger_to_nodes: defaultdict[str, list[str]] = defaultdict(list) - for name, node in nodes.items(): - for trigger in node.triggers: - trigger_to_nodes[trigger].append(name) - return dict(trigger_to_nodes) - - -def _output( - stream_mode: StreamMode | Sequence[StreamMode], - print_mode: StreamMode | Sequence[StreamMode], - stream_subgraphs: bool, - getter: Callable[[], tuple[tuple[str, ...], str, Any]], - empty_exc: type[Exception], -) -> Iterator: - while True: - try: - ns, mode, payload = getter() - except empty_exc: - break - if mode in print_mode: - if stream_subgraphs and ns: - print( - " ".join( - ( - get_bolded_text(f"[{mode}]"), - get_colored_text(f"[graph={ns}]", color="yellow"), - repr(payload), - ) - ) - ) - else: - print( - " ".join( - ( - get_bolded_text(f"[{mode}]"), - repr(payload), - ) - ) - ) - if mode in stream_mode: - if stream_subgraphs and isinstance(stream_mode, list): - yield (ns, mode, payload) - elif isinstance(stream_mode, list): - yield (mode, payload) - elif stream_subgraphs: - yield (ns, payload) - else: - yield payload +__all__ = ("Pregel", "NodeBuilder") diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/_algo.py similarity index 96% rename from libs/langgraph/langgraph/pregel/algo.py rename to libs/langgraph/langgraph/pregel/_algo.py index 9d15aff3c..8b9a433fb 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/_algo.py @@ -25,33 +25,22 @@ from langchain_core.callbacks.manager import AsyncParentRunManager, ParentRunMan from langchain_core.runnables.config import RunnableConfig from xxhash import xxh3_128_hexdigest -from langgraph.channels.base import BaseChannel -from langgraph.channels.topic import Topic -from langgraph.checkpoint.base import ( - BaseCheckpointSaver, - ChannelVersions, - Checkpoint, - PendingWrite, - V, -) -from langgraph.constants import ( +from langgraph._internal._config import merge_configs, patch_config +from langgraph._internal._constants import ( CACHE_NS_WRITES, CONF, CONFIG_KEY_CHECKPOINT_ID, CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_CHECKPOINT_NS, CONFIG_KEY_CHECKPOINTER, - CONFIG_KEY_PREVIOUS, CONFIG_KEY_READ, CONFIG_KEY_RESUME_MAP, + CONFIG_KEY_RUNTIME, CONFIG_KEY_SCRATCHPAD, CONFIG_KEY_SEND, - CONFIG_KEY_STORE, CONFIG_KEY_TASK_ID, - EMPTY_SEQ, ERROR, INTERRUPT, - MISSING, NO_WRITES, NS_END, NS_SEP, @@ -62,26 +51,36 @@ from langgraph.constants import ( RESERVED, RESUME, RETURN, - TAG_HIDDEN, TASKS, - Send, ) +from langgraph._internal._typing import EMPTY_SEQ, MISSING +from langgraph.channels.base import BaseChannel +from langgraph.channels.topic import Topic +from langgraph.checkpoint.base import ( + BaseCheckpointSaver, + ChannelVersions, + Checkpoint, + PendingWrite, + V, +) +from langgraph.constants import TAG_HIDDEN from langgraph.managed.base import ManagedValueMapping -from langgraph.pregel.call import get_runnable_for_task, identifier -from langgraph.pregel.io import read_channels -from langgraph.pregel.log import logger -from langgraph.pregel.read import INPUT_CACHE_KEY_TYPE, PregelNode +from langgraph.pregel._call import get_runnable_for_task, identifier +from langgraph.pregel._io import read_channels +from langgraph.pregel._log import logger +from langgraph.pregel._read import INPUT_CACHE_KEY_TYPE, PregelNode +from langgraph.pregel._scratchpad import PregelScratchpad +from langgraph.runtime import DEFAULT_RUNTIME, Runtime from langgraph.store.base import BaseStore from langgraph.types import ( All, CacheKey, CachePolicy, PregelExecutableTask, - PregelScratchpad, PregelTask, RetryPolicy, + Send, ) -from langgraph.utils.config import merge_configs, patch_config GetNextVersion = Callable[[Optional[V], None], V] SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11) @@ -583,6 +582,10 @@ def prepare_single_task( step, stop, ) + runtime = cast( + Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME) + ) + runtime = runtime.override(store=store) return PregelExecutableTask( name, call.input, @@ -604,7 +607,6 @@ def prepare_single_task( managed, PregelTaskWrites(task_path, name, writes, triggers), ), - CONFIG_KEY_STORE: (store or configurable.get(CONFIG_KEY_STORE)), CONFIG_KEY_CHECKPOINTER: ( checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER) ), @@ -615,6 +617,7 @@ def prepare_single_task( CONFIG_KEY_CHECKPOINT_ID: None, CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns, CONFIG_KEY_SCRATCHPAD: scratchpad, + CONFIG_KEY_RUNTIME: runtime, }, ), triggers, @@ -709,6 +712,12 @@ def prepare_single_task( step, stop, ) + runtime = cast( + Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME) + ) + runtime = runtime.override( + store=store, previous=checkpoint["channel_values"].get(PREVIOUS, None) + ) return PregelExecutableTask( packet.node, packet.arg, @@ -731,7 +740,6 @@ def prepare_single_task( managed, PregelTaskWrites(task_path, packet.node, writes, triggers), ), - CONFIG_KEY_STORE: (store or configurable.get(CONFIG_KEY_STORE)), CONFIG_KEY_CHECKPOINTER: ( checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER) ), @@ -742,9 +750,7 @@ def prepare_single_task( CONFIG_KEY_CHECKPOINT_ID: None, CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns, CONFIG_KEY_SCRATCHPAD: scratchpad, - CONFIG_KEY_PREVIOUS: checkpoint["channel_values"].get( - PREVIOUS, None - ), + CONFIG_KEY_RUNTIME: runtime, }, ), triggers, @@ -846,6 +852,13 @@ def prepare_single_task( ) else: cache_key = None + runtime = cast( + Runtime, configurable.get(CONFIG_KEY_RUNTIME, DEFAULT_RUNTIME) + ) + runtime = runtime.override( + previous=checkpoint["channel_values"].get(PREVIOUS, None), + store=store, + ) return PregelExecutableTask( name, val, @@ -877,9 +890,6 @@ def prepare_single_task( triggers, ), ), - CONFIG_KEY_STORE: ( - store or configurable.get(CONFIG_KEY_STORE) - ), CONFIG_KEY_CHECKPOINTER: ( checkpointer or configurable.get(CONFIG_KEY_CHECKPOINTER) @@ -891,9 +901,7 @@ def prepare_single_task( CONFIG_KEY_CHECKPOINT_ID: None, CONFIG_KEY_CHECKPOINT_NS: task_checkpoint_ns, CONFIG_KEY_SCRATCHPAD: scratchpad, - CONFIG_KEY_PREVIOUS: checkpoint["channel_values"].get( - PREVIOUS, None - ), + CONFIG_KEY_RUNTIME: runtime, }, ), triggers, diff --git a/libs/langgraph/langgraph/pregel/call.py b/libs/langgraph/langgraph/pregel/_call.py similarity index 96% rename from libs/langgraph/langgraph/pregel/call.py rename to libs/langgraph/langgraph/pregel/_call.py index df1e158cb..5956160aa 100644 --- a/libs/langgraph/langgraph/pregel/call.py +++ b/libs/langgraph/langgraph/pregel/_call.py @@ -13,16 +13,16 @@ from typing import Any, Callable, Generic, TypeVar, cast from langchain_core.runnables import Runnable from typing_extensions import ParamSpec -from langgraph.constants import CONF, CONFIG_KEY_CALL, RETURN -from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry -from langgraph.types import CachePolicy, RetryPolicy -from langgraph.utils.config import get_config -from langgraph.utils.runnable import ( +from langgraph._internal._constants import CONF, CONFIG_KEY_CALL, RETURN +from langgraph._internal._runnable import ( RunnableCallable, RunnableSeq, is_async_callable, run_in_executor, ) +from langgraph.config import get_config +from langgraph.pregel._write import ChannelWrite, ChannelWriteEntry +from langgraph.types import CachePolicy, RetryPolicy ## # Utilities borrowed from cloudpickle. @@ -78,8 +78,8 @@ def _whichmodule(obj: Any, name: str) -> str | None: def identifier(obj: Any, name: str | None = None) -> str | None: """Return the module and name of an object.""" - from langgraph.pregel.read import PregelNode - from langgraph.utils.runnable import RunnableCallable, RunnableSeq + from langgraph._internal._runnable import RunnableCallable, RunnableSeq + from langgraph.pregel._read import PregelNode if isinstance(obj, PregelNode): obj = obj.bound diff --git a/libs/langgraph/langgraph/pregel/checkpoint.py b/libs/langgraph/langgraph/pregel/_checkpoint.py similarity index 98% rename from libs/langgraph/langgraph/pregel/checkpoint.py rename to libs/langgraph/langgraph/pregel/_checkpoint.py index b404ee550..50eb254b8 100644 --- a/libs/langgraph/langgraph/pregel/checkpoint.py +++ b/libs/langgraph/langgraph/pregel/_checkpoint.py @@ -3,10 +3,10 @@ from __future__ import annotations from collections.abc import Mapping from datetime import datetime, timezone +from langgraph._internal._typing import MISSING from langgraph.channels.base import BaseChannel from langgraph.checkpoint.base import Checkpoint from langgraph.checkpoint.base.id import uuid6 -from langgraph.constants import MISSING from langgraph.managed.base import ManagedValueMapping, ManagedValueSpec LATEST_VERSION = 4 diff --git a/libs/langgraph/langgraph/channels/py.typed b/libs/langgraph/langgraph/pregel/_config.py similarity index 100% rename from libs/langgraph/langgraph/channels/py.typed rename to libs/langgraph/langgraph/pregel/_config.py diff --git a/libs/langgraph/langgraph/pregel/draw.py b/libs/langgraph/langgraph/pregel/_draw.py similarity index 95% rename from libs/langgraph/langgraph/pregel/draw.py rename to libs/langgraph/langgraph/pregel/_draw.py index 091e92be3..b8ae73389 100644 --- a/libs/langgraph/langgraph/pregel/draw.py +++ b/libs/langgraph/langgraph/pregel/_draw.py @@ -7,20 +7,21 @@ from typing import Any, cast from langchain_core.runnables.config import RunnableConfig from langchain_core.runnables.graph import Graph, Node +from langgraph._internal._constants import CONF, CONFIG_KEY_SEND, INPUT from langgraph.channels.base import BaseChannel from langgraph.checkpoint.base import BaseCheckpointSaver -from langgraph.constants import CONF, CONFIG_KEY_SEND, END, INPUT, START +from langgraph.constants import END, START from langgraph.managed.base import ManagedValueSpec -from langgraph.pregel.algo import ( +from langgraph.pregel._algo import ( PregelTaskWrites, apply_writes, increment, prepare_next_tasks, ) -from langgraph.pregel.checkpoint import channels_from_checkpoint, empty_checkpoint -from langgraph.pregel.io import map_input -from langgraph.pregel.read import PregelNode -from langgraph.pregel.write import ChannelWrite +from langgraph.pregel._checkpoint import channels_from_checkpoint, empty_checkpoint +from langgraph.pregel._io import map_input +from langgraph.pregel._read import PregelNode +from langgraph.pregel._write import ChannelWrite from langgraph.types import All, Checkpointer diff --git a/libs/langgraph/langgraph/pregel/executor.py b/libs/langgraph/langgraph/pregel/_executor.py similarity index 98% rename from libs/langgraph/langgraph/pregel/executor.py rename to libs/langgraph/langgraph/pregel/_executor.py index 62df4b19f..db37135c0 100644 --- a/libs/langgraph/langgraph/pregel/executor.py +++ b/libs/langgraph/langgraph/pregel/_executor.py @@ -18,8 +18,8 @@ from langchain_core.runnables import RunnableConfig from langchain_core.runnables.config import get_executor_for_config from typing_extensions import ParamSpec +from langgraph._internal._future import CONTEXT_NOT_SUPPORTED, run_coroutine_threadsafe from langgraph.errors import GraphBubbleUp -from langgraph.utils.future import CONTEXT_NOT_SUPPORTED, run_coroutine_threadsafe P = ParamSpec("P") T = TypeVar("T") diff --git a/libs/langgraph/langgraph/pregel/io.py b/libs/langgraph/langgraph/pregel/_io.py similarity index 96% rename from libs/langgraph/langgraph/pregel/io.py rename to libs/langgraph/langgraph/pregel/_io.py index 48268af76..3c05dbda7 100644 --- a/libs/langgraph/langgraph/pregel/io.py +++ b/libs/langgraph/langgraph/pregel/_io.py @@ -4,21 +4,19 @@ from collections import Counter from collections.abc import Iterator, Mapping, Sequence from typing import Any, Literal -from langgraph.channels.base import BaseChannel, EmptyChannelError -from langgraph.constants import ( - EMPTY_SEQ, +from langgraph._internal._constants import ( ERROR, INTERRUPT, - MISSING, NULL_TASK_ID, RESUME, RETURN, - START, - TAG_HIDDEN, TASKS, ) +from langgraph._internal._typing import EMPTY_SEQ, MISSING +from langgraph.channels.base import BaseChannel, EmptyChannelError +from langgraph.constants import START, TAG_HIDDEN from langgraph.errors import InvalidUpdateError -from langgraph.pregel.log import logger +from langgraph.pregel._log import logger from langgraph.types import Command, PregelExecutableTask, Send diff --git a/libs/langgraph/langgraph/pregel/log.py b/libs/langgraph/langgraph/pregel/_log.py similarity index 100% rename from libs/langgraph/langgraph/pregel/log.py rename to libs/langgraph/langgraph/pregel/_log.py diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/_loop.py similarity index 97% rename from libs/langgraph/langgraph/pregel/loop.py rename to libs/langgraph/langgraph/pregel/_loop.py index e5d4e1534..0f1c30d47 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/_loop.py @@ -27,6 +27,28 @@ from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager from langchain_core.runnables import RunnableConfig from typing_extensions import ParamSpec, Self +from langgraph._internal._config import patch_configurable +from langgraph._internal._constants import ( + CONF, + CONFIG_KEY_CHECKPOINT_ID, + CONFIG_KEY_CHECKPOINT_MAP, + CONFIG_KEY_CHECKPOINT_NS, + CONFIG_KEY_RESUME_MAP, + CONFIG_KEY_RESUMING, + CONFIG_KEY_SCRATCHPAD, + CONFIG_KEY_STREAM, + CONFIG_KEY_TASK_ID, + CONFIG_KEY_THREAD_ID, + ERROR, + INPUT, + INTERRUPT, + NS_END, + NS_SEP, + NULL_TASK_ID, + PUSH, + RESUME, +) +from langgraph._internal._typing import EMPTY_SEQ, MISSING from langgraph.cache.base import BaseCache from langgraph.channels.base import BaseChannel from langgraph.checkpoint.base import ( @@ -38,29 +60,7 @@ from langgraph.checkpoint.base import ( CheckpointTuple, PendingWrite, ) -from langgraph.constants import ( - CONF, - CONFIG_KEY_CHECKPOINT_ID, - CONFIG_KEY_CHECKPOINT_MAP, - CONFIG_KEY_CHECKPOINT_NS, - CONFIG_KEY_RESUME_MAP, - CONFIG_KEY_RESUMING, - CONFIG_KEY_SCRATCHPAD, - CONFIG_KEY_STREAM, - CONFIG_KEY_TASK_ID, - CONFIG_KEY_THREAD_ID, - EMPTY_SEQ, - ERROR, - INPUT, - INTERRUPT, - MISSING, - NS_END, - NS_SEP, - NULL_TASK_ID, - PUSH, - RESUME, - TAG_HIDDEN, -) +from langgraph.constants import TAG_HIDDEN from langgraph.errors import ( EmptyInputError, GraphInterrupt, @@ -69,7 +69,7 @@ from langgraph.managed.base import ( ManagedValueMapping, ManagedValueSpec, ) -from langgraph.pregel.algo import ( +from langgraph.pregel._algo import ( Call, GetNextVersion, PregelTaskWrites, @@ -81,44 +81,43 @@ from langgraph.pregel.algo import ( should_interrupt, task_path_str, ) -from langgraph.pregel.checkpoint import ( +from langgraph.pregel._checkpoint import ( channels_from_checkpoint, copy_checkpoint, create_checkpoint, empty_checkpoint, ) -from langgraph.pregel.debug import ( - map_debug_checkpoint, - map_debug_task_results, - map_debug_tasks, -) -from langgraph.pregel.executor import ( +from langgraph.pregel._executor import ( AsyncBackgroundExecutor, BackgroundExecutor, Submit, ) -from langgraph.pregel.io import ( +from langgraph.pregel._io import ( map_command, map_input, map_output_updates, map_output_values, read_channels, ) -from langgraph.pregel.read import PregelNode -from langgraph.pregel.utils import get_new_channel_versions, is_xxh3_128_hexdigest +from langgraph.pregel._read import PregelNode +from langgraph.pregel._scratchpad import PregelScratchpad +from langgraph.pregel._utils import get_new_channel_versions, is_xxh3_128_hexdigest +from langgraph.pregel.debug import ( + map_debug_checkpoint, + map_debug_task_results, + map_debug_tasks, +) +from langgraph.pregel.protocol import StreamChunk, StreamProtocol from langgraph.store.base import BaseStore from langgraph.types import ( All, CachePolicy, Command, + Durability, PregelExecutableTask, - PregelScratchpad, RetryPolicy, - StreamChunk, StreamMode, - StreamProtocol, ) -from langgraph.utils.config import patch_configurable V = TypeVar("V") P = ParamSpec("P") @@ -156,7 +155,7 @@ class PregelLoop: manager: None | AsyncParentRunManager | ParentRunManager interrupt_after: All | Sequence[str] interrupt_before: All | Sequence[str] - checkpoint_during: bool + durability: Durability retry_policy: Sequence[RetryPolicy] cache_policy: CachePolicy | None @@ -218,13 +217,13 @@ class PregelLoop: output_keys: str | Sequence[str], stream_keys: str | Sequence[str], trigger_to_nodes: Mapping[str, Sequence[str]], + durability: Durability, interrupt_after: All | Sequence[str] = EMPTY_SEQ, interrupt_before: All | Sequence[str] = EMPTY_SEQ, manager: None | AsyncParentRunManager | ParentRunManager = None, migrate_checkpoint: Callable[[Checkpoint], None] | None = None, retry_policy: Sequence[RetryPolicy] = (), cache_policy: CachePolicy | None = None, - checkpoint_during: bool = True, ) -> None: self.stream = stream self.config = config @@ -248,7 +247,7 @@ class PregelLoop: self.trigger_to_nodes = trigger_to_nodes self.retry_policy = retry_policy self.cache_policy = cache_policy - self.checkpoint_during = checkpoint_during + self.durability = durability if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]: self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM]) scratchpad: PregelScratchpad | None = config[CONF].get(CONFIG_KEY_SCRATCHPAD) @@ -325,7 +324,7 @@ class PregelLoop: writes_to_save = writes # save writes self.checkpoint_pending_writes.extend((task_id, c, v) for c, v in writes) - if self.checkpoint_during and self.checkpointer_put_writes is not None: + if self.durability != "exit" and self.checkpointer_put_writes is not None: config = patch_configurable( self.checkpoint_config, { @@ -686,7 +685,7 @@ class PregelLoop: self.checkpoint_metadata = metadata # do checkpoint? do_checkpoint = self._checkpointer_put_after_previous is not None and ( - exiting or self.checkpoint_during + exiting or self.durability != "exit" ) # create new checkpoint self.checkpoint = create_checkpoint( @@ -748,7 +747,7 @@ class PregelLoop: traceback: TracebackType | None, ) -> bool | None: # persist current checkpoint and writes - if not self.checkpoint_during and ( + if self.durability == "exit" and ( # if it's a top graph not self.is_nested # or a nested graph with error or interrupt @@ -893,6 +892,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager): nodes: Mapping[str, PregelNode], specs: Mapping[str, BaseChannel | ManagedValueSpec], trigger_to_nodes: Mapping[str, Sequence[str]], + durability: Durability, manager: None | AsyncParentRunManager | ParentRunManager = None, interrupt_after: All | Sequence[str] = EMPTY_SEQ, interrupt_before: All | Sequence[str] = EMPTY_SEQ, @@ -902,7 +902,6 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager): migrate_checkpoint: Callable[[Checkpoint], None] | None = None, retry_policy: Sequence[RetryPolicy] = (), cache_policy: CachePolicy | None = None, - checkpoint_during: bool = True, ) -> None: super().__init__( input, @@ -923,7 +922,7 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager): trigger_to_nodes=trigger_to_nodes, retry_policy=retry_policy, cache_policy=cache_policy, - checkpoint_during=checkpoint_during, + durability=durability, ) self.stack = ExitStack() if checkpointer: @@ -1064,6 +1063,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager): nodes: Mapping[str, PregelNode], specs: Mapping[str, BaseChannel | ManagedValueSpec], trigger_to_nodes: Mapping[str, Sequence[str]], + durability: Durability, interrupt_after: All | Sequence[str] = EMPTY_SEQ, interrupt_before: All | Sequence[str] = EMPTY_SEQ, manager: None | AsyncParentRunManager | ParentRunManager = None, @@ -1073,7 +1073,6 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager): migrate_checkpoint: Callable[[Checkpoint], None] | None = None, retry_policy: Sequence[RetryPolicy] = (), cache_policy: CachePolicy | None = None, - checkpoint_during: bool = True, ) -> None: super().__init__( input, @@ -1094,7 +1093,7 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager): trigger_to_nodes=trigger_to_nodes, retry_policy=retry_policy, cache_policy=cache_policy, - checkpoint_during=checkpoint_during, + durability=durability, ) self.stack = AsyncExitStack() if checkpointer: diff --git a/libs/langgraph/langgraph/pregel/messages.py b/libs/langgraph/langgraph/pregel/_messages.py similarity index 96% rename from libs/langgraph/langgraph/pregel/messages.py rename to libs/langgraph/langgraph/pregel/_messages.py index 9a9210aa6..550ea789c 100644 --- a/libs/langgraph/langgraph/pregel/messages.py +++ b/libs/langgraph/langgraph/pregel/_messages.py @@ -13,8 +13,10 @@ from langchain_core.callbacks import BaseCallbackHandler from langchain_core.messages import BaseMessage from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, LLMResult -from langgraph.constants import NS_SEP, TAG_HIDDEN, TAG_NOSTREAM, TAG_NOSTREAM_ALT -from langgraph.types import Command, StreamChunk +from langgraph._internal._constants import NS_SEP +from langgraph.constants import TAG_HIDDEN, TAG_NOSTREAM +from langgraph.pregel.protocol import StreamChunk +from langgraph.types import Command try: from langchain_core.tracers._streaming import _StreamingCallbackHandler @@ -94,9 +96,7 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler): metadata: dict[str, Any] | None = None, **kwargs: Any, ) -> Any: - if metadata and ( - not tags or (TAG_NOSTREAM not in tags and TAG_NOSTREAM_ALT not in tags) - ): + if metadata and (not tags or (TAG_NOSTREAM not in tags)): ns = tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP))[ :-1 ] diff --git a/libs/langgraph/langgraph/pregel/read.py b/libs/langgraph/langgraph/pregel/_read.py similarity index 96% rename from libs/langgraph/langgraph/pregel/read.py rename to libs/langgraph/langgraph/pregel/_read.py index 30a9af652..bb3a6bf12 100644 --- a/libs/langgraph/langgraph/pregel/read.py +++ b/libs/langgraph/langgraph/pregel/_read.py @@ -10,13 +10,13 @@ from typing import ( from langchain_core.runnables import Runnable, RunnableConfig -from langgraph.constants import CONF, CONFIG_KEY_READ +from langgraph._internal._config import merge_configs +from langgraph._internal._constants import CONF, CONFIG_KEY_READ +from langgraph._internal._runnable import RunnableCallable, RunnableSeq +from langgraph.pregel._utils import find_subgraph_pregel +from langgraph.pregel._write import ChannelWrite from langgraph.pregel.protocol import PregelProtocol -from langgraph.pregel.utils import find_subgraph_pregel -from langgraph.pregel.write import ChannelWrite from langgraph.types import CachePolicy, RetryPolicy -from langgraph.utils.config import merge_configs -from langgraph.utils.runnable import RunnableCallable, RunnableSeq READ_TYPE = Callable[[Union[str, Sequence[str]], bool], Union[Any, dict[str, Any]]] INPUT_CACHE_KEY_TYPE = tuple[Callable[..., Any], tuple[str, ...]] @@ -46,7 +46,6 @@ class ChannelRead(RunnableCallable): tags=tags, name=None, trace=False, - func_accepts_config=True, ) self.fresh = fresh self.mapper = mapper diff --git a/libs/langgraph/langgraph/pregel/retry.py b/libs/langgraph/langgraph/pregel/_retry.py similarity index 98% rename from libs/langgraph/langgraph/pregel/retry.py rename to libs/langgraph/langgraph/pregel/_retry.py index a91edb9c4..d54797108 100644 --- a/libs/langgraph/langgraph/pregel/retry.py +++ b/libs/langgraph/langgraph/pregel/_retry.py @@ -9,7 +9,8 @@ from collections.abc import Awaitable, Sequence from dataclasses import replace from typing import Any, Callable -from langgraph.constants import ( +from langgraph._internal._config import patch_configurable +from langgraph._internal._constants import ( CONF, CONFIG_KEY_CHECKPOINT_NS, CONFIG_KEY_RESUMING, @@ -17,7 +18,6 @@ from langgraph.constants import ( ) from langgraph.errors import GraphBubbleUp, ParentCommand from langgraph.types import Command, PregelExecutableTask, RetryPolicy -from langgraph.utils.config import patch_configurable logger = logging.getLogger(__name__) SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11) diff --git a/libs/langgraph/langgraph/pregel/runner.py b/libs/langgraph/langgraph/pregel/_runner.py similarity index 98% rename from libs/langgraph/langgraph/pregel/runner.py rename to libs/langgraph/langgraph/pregel/_runner.py index 39dd92256..9a6f117e0 100644 --- a/libs/langgraph/langgraph/pregel/runner.py +++ b/libs/langgraph/langgraph/pregel/_runner.py @@ -19,29 +19,29 @@ from typing import ( from langchain_core.callbacks import Callbacks -from langgraph.constants import ( +from langgraph._internal._constants import ( CONF, CONFIG_KEY_CALL, CONFIG_KEY_SCRATCHPAD, ERROR, INTERRUPT, - MISSING, NO_WRITES, RESUME, RETURN, - TAG_HIDDEN, ) +from langgraph._internal._future import chain_future, run_coroutine_threadsafe +from langgraph._internal._typing import MISSING +from langgraph.constants import TAG_HIDDEN from langgraph.errors import GraphBubbleUp, GraphInterrupt -from langgraph.pregel.algo import Call -from langgraph.pregel.executor import Submit -from langgraph.pregel.retry import arun_with_retry, run_with_retry +from langgraph.pregel._algo import Call +from langgraph.pregel._executor import Submit +from langgraph.pregel._retry import arun_with_retry, run_with_retry +from langgraph.pregel._scratchpad import PregelScratchpad from langgraph.types import ( CachePolicy, PregelExecutableTask, - PregelScratchpad, RetryPolicy, ) -from langgraph.utils.future import chain_future, run_coroutine_threadsafe F = TypeVar("F", concurrent.futures.Future, asyncio.Future) E = TypeVar("E", threading.Event, asyncio.Event) diff --git a/libs/langgraph/langgraph/pregel/_scratchpad.py b/libs/langgraph/langgraph/pregel/_scratchpad.py new file mode 100644 index 000000000..1e8eb8a8b --- /dev/null +++ b/libs/langgraph/langgraph/pregel/_scratchpad.py @@ -0,0 +1,18 @@ +import dataclasses +from typing import Any, Callable + +from langgraph.types import _DC_KWARGS + + +@dataclasses.dataclass(**_DC_KWARGS) +class PregelScratchpad: + step: int + stop: int + # call + call_counter: Callable[[], int] + # interrupt + interrupt_counter: Callable[[], int] + get_null_resume: Callable[[bool], Any] + resume: list[Any] + # subgraph + subgraph_counter: Callable[[], int] diff --git a/libs/langgraph/langgraph/pregel/utils.py b/libs/langgraph/langgraph/pregel/_utils.py similarity index 97% rename from libs/langgraph/langgraph/pregel/utils.py rename to libs/langgraph/langgraph/pregel/_utils.py index a37228c05..87c026ae4 100644 --- a/libs/langgraph/langgraph/pregel/utils.py +++ b/libs/langgraph/langgraph/pregel/_utils.py @@ -6,12 +6,12 @@ import re import textwrap from typing import Any, Callable -from langchain_core.runnables import RunnableLambda, RunnableSequence +from langchain_core.runnables import Runnable, RunnableLambda, RunnableSequence from typing_extensions import override +from langgraph._internal._runnable import RunnableCallable, RunnableSeq from langgraph.checkpoint.base import ChannelVersions from langgraph.pregel.protocol import PregelProtocol -from langgraph.utils.runnable import Runnable, RunnableCallable, RunnableSeq def get_new_channel_versions( diff --git a/libs/langgraph/langgraph/pregel/validate.py b/libs/langgraph/langgraph/pregel/_validate.py similarity index 97% rename from libs/langgraph/langgraph/pregel/validate.py rename to libs/langgraph/langgraph/pregel/_validate.py index 00da8b4b1..fcfb54c9a 100644 --- a/libs/langgraph/langgraph/pregel/validate.py +++ b/libs/langgraph/langgraph/pregel/_validate.py @@ -3,10 +3,10 @@ from __future__ import annotations from collections.abc import Mapping, Sequence from typing import Any +from langgraph._internal._constants import RESERVED from langgraph.channels.base import BaseChannel -from langgraph.constants import RESERVED from langgraph.managed.base import ManagedValueMapping -from langgraph.pregel.read import PregelNode +from langgraph.pregel._read import PregelNode from langgraph.types import All diff --git a/libs/langgraph/langgraph/pregel/write.py b/libs/langgraph/langgraph/pregel/_write.py similarity index 97% rename from libs/langgraph/langgraph/pregel/write.py rename to libs/langgraph/langgraph/pregel/_write.py index 7f1fdc73f..6a6e4b612 100644 --- a/libs/langgraph/langgraph/pregel/write.py +++ b/libs/langgraph/langgraph/pregel/_write.py @@ -13,9 +13,11 @@ from typing import ( from langchain_core.runnables import Runnable, RunnableConfig -from langgraph.constants import CONF, CONFIG_KEY_SEND, MISSING, TASKS, Send +from langgraph._internal._constants import CONF, CONFIG_KEY_SEND, TASKS +from langgraph._internal._runnable import RunnableCallable +from langgraph._internal._typing import MISSING from langgraph.errors import InvalidUpdateError -from langgraph.utils.runnable import RunnableCallable +from langgraph.types import Send TYPE_SEND = Callable[[Sequence[tuple[str, Any]]], None] R = TypeVar("R", bound=Runnable) @@ -63,7 +65,6 @@ class ChannelWrite(RunnableCallable): name=None, tags=tags, trace=False, - func_accepts_config=True, ) self.writes = cast( list[Union[ChannelWriteEntry, ChannelWriteTupleEntry, Send]], writes diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index d8d3bbdae..d6fb1d630 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -5,24 +5,27 @@ from dataclasses import asdict from typing import Any from uuid import UUID +from langchain_core.runnables import RunnableConfig from typing_extensions import TypedDict -from langgraph.channels.base import BaseChannel -from langgraph.checkpoint.base import CheckpointMetadata, PendingWrite -from langgraph.constants import ( +from langgraph._internal._config import patch_checkpoint_map +from langgraph._internal._constants import ( CONF, CONFIG_KEY_CHECKPOINT_NS, ERROR, INTERRUPT, - MISSING, NS_END, NS_SEP, RETURN, - TAG_HIDDEN, ) -from langgraph.pregel.io import read_channels +from langgraph._internal._typing import MISSING +from langgraph.channels.base import BaseChannel +from langgraph.checkpoint.base import CheckpointMetadata, PendingWrite +from langgraph.constants import TAG_HIDDEN +from langgraph.pregel._io import read_channels from langgraph.types import PregelExecutableTask, PregelTask, StateSnapshot -from langgraph.utils.config import RunnableConfig, patch_checkpoint_map + +__all__ = ("TaskPayload", "TaskResultPayload", "CheckpointTask", "CheckpointPayload") class TaskPayload(TypedDict): diff --git a/libs/langgraph/langgraph/pregel/main.py b/libs/langgraph/langgraph/pregel/main.py new file mode 100644 index 000000000..1aec1da33 --- /dev/null +++ b/libs/langgraph/langgraph/pregel/main.py @@ -0,0 +1,3209 @@ +from __future__ import annotations + +import asyncio +import concurrent +import concurrent.futures +import queue +import warnings +import weakref +from collections import defaultdict, deque +from collections.abc import AsyncIterator, Iterator, Mapping, Sequence +from dataclasses import is_dataclass +from functools import partial +from inspect import isclass +from typing import Any, Callable, Generic, Optional, Union, cast, get_type_hints +from uuid import UUID, uuid5 + +from langchain_core.globals import get_debug +from langchain_core.runnables import ( + RunnableSequence, +) +from langchain_core.runnables.base import Input, Output +from langchain_core.runnables.config import ( + RunnableConfig, + get_async_callback_manager_for_config, + get_callback_manager_for_config, +) +from langchain_core.runnables.graph import Graph +from pydantic import BaseModel, TypeAdapter +from typing_extensions import Self, Unpack, deprecated, is_typeddict + +from langgraph._internal._config import ( + ensure_config, + merge_configs, + patch_checkpoint_map, + patch_config, + patch_configurable, + recast_checkpoint_ns, +) +from langgraph._internal._constants import ( + CACHE_NS_WRITES, + CONF, + CONFIG_KEY_CACHE, + CONFIG_KEY_CHECKPOINT_ID, + CONFIG_KEY_CHECKPOINT_NS, + CONFIG_KEY_CHECKPOINTER, + CONFIG_KEY_DURABILITY, + CONFIG_KEY_NODE_FINISHED, + CONFIG_KEY_READ, + CONFIG_KEY_RUNNER_SUBMIT, + CONFIG_KEY_RUNTIME, + CONFIG_KEY_SEND, + CONFIG_KEY_STREAM, + CONFIG_KEY_TASK_ID, + CONFIG_KEY_THREAD_ID, + ERROR, + INPUT, + INTERRUPT, + NS_END, + NS_SEP, + NULL_TASK_ID, + PUSH, + TASKS, +) +from langgraph._internal._pydantic import create_model +from langgraph._internal._queue import ( # type: ignore[attr-defined] + AsyncQueue, + SyncQueue, +) +from langgraph._internal._runnable import ( + Runnable, + RunnableLike, + RunnableSeq, + coerce_to_runnable, +) +from langgraph._internal._typing import DeprecatedKwargs +from langgraph.cache.base import BaseCache +from langgraph.channels.base import BaseChannel +from langgraph.channels.topic import Topic +from langgraph.checkpoint.base import ( + BaseCheckpointSaver, + Checkpoint, + CheckpointTuple, +) +from langgraph.config import get_config +from langgraph.constants import END +from langgraph.errors import ( + ErrorCode, + GraphRecursionError, + InvalidUpdateError, + create_error_message, +) +from langgraph.managed.base import ManagedValueSpec +from langgraph.pregel._algo import ( + PregelTaskWrites, + _scratchpad, + apply_writes, + local_read, + prepare_next_tasks, +) +from langgraph.pregel._call import identifier +from langgraph.pregel._checkpoint import ( + channels_from_checkpoint, + copy_checkpoint, + create_checkpoint, + empty_checkpoint, +) +from langgraph.pregel._draw import draw_graph +from langgraph.pregel._io import map_input, read_channels +from langgraph.pregel._loop import AsyncPregelLoop, SyncPregelLoop +from langgraph.pregel._messages import StreamMessagesHandler +from langgraph.pregel._read import DEFAULT_BOUND, PregelNode +from langgraph.pregel._retry import RetryPolicy +from langgraph.pregel._runner import PregelRunner +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.pregel.debug import get_bolded_text, get_colored_text, tasks_w_writes +from langgraph.pregel.protocol import PregelProtocol, StreamChunk, StreamProtocol +from langgraph.runtime import Runtime +from langgraph.store.base import BaseStore +from langgraph.types import ( + All, + CachePolicy, + Checkpointer, + Command, + Durability, + Interrupt, + Send, + StateSnapshot, + StateUpdate, + StreamMode, +) +from langgraph.typing import ContextT, InputT, OutputT, StateT +from langgraph.warnings import LangGraphDeprecatedSinceV10 + +try: + from langchain_core.tracers._streaming import _StreamingCallbackHandler +except ImportError: + _StreamingCallbackHandler = None # type: ignore + +__all__ = ("NodeBuilder", "Pregel") + +_WriteValue = Union[Callable[[Input], Output], Any] + + +class NodeBuilder: + __slots__ = ( + "_channels", + "_triggers", + "_tags", + "_metadata", + "_writes", + "_bound", + "_retry_policy", + "_cache_policy", + ) + + _channels: str | list[str] + _triggers: list[str] + _tags: list[str] + _metadata: dict[str, Any] + _writes: list[ChannelWriteEntry] + _bound: Runnable + _retry_policy: list[RetryPolicy] + _cache_policy: CachePolicy | None + + def __init__( + self, + ) -> None: + self._channels = [] + self._triggers = [] + self._tags = [] + self._metadata = {} + self._writes = [] + self._bound = DEFAULT_BOUND + self._retry_policy = [] + self._cache_policy = None + + def subscribe_only( + self, + channel: str, + ) -> Self: + """Subscribe to a single channel.""" + if not self._channels: + self._channels = channel + else: + raise ValueError( + "Cannot subscribe to single channels when other channels are already subscribed to" + ) + + self._triggers.append(channel) + + return self + + def subscribe_to( + self, + *channels: str, + read: bool = True, + ) -> Self: + """Add channels to subscribe to. Node will be invoked when any of these + channels are updated, with a dict of the channel values as input. + + Args: + channels: Channel name(s) to subscribe to + read: If True, the channels will be included in the input to the node. + Otherwise, they will trigger the node without being sent in input. + + Returns: + Self for chaining + """ + if isinstance(self._channels, str): + raise ValueError( + "Cannot subscribe to channels when subscribed to a single channel" + ) + if read: + if not self._channels: + self._channels = list(channels) + else: + self._channels.extend(channels) + + if isinstance(channels, str): + self._triggers.append(channels) + else: + self._triggers.extend(channels) + + return self + + def read_from( + self, + *channels: str, + ) -> Self: + """Adds the specified channels to read from, without subscribing to them.""" + assert isinstance(self._channels, list), ( + "Cannot read additional channels when subscribed to single channels" + ) + self._channels.extend(channels) + return self + + def do( + self, + node: RunnableLike, + ) -> Self: + """Adds the specified node.""" + if self._bound is not DEFAULT_BOUND: + self._bound = RunnableSeq( + self._bound, coerce_to_runnable(node, name=None, trace=True) + ) + else: + self._bound = coerce_to_runnable(node, name=None, trace=True) + return self + + def write_to( + self, + *channels: str | ChannelWriteEntry, + **kwargs: _WriteValue, + ) -> Self: + """Add channel writes. + + Args: + *channels: Channel names to write to + **kwargs: Channel name and value mappings + + Returns: + Self for chaining + """ + self._writes.extend( + ChannelWriteEntry(c) if isinstance(c, str) else c for c in channels + ) + self._writes.extend( + ChannelWriteEntry(k, mapper=v) + if callable(v) + else ChannelWriteEntry(k, value=v) + for k, v in kwargs.items() + ) + + return self + + def meta(self, *tags: str, **metadata: Any) -> Self: + """Add tags or metadata to the node.""" + self._tags.extend(tags) + self._metadata.update(metadata) + return self + + def add_retry_policies(self, *policies: RetryPolicy) -> Self: + """Adds retry policies to the node.""" + self._retry_policy.extend(policies) + return self + + def add_cache_policy(self, policy: CachePolicy) -> Self: + """Adds cache policies to the node.""" + self._cache_policy = policy + return self + + def build(self) -> PregelNode: + """Builds the node.""" + return PregelNode( + channels=self._channels, + triggers=self._triggers, + tags=self._tags, + metadata=self._metadata, + writers=[ChannelWrite(self._writes)], + bound=self._bound, + retry_policy=self._retry_policy, + cache_policy=self._cache_policy, + ) + + +class Pregel( + PregelProtocol[StateT, ContextT, InputT, OutputT], + Generic[StateT, ContextT, InputT, OutputT], +): + """Pregel manages the runtime behavior for LangGraph applications. + + ## Overview + + Pregel combines [**actors**](https://en.wikipedia.org/wiki/Actor_model) + and **channels** into a single application. + **Actors** read data from channels and write data to channels. + Pregel organizes the execution of the application into multiple steps, + following the **Pregel Algorithm**/**Bulk Synchronous Parallel** model. + + Each step consists of three phases: + + - **Plan**: Determine which **actors** to execute in this step. For example, + in the first step, select the **actors** that subscribe to the special + **input** channels; in subsequent steps, + select the **actors** that subscribe to channels updated in the previous step. + - **Execution**: Execute all selected **actors** in parallel, + until all complete, or one fails, or a timeout is reached. During this + phase, channel updates are invisible to actors until the next step. + - **Update**: Update the channels with the values written by the **actors** + in this step. + + Repeat until no **actors** are selected for execution, or a maximum number of + steps is reached. + + ## Actors + + An **actor** is a `PregelNode`. + It subscribes to channels, reads data from them, and writes data to them. + It can be thought of as an **actor** in the Pregel algorithm. + `PregelNodes` implement LangChain's + Runnable interface. + + ## Channels + + Channels are used to communicate between actors (`PregelNodes`). + Each channel has a value type, an update type, and an update function – which + takes a sequence of updates and + modifies the stored value. Channels can be used to send data from one chain to + another, or to send data from a chain to itself in a future step. LangGraph + provides a number of built-in channels: + + ### Basic channels: LastValue and Topic + + - `LastValue`: The default channel, stores the last value sent to the channel, + useful for input and output values, or for sending data from one step to the next + - `Topic`: A configurable PubSub Topic, useful for sending multiple values + between *actors*, or for accumulating output. Can be configured to deduplicate + values, and/or to accumulate values over the course of multiple steps. + + ### Advanced channels: Context and BinaryOperatorAggregate + + - `Context`: exposes the value of a context manager, managing its lifecycle. + Useful for accessing external resources that require setup and/or teardown. eg. + `client = Context(httpx.Client)` + - `BinaryOperatorAggregate`: stores a persistent value, updated by applying + a binary operator to the current value and each update + sent to the channel, useful for computing aggregates over multiple steps. eg. + `total = BinaryOperatorAggregate(int, operator.add)` + + ## Examples + + Most users will interact with Pregel via a + [StateGraph (Graph API)][langgraph.graph.StateGraph] or via an + [entrypoint (Functional API)][langgraph.func.entrypoint]. + + However, for **advanced** use cases, Pregel can be used directly. If you're + not sure whether you need to use Pregel directly, then the answer is probably no + – you should use the Graph API or Functional API instead. These are higher-level + interfaces that will compile down to Pregel under the hood. + + Here are some examples to give you a sense of how it works: + + Example: Single node application + ```python + from langgraph.channels import EphemeralValue + from langgraph.pregel import Pregel, NodeBuilder + + node1 = ( + NodeBuilder().subscribe_only("a") + .do(lambda x: x + x) + .write_to("b") + ) + + app = Pregel( + nodes={"node1": node1}, + channels={ + "a": EphemeralValue(str), + "b": EphemeralValue(str), + }, + input_channels=["a"], + output_channels=["b"], + ) + + app.invoke({"a": "foo"}) + ``` + + ```con + {'b': 'foofoo'} + ``` + + Example: Using multiple nodes and multiple output channels + ```python + from langgraph.channels import LastValue, EphemeralValue + from langgraph.pregel import Pregel, NodeBuilder + + node1 = ( + NodeBuilder().subscribe_only("a") + .do(lambda x: x + x) + .write_to("b") + ) + + node2 = ( + NodeBuilder().subscribe_to("b") + .do(lambda x: x["b"] + x["b"]) + .write_to("c") + ) + + + app = Pregel( + nodes={"node1": node1, "node2": node2}, + channels={ + "a": EphemeralValue(str), + "b": LastValue(str), + "c": EphemeralValue(str), + }, + input_channels=["a"], + output_channels=["b", "c"], + ) + + app.invoke({"a": "foo"}) + ``` + + ```con + {'b': 'foofoo', 'c': 'foofoofoofoo'} + ``` + + Example: Using a Topic channel + ```python + from langgraph.channels import LastValue, EphemeralValue, Topic + from langgraph.pregel import Pregel, NodeBuilder + + node1 = ( + NodeBuilder().subscribe_only("a") + .do(lambda x: x + x) + .write_to("b", "c") + ) + + node2 = ( + NodeBuilder().subscribe_only("b") + .do(lambda x: x + x) + .write_to("c") + ) + + + app = Pregel( + nodes={"node1": node1, "node2": node2}, + channels={ + "a": EphemeralValue(str), + "b": EphemeralValue(str), + "c": Topic(str, accumulate=True), + }, + input_channels=["a"], + output_channels=["c"], + ) + + app.invoke({"a": "foo"}) + ``` + + ```pycon + {'c': ['foofoo', 'foofoofoofoo']} + ``` + + Example: Using a BinaryOperatorAggregate channel + ```python + from langgraph.channels import EphemeralValue, BinaryOperatorAggregate + from langgraph.pregel import Pregel, NodeBuilder + + + node1 = ( + NodeBuilder().subscribe_only("a") + .do(lambda x: x + x) + .write_to("b", "c") + ) + + node2 = ( + NodeBuilder().subscribe_only("b") + .do(lambda x: x + x) + .write_to("c") + ) + + + def reducer(current, update): + if current: + return current + " | " + update + else: + return update + + app = Pregel( + nodes={"node1": node1, "node2": node2}, + channels={ + "a": EphemeralValue(str), + "b": EphemeralValue(str), + "c": BinaryOperatorAggregate(str, operator=reducer), + }, + input_channels=["a"], + output_channels=["c"] + ) + + app.invoke({"a": "foo"}) + ``` + + ```con + {'c': 'foofoo | foofoofoofoo'} + ``` + + Example: Introducing a cycle + This example demonstrates how to introduce a cycle in the graph, by having + a chain write to a channel it subscribes to. Execution will continue + until a None value is written to the channel. + + ```python + from langgraph.channels import EphemeralValue + from langgraph.pregel import Pregel, NodeBuilder, ChannelWriteEntry + + example_node = ( + NodeBuilder().subscribe_only("value") + .do(lambda x: x + x if len(x) < 10 else None) + .write_to(ChannelWriteEntry(channel="value", skip_none=True)) + ) + + app = Pregel( + nodes={"example_node": example_node}, + channels={ + "value": EphemeralValue(str), + }, + input_channels=["value"], + output_channels=["value"] + ) + + app.invoke({"value": "a"}) + ``` + + ```con + {'value': 'aaaaaaaaaaaaaaaa'} + ``` + """ + + nodes: dict[str, PregelNode] + + channels: dict[str, BaseChannel | ManagedValueSpec] + + stream_mode: StreamMode = "values" + """Mode to stream output, defaults to 'values'.""" + + stream_eager: bool = False + """Whether to force emitting stream events eagerly, automatically turned on + for stream_mode "messages" and "custom".""" + + output_channels: str | Sequence[str] + + stream_channels: str | Sequence[str] | None = None + """Channels to stream, defaults to all channels not in reserved channels""" + + interrupt_after_nodes: All | Sequence[str] + + interrupt_before_nodes: All | Sequence[str] + + input_channels: str | Sequence[str] + + step_timeout: float | None = None + """Maximum time to wait for a step to complete, in seconds. Defaults to None.""" + + debug: bool + """Whether to print debug information during execution. Defaults to False.""" + + checkpointer: Checkpointer = None + """Checkpointer used to save and load graph state. Defaults to None.""" + + store: BaseStore | None = None + """Memory store to use for SharedValues. Defaults to None.""" + + cache: BaseCache | None = None + """Cache to use for storing node results. Defaults to None.""" + + retry_policy: Sequence[RetryPolicy] = () + """Retry policies to use when running tasks. Empty set disables retries.""" + + cache_policy: CachePolicy | None = None + """Cache policy to use for all nodes. Can be overridden by individual nodes. + Defaults to None.""" + + context_schema: type[ContextT] | None = None + + config: RunnableConfig | None = None + + name: str = "LangGraph" + + trigger_to_nodes: Mapping[str, Sequence[str]] + + def __init__( + self, + *, + nodes: dict[str, PregelNode | NodeBuilder], + channels: dict[str, BaseChannel | ManagedValueSpec] | None, + auto_validate: bool = True, + stream_mode: StreamMode = "values", + stream_eager: bool = False, + output_channels: str | Sequence[str], + stream_channels: str | Sequence[str] | None = None, + interrupt_after_nodes: All | Sequence[str] = (), + interrupt_before_nodes: All | Sequence[str] = (), + input_channels: str | Sequence[str], + step_timeout: float | None = None, + debug: bool | None = None, + checkpointer: BaseCheckpointSaver | None = None, + store: BaseStore | None = None, + cache: BaseCache | None = None, + retry_policy: RetryPolicy | Sequence[RetryPolicy] = (), + cache_policy: CachePolicy | None = None, + context_schema: type[ContextT] | None = None, + config: RunnableConfig | None = None, + trigger_to_nodes: Mapping[str, Sequence[str]] | None = None, + name: str = "LangGraph", + **deprecated_kwargs: Unpack[DeprecatedKwargs], + ) -> None: + if config_type := deprecated_kwargs.get("config_type"): + warnings.warn( + "`config_type` is deprecated and will be removed. Please use `context_schema` instead.", + category=LangGraphDeprecatedSinceV10, + stacklevel=2, + ) + + if context_schema is None: + context_schema = cast(type[ContextT], config_type) + + self.nodes = { + k: v.build() if isinstance(v, NodeBuilder) else v for k, v in nodes.items() + } + self.channels = channels or {} + if TASKS in self.channels and not isinstance(self.channels[TASKS], Topic): + raise ValueError( + f"Channel '{TASKS}' is reserved and cannot be used in the graph." + ) + else: + self.channels[TASKS] = Topic(Send, accumulate=False) + self.stream_mode = stream_mode + self.stream_eager = stream_eager + 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.cache = cache + self.retry_policy = ( + (retry_policy,) if isinstance(retry_policy, RetryPolicy) else retry_policy + ) + self.cache_policy = cache_policy + self.context_schema = context_schema + self.config = config + self.trigger_to_nodes = trigger_to_nodes or {} + self.name = name + if auto_validate: + self.validate() + + def get_graph( + self, config: RunnableConfig | None = None, *, xray: int | bool = False + ) -> Graph: + """Return a drawable representation of the computation graph.""" + # gather subgraphs + if xray: + subgraphs = { + k: v.get_graph( + config, + xray=xray if isinstance(xray, bool) or xray <= 0 else xray - 1, + ) + for k, v in self.get_subgraphs() + } + else: + subgraphs = {} + + return draw_graph( + merge_configs(self.config, config), + nodes=self.nodes, + specs=self.channels, + input_channels=self.input_channels, + interrupt_after_nodes=self.interrupt_after_nodes, + interrupt_before_nodes=self.interrupt_before_nodes, + trigger_to_nodes=self.trigger_to_nodes, + checkpointer=self.checkpointer, + subgraphs=subgraphs, + ) + + async def aget_graph( + self, config: RunnableConfig | None = None, *, xray: int | bool = False + ) -> Graph: + """Return a drawable representation of the computation graph.""" + + # gather subgraphs + if xray: + subpregels: dict[str, PregelProtocol] = { + k: v async for k, v in self.aget_subgraphs() + } + subgraphs = { + k: v + for k, v in zip( + subpregels, + await asyncio.gather( + *( + p.aget_graph( + config, + xray=xray + if isinstance(xray, bool) or xray <= 0 + else xray - 1, + ) + for p in subpregels.values() + ) + ), + ) + } + else: + subgraphs = {} + + return draw_graph( + merge_configs(self.config, config), + nodes=self.nodes, + specs=self.channels, + input_channels=self.input_channels, + interrupt_after_nodes=self.interrupt_after_nodes, + interrupt_before_nodes=self.interrupt_before_nodes, + trigger_to_nodes=self.trigger_to_nodes, + checkpointer=self.checkpointer, + subgraphs=subgraphs, + ) + + def _repr_mimebundle_(self, **kwargs: Any) -> dict[str, Any]: + """Mime bundle used by Jupyter to display the graph""" + return { + "text/plain": repr(self), + "image/png": self.get_graph().draw_mermaid_png(), + } + + def copy(self, update: dict[str, Any] | None = None) -> Self: + attrs = {k: v for k, v in self.__dict__.items() if k != "__orig_class__"} + attrs.update(update or {}) + return self.__class__(**attrs) + + def with_config(self, config: RunnableConfig | None = None, **kwargs: Any) -> Self: + """Create a copy of the Pregel object with an updated config.""" + return self.copy( + {"config": merge_configs(self.config, config, cast(RunnableConfig, kwargs))} + ) + + def validate(self) -> Self: + validate_graph( + self.nodes, + {k: v for k, v in self.channels.items() if isinstance(v, BaseChannel)}, + {k: v for k, v in self.channels.items() if not isinstance(v, BaseChannel)}, + self.input_channels, + self.output_channels, + self.stream_channels, + self.interrupt_after_nodes, + self.interrupt_before_nodes, + ) + self.trigger_to_nodes = _trigger_to_nodes(self.nodes) + return self + + @deprecated( + "`config_schema` is deprecated. Use `get_context_jsonschema` for the relevant schema instead." + ) + def config_schema(self, *, include: Sequence[str] | None = None) -> type[BaseModel]: + warnings.warn( + "`config_schema` is deprecated. Use `get_context_jsonschema` for the relevant schema instead.", + category=LangGraphDeprecatedSinceV10, + stacklevel=2, + ) + + include = include or [] + fields = { + **( + {"configurable": (self.context_schema, None)} + if self.context_schema + else {} + ), + **{ + field_name: (field_type, None) + for field_name, field_type in get_type_hints(RunnableConfig).items() + if field_name in [i for i in include if i != "configurable"] + }, + } + return create_model(self.get_name("Config"), field_definitions=fields) + + @deprecated( + "`get_config_jsonschema` is deprecated. Use `get_context_jsonschema` instead." + ) + def get_config_jsonschema( + self, *, include: Sequence[str] | None = None + ) -> dict[str, Any]: + warnings.warn( + "`get_config_jsonschema` is deprecated. Use `get_context_jsonschema` instead.", + category=LangGraphDeprecatedSinceV10, + stacklevel=2, + ) + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=LangGraphDeprecatedSinceV10) + schema = self.config_schema(include=include) + return schema.model_json_schema() + + def get_context_jsonschema(self) -> dict[str, Any] | None: + if (context_schema := self.context_schema) is None: + return None + + if isclass(context_schema) and issubclass(context_schema, BaseModel): + return context_schema.model_json_schema() + elif is_typeddict(context_schema) or is_dataclass(context_schema): + return TypeAdapter(context_schema).json_schema() + else: + raise ValueError( + f"Invalid context schema type: {context_schema}. Must be a BaseModel, TypedDict or dataclass." + ) + + @property + def InputType(self) -> Any: + if isinstance(self.input_channels, str): + channel = self.channels[self.input_channels] + if isinstance(channel, BaseChannel): + return channel.UpdateType + + def get_input_schema(self, config: RunnableConfig | None = None) -> type[BaseModel]: + config = merge_configs(self.config, config) + if isinstance(self.input_channels, str): + return super().get_input_schema(config) + else: + return create_model( + self.get_name("Input"), + field_definitions={ + k: (c.UpdateType, None) + for k in self.input_channels or self.channels.keys() + if (c := self.channels[k]) and isinstance(c, BaseChannel) + }, + ) + + def get_input_jsonschema( + self, config: RunnableConfig | None = None + ) -> dict[str, Any]: + schema = self.get_input_schema(config) + return schema.model_json_schema() + + @property + def OutputType(self) -> Any: + if isinstance(self.output_channels, str): + channel = self.channels[self.output_channels] + if isinstance(channel, BaseChannel): + return channel.ValueType + + def get_output_schema( + self, config: RunnableConfig | None = None + ) -> type[BaseModel]: + config = merge_configs(self.config, config) + if isinstance(self.output_channels, str): + return super().get_output_schema(config) + else: + return create_model( + self.get_name("Output"), + field_definitions={ + k: (c.ValueType, None) + for k in self.output_channels + if (c := self.channels[k]) and isinstance(c, BaseChannel) + }, + ) + + def get_output_jsonschema( + self, config: RunnableConfig | None = None + ) -> dict[str, Any]: + schema = self.get_output_schema(config) + return schema.model_json_schema() + + @property + def stream_channels_list(self) -> Sequence[str]: + stream_channels = self.stream_channels_asis + return ( + [stream_channels] if isinstance(stream_channels, str) else stream_channels + ) + + @property + def stream_channels_asis(self) -> str | Sequence[str]: + return self.stream_channels or [ + k for k in self.channels if isinstance(self.channels[k], BaseChannel) + ] + + def get_subgraphs( + self, *, namespace: str | None = None, recurse: bool = False + ) -> Iterator[tuple[str, PregelProtocol]]: + """Get the subgraphs of the graph. + + Args: + namespace: The namespace to filter the subgraphs by. + recurse: Whether to recurse into the subgraphs. + If False, only the immediate subgraphs will be returned. + + Returns: + Iterator[tuple[str, PregelProtocol]]: An iterator of the (namespace, subgraph) pairs. + """ + for name, node in self.nodes.items(): + # filter by prefix + if namespace is not None: + if not namespace.startswith(name): + continue + + # find the subgraph, if any + graph = node.subgraphs[0] if node.subgraphs else None + + # if found, yield recursively + if graph: + if name == namespace: + yield name, graph + return # we found it, stop searching + if namespace is None: + yield name, graph + if recurse and isinstance(graph, Pregel): + if namespace is not None: + namespace = namespace[len(name) + 1 :] + yield from ( + (f"{name}{NS_SEP}{n}", s) + for n, s in graph.get_subgraphs( + namespace=namespace, recurse=recurse + ) + ) + + async def aget_subgraphs( + self, *, namespace: str | None = None, recurse: bool = False + ) -> AsyncIterator[tuple[str, PregelProtocol]]: + """Get the subgraphs of the graph. + + Args: + namespace: The namespace to filter the subgraphs by. + recurse: Whether to recurse into the subgraphs. + If False, only the immediate subgraphs will be returned. + + Returns: + AsyncIterator[tuple[str, PregelProtocol]]: An iterator of the (namespace, subgraph) pairs. + """ + for name, node in self.get_subgraphs(namespace=namespace, recurse=recurse): + yield name, node + + def _migrate_checkpoint(self, checkpoint: Checkpoint) -> None: + """Migrate a saved checkpoint to new channel layout.""" + if checkpoint["v"] < 4 and checkpoint.get("pending_sends"): + pending_sends: list[Send] = checkpoint.pop("pending_sends") + checkpoint["channel_values"][TASKS] = pending_sends + checkpoint["channel_versions"][TASKS] = max( + checkpoint["channel_versions"].values() + ) + + def _prepare_state_snapshot( + self, + config: RunnableConfig, + saved: CheckpointTuple | None, + recurse: BaseCheckpointSaver | None = None, + apply_pending_writes: bool = False, + ) -> StateSnapshot: + if not saved: + return StateSnapshot( + values={}, + next=(), + config=config, + metadata=None, + created_at=None, + parent_config=None, + tasks=(), + interrupts=(), + ) + + # migrate checkpoint if needed + self._migrate_checkpoint(saved.checkpoint) + + step = saved.metadata.get("step", -1) + 1 + stop = step + 2 + channels, managed = channels_from_checkpoint( + self.channels, + saved.checkpoint, + ) + # tasks for this checkpoint + next_tasks = prepare_next_tasks( + saved.checkpoint, + saved.pending_writes or [], + self.nodes, + channels, + managed, + saved.config, + step, + stop, + for_execution=True, + store=self.store, + checkpointer=( + self.checkpointer + if isinstance(self.checkpointer, BaseCheckpointSaver) + else None + ), + manager=None, + ) + # get the subgraphs + subgraphs = dict(self.get_subgraphs()) + parent_ns = saved.config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") + task_states: dict[str, RunnableConfig | StateSnapshot] = {} + for task in next_tasks.values(): + 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 = { + CONF: { + "thread_id": saved.config[CONF]["thread_id"], + CONFIG_KEY_CHECKPOINT_NS: task_ns, + } + } + task_states[task.id] = config + else: + # get the state of the subgraph + config = { + CONF: { + CONFIG_KEY_CHECKPOINTER: recurse, + "thread_id": saved.config[CONF]["thread_id"], + CONFIG_KEY_CHECKPOINT_NS: task_ns, + } + } + task_states[task.id] = subgraphs[task.name].get_state( + config, subgraphs=True + ) + # apply pending writes + if null_writes := [ + w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID + ]: + apply_writes( + saved.checkpoint, + channels, + [PregelTaskWrites((), INPUT, null_writes, [])], + None, + self.trigger_to_nodes, + ) + if apply_pending_writes and saved.pending_writes: + for tid, k, v in saved.pending_writes: + if k in (ERROR, INTERRUPT): + continue + if tid not in next_tasks: + continue + next_tasks[tid].writes.append((k, v)) + if tasks := [t for t in next_tasks.values() if t.writes]: + apply_writes( + saved.checkpoint, channels, tasks, None, self.trigger_to_nodes + ) + tasks_with_writes = tasks_w_writes( + next_tasks.values(), + saved.pending_writes, + task_states, + self.stream_channels_asis, + ) + # assemble the state snapshot + return StateSnapshot( + read_channels(channels, self.stream_channels_asis), + tuple(t.name for t in next_tasks.values() if not t.writes), + patch_checkpoint_map(saved.config, saved.metadata), + saved.metadata, + saved.checkpoint["ts"], + patch_checkpoint_map(saved.parent_config, saved.metadata), + tasks_with_writes, + tuple([i for task in tasks_with_writes for i in task.interrupts]), + ) + + async def _aprepare_state_snapshot( + self, + config: RunnableConfig, + saved: CheckpointTuple | None, + recurse: BaseCheckpointSaver | None = None, + apply_pending_writes: bool = False, + ) -> StateSnapshot: + if not saved: + return StateSnapshot( + values={}, + next=(), + config=config, + metadata=None, + created_at=None, + parent_config=None, + tasks=(), + interrupts=(), + ) + + # migrate checkpoint if needed + self._migrate_checkpoint(saved.checkpoint) + + step = saved.metadata.get("step", -1) + 1 + stop = step + 2 + channels, managed = channels_from_checkpoint( + self.channels, + saved.checkpoint, + ) + # tasks for this checkpoint + next_tasks = prepare_next_tasks( + saved.checkpoint, + saved.pending_writes or [], + self.nodes, + channels, + managed, + saved.config, + step, + stop, + for_execution=True, + store=self.store, + checkpointer=( + self.checkpointer + if isinstance(self.checkpointer, BaseCheckpointSaver) + else None + ), + manager=None, + ) + # get the subgraphs + subgraphs = {n: g async for n, g in self.aget_subgraphs()} + parent_ns = saved.config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") + task_states: dict[str, RunnableConfig | StateSnapshot] = {} + for task in next_tasks.values(): + 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 = { + CONF: { + "thread_id": saved.config[CONF]["thread_id"], + CONFIG_KEY_CHECKPOINT_NS: task_ns, + } + } + task_states[task.id] = config + else: + # get the state of the subgraph + config = { + CONF: { + CONFIG_KEY_CHECKPOINTER: recurse, + "thread_id": saved.config[CONF]["thread_id"], + CONFIG_KEY_CHECKPOINT_NS: task_ns, + } + } + task_states[task.id] = await subgraphs[task.name].aget_state( + config, subgraphs=True + ) + # apply pending writes + if null_writes := [ + w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID + ]: + apply_writes( + saved.checkpoint, + channels, + [PregelTaskWrites((), INPUT, null_writes, [])], + None, + self.trigger_to_nodes, + ) + if apply_pending_writes and saved.pending_writes: + for tid, k, v in saved.pending_writes: + if k in (ERROR, INTERRUPT): + continue + if tid not in next_tasks: + continue + next_tasks[tid].writes.append((k, v)) + if tasks := [t for t in next_tasks.values() if t.writes]: + apply_writes( + saved.checkpoint, channels, tasks, None, self.trigger_to_nodes + ) + + tasks_with_writes = tasks_w_writes( + next_tasks.values(), + saved.pending_writes, + task_states, + self.stream_channels_asis, + ) + # assemble the state snapshot + return StateSnapshot( + read_channels(channels, self.stream_channels_asis), + tuple(t.name for t in next_tasks.values() if not t.writes), + patch_checkpoint_map(saved.config, saved.metadata), + saved.metadata, + saved.checkpoint["ts"], + patch_checkpoint_map(saved.parent_config, saved.metadata), + tasks_with_writes, + tuple([i for task in tasks_with_writes for i in task.interrupts]), + ) + + def get_state( + self, config: RunnableConfig, *, subgraphs: bool = False + ) -> StateSnapshot: + """Get the current state of the graph.""" + checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: + raise ValueError("No checkpointer set") + + if ( + checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") + ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: + # remove task_ids from checkpoint_ns + recast = recast_checkpoint_ns(checkpoint_ns) + # find the subgraph with the matching name + for _, pregel in self.get_subgraphs(namespace=recast, recurse=True): + return pregel.get_state( + patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), + subgraphs=subgraphs, + ) + else: + raise ValueError(f"Subgraph {recast} not found") + + config = merge_configs(self.config, config) if self.config else config + if self.checkpointer is True: + ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS]) + config = merge_configs( + config, {CONF: {CONFIG_KEY_CHECKPOINT_NS: recast_checkpoint_ns(ns)}} + ) + thread_id = config[CONF][CONFIG_KEY_THREAD_ID] + if not isinstance(thread_id, str): + config[CONF][CONFIG_KEY_THREAD_ID] = str(thread_id) + + saved = checkpointer.get_tuple(config) + return self._prepare_state_snapshot( + config, + saved, + recurse=checkpointer if subgraphs else None, + apply_pending_writes=CONFIG_KEY_CHECKPOINT_ID not in config[CONF], + ) + + async def aget_state( + self, config: RunnableConfig, *, subgraphs: bool = False + ) -> StateSnapshot: + """Get the current state of the graph.""" + checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: + raise ValueError("No checkpointer set") + + if ( + checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") + ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: + # remove task_ids from checkpoint_ns + recast = recast_checkpoint_ns(checkpoint_ns) + # find the subgraph with the matching name + async for _, pregel in self.aget_subgraphs(namespace=recast, recurse=True): + return await pregel.aget_state( + patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), + subgraphs=subgraphs, + ) + else: + raise ValueError(f"Subgraph {recast} not found") + + config = merge_configs(self.config, config) if self.config else config + if self.checkpointer is True: + ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS]) + config = merge_configs( + config, {CONF: {CONFIG_KEY_CHECKPOINT_NS: recast_checkpoint_ns(ns)}} + ) + thread_id = config[CONF][CONFIG_KEY_THREAD_ID] + if not isinstance(thread_id, str): + config[CONF][CONFIG_KEY_THREAD_ID] = str(thread_id) + + saved = await checkpointer.aget_tuple(config) + return await self._aprepare_state_snapshot( + config, + saved, + recurse=checkpointer if subgraphs else None, + apply_pending_writes=CONFIG_KEY_CHECKPOINT_ID not in config[CONF], + ) + + def get_state_history( + self, + config: RunnableConfig, + *, + filter: dict[str, Any] | None = None, + before: RunnableConfig | None = None, + limit: int | None = None, + ) -> Iterator[StateSnapshot]: + """Get the history of the state of the graph.""" + config = ensure_config(config) + checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: + raise ValueError("No checkpointer set") + + if ( + checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") + ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: + # remove task_ids from checkpoint_ns + recast = recast_checkpoint_ns(checkpoint_ns) + # find the subgraph with the matching name + for _, pregel in self.get_subgraphs(namespace=recast, recurse=True): + yield from pregel.get_state_history( + patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), + filter=filter, + before=before, + limit=limit, + ) + return + else: + raise ValueError(f"Subgraph {recast} not found") + + config = merge_configs( + self.config, + config, + { + CONF: { + CONFIG_KEY_CHECKPOINT_NS: checkpoint_ns, + CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID]), + } + }, + ) + # eagerly consume list() to avoid holding up the db cursor + for checkpoint_tuple in list( + checkpointer.list(config, before=before, limit=limit, filter=filter) + ): + yield self._prepare_state_snapshot( + checkpoint_tuple.config, checkpoint_tuple + ) + + async def aget_state_history( + self, + config: RunnableConfig, + *, + filter: dict[str, Any] | None = None, + before: RunnableConfig | None = None, + limit: int | None = None, + ) -> AsyncIterator[StateSnapshot]: + """Asynchronously get the history of the state of the graph.""" + config = ensure_config(config) + checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: + raise ValueError("No checkpointer set") + + if ( + checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") + ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: + # remove task_ids from checkpoint_ns + recast = recast_checkpoint_ns(checkpoint_ns) + # find the subgraph with the matching name + async for _, pregel in self.aget_subgraphs(namespace=recast, recurse=True): + async for state in pregel.aget_state_history( + patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), + filter=filter, + before=before, + limit=limit, + ): + yield state + return + else: + raise ValueError(f"Subgraph {recast} not found") + + config = merge_configs( + self.config, + config, + { + CONF: { + CONFIG_KEY_CHECKPOINT_NS: checkpoint_ns, + CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID]), + } + }, + ) + # 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 + ) + + def bulk_update_state( + self, + config: RunnableConfig, + supersteps: Sequence[Sequence[StateUpdate]], + ) -> RunnableConfig: + """Apply updates to the graph state in bulk. Requires a checkpointer to be set. + + Args: + config: The config to apply the updates to. + supersteps: A list of supersteps, each including a list of updates to apply sequentially to a graph state. + Each update is a tuple of the form `(values, as_node, task_id)` where task_id is optional. + + Raises: + ValueError: If no checkpointer is set or no updates are provided. + InvalidUpdateError: If an invalid update is provided. + + Returns: + RunnableConfig: The updated config. + """ + + checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: + raise ValueError("No checkpointer set") + + if len(supersteps) == 0: + raise ValueError("No supersteps provided") + + if any(len(u) == 0 for u in supersteps): + raise ValueError("No updates provided") + + # delegate to subgraph + if ( + checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") + ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: + # remove task_ids from checkpoint_ns + recast = recast_checkpoint_ns(checkpoint_ns) + # find the subgraph with the matching name + for _, pregel in self.get_subgraphs(namespace=recast, recurse=True): + return pregel.bulk_update_state( + patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), + supersteps, + ) + else: + raise ValueError(f"Subgraph {recast} not found") + + def perform_superstep( + input_config: RunnableConfig, updates: Sequence[StateUpdate] + ) -> RunnableConfig: + # get last checkpoint + config = ensure_config(self.config, input_config) + saved = checkpointer.get_tuple(config) + if saved is not None: + self._migrate_checkpoint(saved.checkpoint) + checkpoint = ( + copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() + ) + checkpoint_previous_versions = ( + 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 + checkpoint_config = patch_configurable( + config, + { + CONFIG_KEY_CHECKPOINT_NS: config[CONF].get( + CONFIG_KEY_CHECKPOINT_NS, "" + ) + }, + ) + if saved: + checkpoint_config = patch_configurable(config, saved.config[CONF]) + channels, managed = channels_from_checkpoint( + self.channels, + checkpoint, + ) + values, as_node = updates[0][:2] + + # no values as END, just clear all tasks + if values is None and as_node == END: + if len(updates) > 1: + raise InvalidUpdateError( + "Cannot apply multiple updates when clearing state" + ) + + if saved is not None: + # tasks for this checkpoint + next_tasks = prepare_next_tasks( + checkpoint, + saved.pending_writes or [], + self.nodes, + channels, + managed, + saved.config, + step + 1, + step + 3, + for_execution=True, + store=self.store, + checkpointer=checkpointer, + manager=None, + ) + # apply null writes + if null_writes := [ + w[1:] + for w in saved.pending_writes or [] + if w[0] == NULL_TASK_ID + ]: + apply_writes( + checkpoint, + channels, + [PregelTaskWrites((), INPUT, null_writes, [])], + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + # apply writes from tasks that already ran + for tid, k, v in saved.pending_writes or []: + if k in (ERROR, INTERRUPT): + continue + if tid not in next_tasks: + continue + next_tasks[tid].writes.append((k, v)) + # clear all current tasks + apply_writes( + checkpoint, + channels, + next_tasks.values(), + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + # save checkpoint + next_config = checkpointer.put( + checkpoint_config, + create_checkpoint(checkpoint, channels, step), + { + "source": "update", + "step": step + 1, + "parents": saved.metadata.get("parents", {}) if saved else {}, + }, + get_new_channel_versions( + checkpoint_previous_versions, + checkpoint["channel_versions"], + ), + ) + return patch_checkpoint_map( + next_config, saved.metadata if saved else None + ) + + # act as an input + if as_node == INPUT: + if len(updates) > 1: + raise InvalidUpdateError( + "Cannot apply multiple updates when updating as input" + ) + + if input_writes := deque(map_input(self.input_channels, values)): + apply_writes( + checkpoint, + channels, + [PregelTaskWrites((), INPUT, input_writes, [])], + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + + # apply input write to channels + next_step = ( + step + 1 + if saved and saved.metadata.get("step") is not None + else -1 + ) + next_config = checkpointer.put( + checkpoint_config, + create_checkpoint(checkpoint, channels, next_step), + { + "source": "input", + "step": next_step, + "parents": saved.metadata.get("parents", {}) + if saved + else {}, + }, + get_new_channel_versions( + checkpoint_previous_versions, + checkpoint["channel_versions"], + ), + ) + + # store the writes + checkpointer.put_writes( + next_config, + input_writes, + str(uuid5(UUID(checkpoint["id"]), INPUT)), + ) + + return patch_checkpoint_map( + next_config, saved.metadata if saved else None + ) + else: + raise InvalidUpdateError( + f"Received no input writes for {self.input_channels}" + ) + + # copy checkpoint + if as_node == "__copy__": + if len(updates) > 1: + raise InvalidUpdateError( + "Cannot copy checkpoint with multiple updates" + ) + + if saved is None: + raise InvalidUpdateError("Cannot copy a non-existent checkpoint") + + next_checkpoint = create_checkpoint(checkpoint, None, step) + + # copy checkpoint + next_config = checkpointer.put( + saved.parent_config + or patch_configurable( + saved.config, {CONFIG_KEY_CHECKPOINT_ID: None} + ), + next_checkpoint, + { + "source": "fork", + "step": step + 1, + "parents": saved.metadata.get("parents", {}), + }, + {}, + ) + + # we want to both clone a checkpoint and update state in one go. + # reuse the same task ID if possible. + if isinstance(values, list) and len(values) > 0: + # figure out the task IDs for the next update checkpoint + next_tasks = prepare_next_tasks( + next_checkpoint, + saved.pending_writes or [], + self.nodes, + channels, + managed, + next_config, + step + 2, + step + 4, + for_execution=True, + store=self.store, + checkpointer=checkpointer, + manager=None, + ) + + tasks_group_by = defaultdict(list) + user_group_by: dict[str, list[StateUpdate]] = defaultdict(list) + + for task in next_tasks.values(): + tasks_group_by[task.name].append(task.id) + + for item in values: + if not isinstance(item, Sequence): + raise InvalidUpdateError( + f"Invalid update item: {item} when copying checkpoint" + ) + + values, as_node = item[:2] + + user_group = user_group_by[as_node] + tasks_group = tasks_group_by[as_node] + + target_idx = len(user_group) + task_id = ( + tasks_group[target_idx] + if target_idx < len(tasks_group) + else None + ) + + user_group_by[as_node].append( + StateUpdate(values=values, as_node=as_node, task_id=task_id) + ) + + return perform_superstep( + patch_checkpoint_map(next_config, saved.metadata), + [item for lst in user_group_by.values() for item in lst], + ) + + return patch_checkpoint_map(next_config, saved.metadata) + + # apply pending writes, if not on specific checkpoint + if ( + CONFIG_KEY_CHECKPOINT_ID not in config[CONF] + and saved is not None + and saved.pending_writes + ): + # tasks for this checkpoint + next_tasks = prepare_next_tasks( + checkpoint, + saved.pending_writes, + self.nodes, + channels, + managed, + saved.config, + step + 1, + step + 3, + for_execution=True, + store=self.store, + checkpointer=checkpointer, + manager=None, + ) + # apply null writes + if null_writes := [ + w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID + ]: + apply_writes( + checkpoint, + channels, + [PregelTaskWrites((), INPUT, null_writes, [])], + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + # apply writes + for tid, k, v in saved.pending_writes: + if k in (ERROR, INTERRUPT): + continue + if tid not in next_tasks: + continue + next_tasks[tid].writes.append((k, v)) + if tasks := [t for t in next_tasks.values() if t.writes]: + apply_writes( + checkpoint, + channels, + tasks, + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + valid_updates: list[tuple[str, dict[str, Any] | None, str | None]] = [] + if len(updates) == 1: + values, as_node, task_id = updates[0] + # find last node that updated the state, if not provided + if as_node is None and len(self.nodes) == 1: + as_node = tuple(self.nodes)[0] + elif as_node is None and not any( + v + for vv in checkpoint["versions_seen"].values() + for v in vv.values() + ): + if ( + isinstance(self.input_channels, str) + and self.input_channels in self.nodes + ): + as_node = self.input_channels + elif as_node is None: + last_seen_by_node = sorted( + (v, n) + for n, seen in checkpoint["versions_seen"].items() + if n in self.nodes + for v in seen.values() + ) + # if two nodes updated the state at the same time, it's ambiguous + if last_seen_by_node: + if len(last_seen_by_node) == 1: + as_node = last_seen_by_node[0][1] + elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]: + as_node = last_seen_by_node[-1][1] + if as_node is None: + raise InvalidUpdateError("Ambiguous update, specify as_node") + if as_node not in self.nodes: + raise InvalidUpdateError(f"Node {as_node} does not exist") + valid_updates.append((as_node, values, task_id)) + else: + for values, as_node, task_id in updates: + if as_node is None: + raise InvalidUpdateError( + "as_node is required when applying multiple updates" + ) + if as_node not in self.nodes: + raise InvalidUpdateError(f"Node {as_node} does not exist") + + valid_updates.append((as_node, values, task_id)) + + run_tasks: list[PregelTaskWrites] = [] + run_task_ids: list[str] = [] + + for as_node, values, provided_task_id in valid_updates: + # create task to run all writers of the chosen node + writers = self.nodes[as_node].flat_writers + if not writers: + raise InvalidUpdateError(f"Node {as_node} has no writers") + writes: deque[tuple[str, Any]] = deque() + task = PregelTaskWrites((), as_node, writes, [INTERRUPT]) + task_id = provided_task_id or str( + uuid5(UUID(checkpoint["id"]), INTERRUPT) + ) + run_tasks.append(task) + run_task_ids.append(task_id) + run = RunnableSequence(*writers) if len(writers) > 1 else writers[0] + # execute task + run.invoke( + values, + patch_config( + config, + run_name=self.name + "UpdateState", + configurable={ + # deque.extend is thread-safe + CONFIG_KEY_SEND: writes.extend, + CONFIG_KEY_TASK_ID: task_id, + CONFIG_KEY_READ: partial( + local_read, + _scratchpad( + None, + [], + task_id, + "", + None, + step, + step + 2, + ), + channels, + managed, + task, + ), + }, + ), + ) + # save task writes + for task_id, task in zip(run_task_ids, run_tasks): + # channel writes are saved to current checkpoint + channel_writes = [w for w in task.writes if w[0] != PUSH] + if saved and channel_writes: + checkpointer.put_writes(checkpoint_config, channel_writes, task_id) + # apply to checkpoint and save + apply_writes( + checkpoint, + channels, + run_tasks, + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + checkpoint = create_checkpoint(checkpoint, channels, step + 1) + next_config = checkpointer.put( + checkpoint_config, + checkpoint, + { + "source": "update", + "step": step + 1, + "parents": saved.metadata.get("parents", {}) if saved else {}, + }, + get_new_channel_versions( + checkpoint_previous_versions, checkpoint["channel_versions"] + ), + ) + for task_id, task in zip(run_task_ids, run_tasks): + # save push writes + if push_writes := [w for w in task.writes if w[0] == PUSH]: + checkpointer.put_writes(next_config, push_writes, task_id) + + return patch_checkpoint_map(next_config, saved.metadata if saved else None) + + current_config = patch_configurable( + config, {CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID])} + ) + for superstep in supersteps: + current_config = perform_superstep(current_config, superstep) + return current_config + + async def abulk_update_state( + self, + config: RunnableConfig, + supersteps: Sequence[Sequence[StateUpdate]], + ) -> RunnableConfig: + """Asynchronously apply updates to the graph state in bulk. Requires a checkpointer to be set. + + Args: + config: The config to apply the updates to. + supersteps: A list of supersteps, each including a list of updates to apply sequentially to a graph state. + Each update is a tuple of the form `(values, as_node, task_id)` where task_id is optional. + + Raises: + ValueError: If no checkpointer is set or no updates are provided. + InvalidUpdateError: If an invalid update is provided. + + Returns: + RunnableConfig: The updated config. + """ + + checkpointer: BaseCheckpointSaver | None = ensure_config(config)[CONF].get( + CONFIG_KEY_CHECKPOINTER, self.checkpointer + ) + if not checkpointer: + raise ValueError("No checkpointer set") + + if len(supersteps) == 0: + raise ValueError("No supersteps provided") + + if any(len(u) == 0 for u in supersteps): + raise ValueError("No updates provided") + + # delegate to subgraph + if ( + checkpoint_ns := config[CONF].get(CONFIG_KEY_CHECKPOINT_NS, "") + ) and CONFIG_KEY_CHECKPOINTER not in config[CONF]: + # remove task_ids from checkpoint_ns + recast = recast_checkpoint_ns(checkpoint_ns) + # find the subgraph with the matching name + async for _, pregel in self.aget_subgraphs(namespace=recast, recurse=True): + return await pregel.abulk_update_state( + patch_configurable(config, {CONFIG_KEY_CHECKPOINTER: checkpointer}), + supersteps, + ) + else: + raise ValueError(f"Subgraph {recast} not found") + + async def aperform_superstep( + input_config: RunnableConfig, updates: Sequence[StateUpdate] + ) -> RunnableConfig: + # get last checkpoint + config = ensure_config(self.config, input_config) + saved = await checkpointer.aget_tuple(config) + if saved is not None: + self._migrate_checkpoint(saved.checkpoint) + checkpoint = ( + copy_checkpoint(saved.checkpoint) if saved else empty_checkpoint() + ) + checkpoint_previous_versions = ( + 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 + checkpoint_config = patch_configurable( + config, + { + CONFIG_KEY_CHECKPOINT_NS: config[CONF].get( + CONFIG_KEY_CHECKPOINT_NS, "" + ) + }, + ) + if saved: + checkpoint_config = patch_configurable(config, saved.config[CONF]) + channels, managed = channels_from_checkpoint( + self.channels, + checkpoint, + ) + values, as_node = updates[0][:2] + # no values, just clear all tasks + if values is None and as_node == END: + if len(updates) > 1: + raise InvalidUpdateError( + "Cannot apply multiple updates when clearing state" + ) + if saved is not None: + # tasks for this checkpoint + next_tasks = prepare_next_tasks( + checkpoint, + saved.pending_writes or [], + self.nodes, + channels, + managed, + saved.config, + step + 1, + step + 3, + for_execution=True, + store=self.store, + checkpointer=checkpointer, + manager=None, + ) + # apply null writes + if null_writes := [ + w[1:] + for w in saved.pending_writes or [] + if w[0] == NULL_TASK_ID + ]: + apply_writes( + checkpoint, + channels, + [PregelTaskWrites((), INPUT, null_writes, [])], + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + # apply writes from tasks that already ran + for tid, k, v in saved.pending_writes or []: + if k in (ERROR, INTERRUPT): + continue + if tid not in next_tasks: + continue + next_tasks[tid].writes.append((k, v)) + # clear all current tasks + apply_writes( + checkpoint, + channels, + next_tasks.values(), + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + # save checkpoint + next_config = await checkpointer.aput( + checkpoint_config, + create_checkpoint(checkpoint, channels, step), + { + "source": "update", + "step": step + 1, + "parents": saved.metadata.get("parents", {}) if saved else {}, + }, + get_new_channel_versions( + checkpoint_previous_versions, checkpoint["channel_versions"] + ), + ) + return patch_checkpoint_map( + next_config, saved.metadata if saved else None + ) + + # act as an input + if as_node == INPUT: + if len(updates) > 1: + raise InvalidUpdateError( + "Cannot apply multiple updates when updating as input" + ) + + if input_writes := deque(map_input(self.input_channels, values)): + apply_writes( + checkpoint, + channels, + [PregelTaskWrites((), INPUT, input_writes, [])], + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + + # apply input write to channels + next_step = ( + step + 1 + if saved and saved.metadata.get("step") is not None + else -1 + ) + next_config = await checkpointer.aput( + checkpoint_config, + create_checkpoint(checkpoint, channels, next_step), + { + "source": "input", + "step": next_step, + "parents": saved.metadata.get("parents", {}) + if saved + else {}, + }, + get_new_channel_versions( + checkpoint_previous_versions, + checkpoint["channel_versions"], + ), + ) + + # store the writes + await checkpointer.aput_writes( + next_config, + input_writes, + str(uuid5(UUID(checkpoint["id"]), INPUT)), + ) + + return patch_checkpoint_map( + next_config, saved.metadata if saved else None + ) + else: + raise InvalidUpdateError( + f"Received no input writes for {self.input_channels}" + ) + + # no values, copy checkpoint + if as_node == "__copy__": + if len(updates) > 1: + raise InvalidUpdateError( + "Cannot copy checkpoint with multiple updates" + ) + + if saved is None: + raise InvalidUpdateError("Cannot copy a non-existent checkpoint") + + next_checkpoint = create_checkpoint(checkpoint, None, step) + + # copy checkpoint + next_config = await checkpointer.aput( + saved.parent_config + or patch_configurable( + saved.config, {CONFIG_KEY_CHECKPOINT_ID: None} + ), + next_checkpoint, + { + "source": "fork", + "step": step + 1, + "parents": saved.metadata.get("parents", {}), + }, + {}, + ) + + # we want to both clone a checkpoint and update state in one go. + # reuse the same task ID if possible. + if isinstance(values, list) and len(values) > 0: + # figure out the task IDs for the next update checkpoint + next_tasks = prepare_next_tasks( + next_checkpoint, + saved.pending_writes or [], + self.nodes, + channels, + managed, + next_config, + step + 2, + step + 4, + for_execution=True, + store=self.store, + checkpointer=checkpointer, + manager=None, + ) + + tasks_group_by = defaultdict(list) + user_group_by: dict[str, list[StateUpdate]] = defaultdict(list) + + for task in next_tasks.values(): + tasks_group_by[task.name].append(task.id) + + for item in values: + if not isinstance(item, Sequence): + raise InvalidUpdateError( + f"Invalid update item: {item} when copying checkpoint" + ) + + values, as_node = item[:2] + user_group = user_group_by[as_node] + tasks_group = tasks_group_by[as_node] + + target_idx = len(user_group) + task_id = ( + tasks_group[target_idx] + if target_idx < len(tasks_group) + else None + ) + + user_group_by[as_node].append( + StateUpdate(values=values, as_node=as_node, task_id=task_id) + ) + + return await aperform_superstep( + patch_checkpoint_map(next_config, saved.metadata), + [item for lst in user_group_by.values() for item in lst], + ) + + return patch_checkpoint_map( + next_config, saved.metadata if saved else None + ) + # apply pending writes, if not on specific checkpoint + if ( + CONFIG_KEY_CHECKPOINT_ID not in config[CONF] + and saved is not None + and saved.pending_writes + ): + # tasks for this checkpoint + next_tasks = prepare_next_tasks( + checkpoint, + saved.pending_writes, + self.nodes, + channels, + managed, + saved.config, + step + 1, + step + 3, + for_execution=True, + store=self.store, + checkpointer=checkpointer, + manager=None, + ) + # apply null writes + if null_writes := [ + w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID + ]: + apply_writes( + checkpoint, + channels, + [PregelTaskWrites((), INPUT, null_writes, [])], + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + for tid, k, v in saved.pending_writes: + if k in (ERROR, INTERRUPT): + continue + if tid not in next_tasks: + continue + next_tasks[tid].writes.append((k, v)) + if tasks := [t for t in next_tasks.values() if t.writes]: + apply_writes( + checkpoint, + channels, + tasks, + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + valid_updates: list[tuple[str, dict[str, Any] | None, str | None]] = [] + if len(updates) == 1: + values, as_node, task_id = updates[0] + # find last node that updated the state, if not provided + if as_node is None and len(self.nodes) == 1: + as_node = tuple(self.nodes)[0] + elif as_node is None and not saved: + if ( + isinstance(self.input_channels, str) + and self.input_channels in self.nodes + ): + as_node = self.input_channels + elif as_node is None: + last_seen_by_node = sorted( + (v, n) + for n, seen in checkpoint["versions_seen"].items() + if n in self.nodes + for v in seen.values() + ) + # if two nodes updated the state at the same time, it's ambiguous + if last_seen_by_node: + if len(last_seen_by_node) == 1: + as_node = last_seen_by_node[0][1] + elif last_seen_by_node[-1][0] != last_seen_by_node[-2][0]: + as_node = last_seen_by_node[-1][1] + if as_node is None: + raise InvalidUpdateError("Ambiguous update, specify as_node") + if as_node not in self.nodes: + raise InvalidUpdateError(f"Node {as_node} does not exist") + valid_updates.append((as_node, values, task_id)) + else: + for values, as_node, task_id in updates: + if as_node is None: + raise InvalidUpdateError( + "as_node is required when applying multiple updates" + ) + if as_node not in self.nodes: + raise InvalidUpdateError(f"Node {as_node} does not exist") + + valid_updates.append((as_node, values, task_id)) + + run_tasks: list[PregelTaskWrites] = [] + run_task_ids: list[str] = [] + + for as_node, values, provided_task_id in valid_updates: + # create task to run all writers of the chosen node + writers = self.nodes[as_node].flat_writers + if not writers: + raise InvalidUpdateError(f"Node {as_node} has no writers") + writes: deque[tuple[str, Any]] = deque() + task = PregelTaskWrites((), as_node, writes, [INTERRUPT]) + task_id = provided_task_id or str( + uuid5(UUID(checkpoint["id"]), INTERRUPT) + ) + run_tasks.append(task) + run_task_ids.append(task_id) + run = RunnableSequence(*writers) if len(writers) > 1 else writers[0] + # execute task + await run.ainvoke( + values, + patch_config( + config, + run_name=self.name + "UpdateState", + configurable={ + # deque.extend is thread-safe + CONFIG_KEY_SEND: writes.extend, + CONFIG_KEY_TASK_ID: task_id, + CONFIG_KEY_READ: partial( + local_read, + _scratchpad( + None, + [], + task_id, + "", + None, + step, + step + 2, + ), + channels, + managed, + task, + ), + }, + ), + ) + # save task writes + for task_id, task in zip(run_task_ids, run_tasks): + # channel writes are saved to current checkpoint + channel_writes = [w for w in task.writes if w[0] != PUSH] + if saved and channel_writes: + await checkpointer.aput_writes( + checkpoint_config, channel_writes, task_id + ) + # apply to checkpoint and save + apply_writes( + checkpoint, + channels, + run_tasks, + checkpointer.get_next_version, + self.trigger_to_nodes, + ) + checkpoint = create_checkpoint(checkpoint, channels, step + 1) + # save checkpoint, after applying writes + next_config = await checkpointer.aput( + checkpoint_config, + checkpoint, + { + "source": "update", + "step": step + 1, + "parents": saved.metadata.get("parents", {}) if saved else {}, + }, + get_new_channel_versions( + checkpoint_previous_versions, checkpoint["channel_versions"] + ), + ) + for task_id, task in zip(run_task_ids, run_tasks): + # save push writes + if push_writes := [w for w in task.writes if w[0] == PUSH]: + await checkpointer.aput_writes(next_config, push_writes, task_id) + return patch_checkpoint_map(next_config, saved.metadata if saved else None) + + current_config = patch_configurable( + config, {CONFIG_KEY_THREAD_ID: str(config[CONF][CONFIG_KEY_THREAD_ID])} + ) + for superstep in supersteps: + current_config = await aperform_superstep(current_config, superstep) + return current_config + + def update_state( + self, + config: RunnableConfig, + values: dict[str, Any] | Any | None, + as_node: str | None = None, + task_id: str | None = None, + ) -> RunnableConfig: + """Update the state of the graph with the given values, as if they came from + node `as_node`. If `as_node` is not provided, it will be set to the last node + that updated the state, if not ambiguous. + """ + return self.bulk_update_state(config, [[StateUpdate(values, as_node, task_id)]]) + + async def aupdate_state( + self, + config: RunnableConfig, + values: dict[str, Any] | Any, + as_node: str | None = None, + task_id: str | None = None, + ) -> RunnableConfig: + """Asynchronously update the state of the graph with the given values, as if they came from + node `as_node`. If `as_node` is not provided, it will be set to the last node + that updated the state, if not ambiguous. + """ + return await self.abulk_update_state( + config, [[StateUpdate(values, as_node, task_id)]] + ) + + def _defaults( + self, + config: RunnableConfig, + *, + stream_mode: StreamMode | Sequence[StreamMode], + print_mode: StreamMode | Sequence[StreamMode], + output_keys: str | Sequence[str] | None, + interrupt_before: All | Sequence[str] | None, + interrupt_after: All | Sequence[str] | None, + durability: Durability | None = None, + checkpoint_during: bool | None = None, + ) -> tuple[ + set[StreamMode], + str | Sequence[str], + All | Sequence[str], + All | Sequence[str], + BaseCheckpointSaver | None, + BaseStore | None, + BaseCache | None, + Durability, + ]: + if config["recursion_limit"] < 1: + raise ValueError("recursion_limit must be at least 1") + if output_keys is None: + output_keys = self.stream_channels_asis + else: + validate_keys(output_keys, self.channels) + interrupt_before = interrupt_before or self.interrupt_before_nodes + interrupt_after = interrupt_after or self.interrupt_after_nodes + if not isinstance(stream_mode, list): + stream_modes = {stream_mode} + else: + stream_modes = set(stream_mode) + if isinstance(print_mode, str): + stream_modes.add(print_mode) + else: + stream_modes.update(print_mode) + if self.checkpointer is False: + checkpointer: BaseCheckpointSaver | None = None + elif CONFIG_KEY_CHECKPOINTER in config.get(CONF, {}): + checkpointer = config[CONF][CONFIG_KEY_CHECKPOINTER] + elif self.checkpointer is True: + raise RuntimeError("checkpointer=True cannot be used for root graphs.") + else: + checkpointer = self.checkpointer + if checkpointer and not config.get(CONF): + raise ValueError( + "Checkpointer requires one or more of the following 'configurable' " + "keys: thread_id, checkpoint_ns, checkpoint_id" + ) + if CONFIG_KEY_RUNTIME in config.get(CONF, {}): + store: BaseStore | None = config[CONF][CONFIG_KEY_RUNTIME].store + else: + store = self.store + if CONFIG_KEY_CACHE in config.get(CONF, {}): + cache: BaseCache | None = config[CONF][CONFIG_KEY_CACHE] + else: + cache = self.cache + if checkpoint_during is not None: + if durability is not None: + raise ValueError( + "Cannot use both `checkpoint_during` and `durability` parameters." + ) + elif checkpoint_during: + durability = "async" + else: + durability = "exit" + if durability is None: + durability = config.get(CONF, {}).get(CONFIG_KEY_DURABILITY, "async") + return ( + stream_modes, + output_keys, + interrupt_before, + interrupt_after, + checkpointer, + store, + cache, + durability, + ) + + def stream( + self, + input: InputT | Command | None, + config: RunnableConfig | None = None, + *, + context: ContextT | None = None, + stream_mode: StreamMode | Sequence[StreamMode] | None = None, + print_mode: StreamMode | Sequence[StreamMode] = (), + output_keys: str | Sequence[str] | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + durability: Durability | None = None, + subgraphs: bool = False, + debug: bool | None = None, + **kwargs: Unpack[DeprecatedKwargs], + ) -> Iterator[dict[str, Any] | Any]: + """Stream graph steps for a single input. + + Args: + input: The input to the graph. + config: The configuration to use for the run. + stream_mode: The mode to stream output, defaults to `self.stream_mode`. + Options are: + + - `"values"`: Emit all values in the state after each step, including interrupts. + When used with functional API, values are emitted once at the end of the workflow. + - `"updates"`: Emit only the node or task names and updates returned by the nodes or tasks after each step. + If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately. + - `"custom"`: Emit custom data from inside nodes or tasks using `StreamWriter`. + - `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks. + Will be emitted as 2-tuples `(LLM token, metadata)`. + - `"checkpoints"`: Emit an event when a checkpoint is created, in the same format as returned by get_state(). + - `"tasks"`: Emit events when tasks start and finish, including their results and errors. + + You can pass a list as the `stream_mode` parameter to stream multiple modes at once. + The streamed outputs will be tuples of `(mode, data)`. + + See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details. + print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way. + output_keys: The keys to stream, defaults to all non-context channels. + interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph. + interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph. + durability: The durability mode for the graph execution, defaults to "async". Options are: + - `"sync"`: Changes are persisted synchronously before the next step starts. + - `"async"`: Changes are persisted asynchronously while the next step executes. + - `"exit"`: Changes are persisted only when the graph exits. + subgraphs: Whether to stream events from inside subgraphs, defaults to False. + If True, the events will be emitted as tuples `(namespace, data)`, + or `(namespace, mode, data)` if `stream_mode` is a list, + where `namespace` is a tuple with the path to the node where a subgraph is invoked, + e.g. `("parent_node:", "child_node:")`. + + See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details. + + Yields: + The output of each step in the graph. The output shape depends on the stream_mode. + """ + + if stream_mode is None: + # if being called as a node in another graph, default to values mode + # but don't overwrite stream_mode arg if provided + stream_mode = ( + "values" + if config is not None and CONFIG_KEY_TASK_ID in config.get(CONF, {}) + else self.stream_mode + ) + if debug or self.debug: + print_mode = ["updates", "values"] + + stream = SyncQueue() + + config = ensure_config(self.config, config) + callback_manager = get_callback_manager_for_config(config) + run_manager = callback_manager.on_chain_start( + None, + input, + name=config.get("run_name", self.get_name()), + run_id=config.get("run_id"), + ) + try: + deprecated_checkpoint_during = cast( + Optional[bool], kwargs.get("checkpoint_during") + ) + if deprecated_checkpoint_during is not None: + warnings.warn( + "`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.", + category=LangGraphDeprecatedSinceV10, + ) + # assign defaults + ( + stream_modes, + output_keys, + interrupt_before_, + interrupt_after_, + checkpointer, + store, + cache, + durability_, + ) = self._defaults( + config, + stream_mode=stream_mode, + print_mode=print_mode, + output_keys=output_keys, + interrupt_before=interrupt_before, + interrupt_after=interrupt_after, + durability=durability, + checkpoint_during=deprecated_checkpoint_during, + ) + if checkpointer is None and ( + durability is not None or deprecated_checkpoint_during is not None + ): + warnings.warn( + "`durability` has no effect when no checkpointer is present.", + ) + # set up subgraph checkpointing + if self.checkpointer is True: + ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS]) + config[CONF][CONFIG_KEY_CHECKPOINT_NS] = recast_checkpoint_ns(ns) + # set up messages stream mode + if "messages" in stream_modes: + run_manager.inheritable_handlers.append( + StreamMessagesHandler(stream.put, subgraphs) + ) + + # set up custom stream mode + if "custom" in stream_modes: + + def stream_writer(c: Any) -> None: + stream.put( + ( + tuple( + get_config()[CONF][CONFIG_KEY_CHECKPOINT_NS].split( + NS_SEP + )[:-1] + ), + "custom", + c, + ) + ) + elif CONFIG_KEY_STREAM in config[CONF]: + stream_writer = config[CONF][CONFIG_KEY_RUNTIME].stream_writer + else: + + def stream_writer(c: Any) -> None: + pass + + # set durability mode for subgraphs + if durability is not None or deprecated_checkpoint_during is not None: + config[CONF][CONFIG_KEY_DURABILITY] = durability_ + + config[CONF][CONFIG_KEY_RUNTIME] = Runtime( + context=context, + store=store, + stream_writer=stream_writer, + previous=None, + ) + with SyncPregelLoop( + input, + stream=StreamProtocol(stream.put, stream_modes), + config=config, + store=store, + cache=cache, + checkpointer=checkpointer, + nodes=self.nodes, + specs=self.channels, + output_keys=output_keys, + input_keys=self.input_channels, + stream_keys=self.stream_channels_asis, + interrupt_before=interrupt_before_, + interrupt_after=interrupt_after_, + manager=run_manager, + durability=durability_, + trigger_to_nodes=self.trigger_to_nodes, + migrate_checkpoint=self._migrate_checkpoint, + retry_policy=self.retry_policy, + cache_policy=self.cache_policy, + ) as loop: + # create runner + runner = PregelRunner( + submit=config[CONF].get( + CONFIG_KEY_RUNNER_SUBMIT, weakref.WeakMethod(loop.submit) + ), + put_writes=weakref.WeakMethod(loop.put_writes), + node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED), + ) + # enable subgraph streaming + if subgraphs: + loop.config[CONF][CONFIG_KEY_STREAM] = loop.stream + # enable concurrent streaming + if ( + self.stream_eager + or subgraphs + or "messages" in stream_modes + or "custom" in stream_modes + ): + # we are careful to have a single waiter live at any one time + # because on exit we increment semaphore count by exactly 1 + waiter: concurrent.futures.Future | None = None + # because sync futures cannot be cancelled, we instead + # release the stream semaphore on exit, which will cause + # a pending waiter to return immediately + loop.stack.callback(stream._count.release) + + def get_waiter() -> concurrent.futures.Future[None]: + nonlocal waiter + if waiter is None or waiter.done(): + waiter = loop.submit(stream.wait) + return waiter + else: + return waiter + + else: + get_waiter = None # type: ignore[assignment] + # 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 + # 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(): + for task in loop.match_cached_writes(): + loop.output_writes(task.id, task.writes, cached=True) + for _ in runner.tick( + [t for t in loop.tasks.values() if not t.writes], + timeout=self.step_timeout, + get_waiter=get_waiter, + schedule_task=loop.accept_push, + ): + # emit output + yield from _output( + stream_mode, print_mode, subgraphs, stream.get, queue.Empty + ) + loop.after_tick() + # wait for checkpoint + if durability_ == "sync": + loop._put_checkpoint_fut.result() + # emit output + yield from _output( + stream_mode, print_mode, subgraphs, stream.get, queue.Empty + ) + # handle exit + if loop.status == "out_of_steps": + msg = create_error_message( + message=( + 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." + ), + error_code=ErrorCode.GRAPH_RECURSION_LIMIT, + ) + raise GraphRecursionError(msg) + # 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 + + async def astream( + self, + input: InputT | Command | None, + config: RunnableConfig | None = None, + *, + context: ContextT | None = None, + stream_mode: StreamMode | Sequence[StreamMode] | None = None, + print_mode: StreamMode | Sequence[StreamMode] = (), + output_keys: str | Sequence[str] | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + durability: Durability | None = None, + subgraphs: bool = False, + debug: bool | None = None, + **kwargs: Unpack[DeprecatedKwargs], + ) -> AsyncIterator[dict[str, Any] | Any]: + """Asynchronously stream graph steps for a single input. + + Args: + input: The input to the graph. + config: The configuration to use for the run. + stream_mode: The mode to stream output, defaults to `self.stream_mode`. + Options are: + + - `"values"`: Emit all values in the state after each step, including interrupts. + When used with functional API, values are emitted once at the end of the workflow. + - `"updates"`: Emit only the node or task names and updates returned by the nodes or tasks after each step. + If multiple updates are made in the same step (e.g. multiple nodes are run) then those updates are emitted separately. + - `"custom"`: Emit custom data from inside nodes or tasks using `StreamWriter`. + - `"messages"`: Emit LLM messages token-by-token together with metadata for any LLM invocations inside nodes or tasks. + Will be emitted as 2-tuples `(LLM token, metadata)`. + - `"debug"`: Emit debug events with as much information as possible for each step. + + You can pass a list as the `stream_mode` parameter to stream multiple modes at once. + The streamed outputs will be tuples of `(mode, data)`. + + See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details. + print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way. + output_keys: The keys to stream, defaults to all non-context channels. + interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph. + interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph. + durability: The durability mode for the graph execution, defaults to "async". Options are: + - `"sync"`: Changes are persisted synchronously before the next step starts. + - `"async"`: Changes are persisted asynchronously while the next step executes. + - `"exit"`: Changes are persisted only when the graph exits. + subgraphs: Whether to stream events from inside subgraphs, defaults to False. + If True, the events will be emitted as tuples `(namespace, data)`, + or `(namespace, mode, data)` if `stream_mode` is a list, + where `namespace` is a tuple with the path to the node where a subgraph is invoked, + e.g. `("parent_node:", "child_node:")`. + + See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details. + + Yields: + The output of each step in the graph. The output shape depends on the stream_mode. + """ + + if stream_mode is None: + # if being called as a node in another graph, default to values mode + # but don't overwrite stream_mode arg if provided + stream_mode = ( + "values" + if config is not None and CONFIG_KEY_TASK_ID in config.get(CONF, {}) + else self.stream_mode + ) + if debug or self.debug: + print_mode = ["updates", "values"] + + stream = AsyncQueue() + aioloop = asyncio.get_running_loop() + stream_put = cast( + Callable[[StreamChunk], None], + partial(aioloop.call_soon_threadsafe, stream.put_nowait), + ) + + config = ensure_config(self.config, config) + callback_manager = get_async_callback_manager_for_config(config) + run_manager = await callback_manager.on_chain_start( + None, + input, + name=config.get("run_name", self.get_name()), + run_id=config.get("run_id"), + ) + # if running from astream_log() run each proc with streaming + do_stream = ( + next( + ( + True + for h in run_manager.handlers + if isinstance(h, _StreamingCallbackHandler) + and not isinstance(h, StreamMessagesHandler) + ), + False, + ) + if _StreamingCallbackHandler is not None + else False + ) + try: + deprecated_checkpoint_during = cast( + Optional[bool], kwargs.get("checkpoint_during") + ) + if deprecated_checkpoint_during is not None: + warnings.warn( + "`checkpoint_during` is deprecated and will be removed. Please use `durability` instead.", + category=LangGraphDeprecatedSinceV10, + ) + # assign defaults + ( + stream_modes, + output_keys, + interrupt_before_, + interrupt_after_, + checkpointer, + store, + cache, + durability_, + ) = self._defaults( + config, + stream_mode=stream_mode, + print_mode=print_mode, + output_keys=output_keys, + interrupt_before=interrupt_before, + interrupt_after=interrupt_after, + durability=durability, + checkpoint_during=deprecated_checkpoint_during, + ) + if checkpointer is None and ( + durability is not None or deprecated_checkpoint_during is not None + ): + warnings.warn( + "`durability` has no effect when no checkpointer is present.", + ) + # set up subgraph checkpointing + if self.checkpointer is True: + ns = cast(str, config[CONF][CONFIG_KEY_CHECKPOINT_NS]) + config[CONF][CONFIG_KEY_CHECKPOINT_NS] = recast_checkpoint_ns(ns) + # set up messages stream mode + if "messages" in stream_modes: + run_manager.inheritable_handlers.append( + StreamMessagesHandler(stream_put, subgraphs) + ) + + # set up custom stream mode + def stream_writer(c: Any) -> None: + aioloop.call_soon_threadsafe( + stream.put_nowait, + ( + tuple( + get_config()[CONF][CONFIG_KEY_CHECKPOINT_NS].split(NS_SEP)[ + :-1 + ] + ), + "custom", + c, + ), + ) + + if "custom" in stream_modes: + + def stream_writer(c: Any) -> None: + aioloop.call_soon_threadsafe( + stream.put_nowait, + ( + tuple( + get_config()[CONF][CONFIG_KEY_CHECKPOINT_NS].split( + NS_SEP + )[:-1] + ), + "custom", + c, + ), + ) + elif CONFIG_KEY_STREAM in config[CONF]: + stream_writer = config[CONF][CONFIG_KEY_RUNTIME].stream_writer + else: + + def stream_writer(c: Any) -> None: + pass + + # set durability mode for subgraphs + if durability is not None or deprecated_checkpoint_during is not None: + config[CONF][CONFIG_KEY_DURABILITY] = durability_ + + config[CONF][CONFIG_KEY_RUNTIME] = Runtime( + context=context, + store=store, + stream_writer=stream_writer, + previous=None, + ) + async with AsyncPregelLoop( + input, + stream=StreamProtocol(stream.put_nowait, stream_modes), + config=config, + store=store, + cache=cache, + checkpointer=checkpointer, + nodes=self.nodes, + specs=self.channels, + output_keys=output_keys, + input_keys=self.input_channels, + stream_keys=self.stream_channels_asis, + interrupt_before=interrupt_before_, + interrupt_after=interrupt_after_, + manager=run_manager, + durability=durability_, + trigger_to_nodes=self.trigger_to_nodes, + migrate_checkpoint=self._migrate_checkpoint, + retry_policy=self.retry_policy, + cache_policy=self.cache_policy, + ) as loop: + # create runner + runner = PregelRunner( + submit=config[CONF].get( + CONFIG_KEY_RUNNER_SUBMIT, weakref.WeakMethod(loop.submit) + ), + put_writes=weakref.WeakMethod(loop.put_writes), + use_astream=do_stream, + node_finished=config[CONF].get(CONFIG_KEY_NODE_FINISHED), + ) + # enable subgraph streaming + if subgraphs: + loop.config[CONF][CONFIG_KEY_STREAM] = StreamProtocol( + stream_put, stream_modes + ) + # enable concurrent streaming + if ( + self.stream_eager + or subgraphs + or "messages" in stream_modes + or "custom" in stream_modes + ): + + def get_waiter() -> asyncio.Task[None]: + return aioloop.create_task(stream.wait()) + + else: + get_waiter = None # type: ignore[assignment] + # 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 + # 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(): + for task in await loop.amatch_cached_writes(): + loop.output_writes(task.id, task.writes, cached=True) + async for _ in runner.atick( + [t for t in loop.tasks.values() if not t.writes], + timeout=self.step_timeout, + get_waiter=get_waiter, + schedule_task=loop.aaccept_push, + ): + # emit output + for o in _output( + stream_mode, + print_mode, + subgraphs, + stream.get_nowait, + asyncio.QueueEmpty, + ): + yield o + loop.after_tick() + # wait for checkpoint + if durability_ == "sync": + await cast(asyncio.Future, loop._put_checkpoint_fut) + # emit output + for o in _output( + stream_mode, + print_mode, + subgraphs, + stream.get_nowait, + asyncio.QueueEmpty, + ): + yield o + # handle exit + if loop.status == "out_of_steps": + msg = create_error_message( + message=( + 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." + ), + error_code=ErrorCode.GRAPH_RECURSION_LIMIT, + ) + raise GraphRecursionError(msg) + # set final channel values as run output + await run_manager.on_chain_end(loop.output) + except BaseException as e: + await asyncio.shield(run_manager.on_chain_error(e)) + raise + + def invoke( + self, + input: InputT | Command | None, + config: RunnableConfig | None = None, + *, + context: ContextT | None = None, + stream_mode: StreamMode = "values", + print_mode: StreamMode | Sequence[StreamMode] = (), + output_keys: str | Sequence[str] | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + **kwargs: Any, + ) -> dict[str, Any] | Any: + """Run the graph with a single input and config. + + Args: + input: The input data for the graph. It can be a dictionary or any other type. + config: Optional. The configuration for the graph run. + stream_mode: Optional[str]. The stream mode for the graph run. Default is "values". + print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way. + output_keys: Optional. The output keys to retrieve from the graph run. + interrupt_before: Optional. The nodes to interrupt the graph run before. + interrupt_after: Optional. The nodes to interrupt the graph run after. + **kwargs: Additional keyword arguments to pass to the graph run. + + Returns: + The output of the graph run. If stream_mode is "values", it returns the latest output. + If stream_mode is not "values", it returns a list of output chunks. + """ + output_keys = output_keys if output_keys is not None else self.output_channels + + latest: dict[str, Any] | Any = None + chunks: list[dict[str, Any] | Any] = [] + interrupts: list[Interrupt] = [] + + for chunk in self.stream( + input, + config, + context=context, + stream_mode=["updates", "values"] + if stream_mode == "values" + else stream_mode, + print_mode=print_mode, + output_keys=output_keys, + interrupt_before=interrupt_before, + interrupt_after=interrupt_after, + **kwargs, + ): + if stream_mode == "values": + if len(chunk) == 2: + mode, payload = cast(tuple[StreamMode, Any], chunk) + else: + _, mode, payload = cast( + tuple[tuple[str, ...], StreamMode, Any], chunk + ) + if ( + mode == "updates" + and isinstance(payload, dict) + and (ints := payload.get(INTERRUPT)) is not None + ): + interrupts.extend(ints) + elif mode == "values": + latest = payload + else: + chunks.append(chunk) + + if stream_mode == "values": + if interrupts: + return ( + {**latest, INTERRUPT: interrupts} + if isinstance(latest, dict) + else {INTERRUPT: interrupts} + ) + return latest + else: + return chunks + + async def ainvoke( + self, + input: InputT | Command | None, + config: RunnableConfig | None = None, + *, + context: ContextT | None = None, + stream_mode: StreamMode = "values", + print_mode: StreamMode | Sequence[StreamMode] = (), + output_keys: str | Sequence[str] | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + **kwargs: Any, + ) -> dict[str, Any] | Any: + """Asynchronously invoke the graph on a single input. + + Args: + input: The input data for the computation. It can be a dictionary or any other type. + config: Optional. The configuration for the computation. + stream_mode: Optional. The stream mode for the computation. Default is "values". + print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way. + output_keys: Optional. The output keys to include in the result. Default is None. + interrupt_before: Optional. The nodes to interrupt before. Default is None. + interrupt_after: Optional. The nodes to interrupt after. Default is None. + **kwargs: Additional keyword arguments. + + Returns: + The result of the computation. If stream_mode is "values", it returns the latest value. + If stream_mode is "chunks", it returns a list of chunks. + """ + + output_keys = output_keys if output_keys is not None else self.output_channels + + latest: dict[str, Any] | Any = None + chunks: list[dict[str, Any] | Any] = [] + interrupts: list[Interrupt] = [] + + async for chunk in self.astream( + input, + config, + context=context, + stream_mode=["updates", "values"] + if stream_mode == "values" + else stream_mode, + print_mode=print_mode, + output_keys=output_keys, + interrupt_before=interrupt_before, + interrupt_after=interrupt_after, + **kwargs, + ): + if stream_mode == "values": + if len(chunk) == 2: + mode, payload = cast(tuple[StreamMode, Any], chunk) + else: + _, mode, payload = cast( + tuple[tuple[str, ...], StreamMode, Any], chunk + ) + if ( + mode == "updates" + and isinstance(payload, dict) + and (ints := payload.get(INTERRUPT)) is not None + ): + interrupts.extend(ints) + elif mode == "values": + latest = payload + else: + chunks.append(chunk) + + if stream_mode == "values": + if interrupts: + return ( + {**latest, INTERRUPT: interrupts} + if isinstance(latest, dict) + else {INTERRUPT: interrupts} + ) + return latest + else: + return chunks + + def clear_cache(self, nodes: Sequence[str] | None = None) -> None: + """Clear the cache for the given nodes.""" + if not self.cache: + raise ValueError("No cache is set for this graph. Cannot clear cache.") + nodes = nodes or self.nodes.keys() + # collect namespaces to clear + namespaces: list[tuple[str, ...]] = [] + for node in nodes: + if node in self.nodes: + namespaces.append( + ( + CACHE_NS_WRITES, + (identifier(self.nodes[node]) or "__dynamic__"), + node, + ), + ) + # clear cache + self.cache.clear(namespaces) + + async def aclear_cache(self, nodes: Sequence[str] | None = None) -> None: + """Asynchronously clear the cache for the given nodes.""" + if not self.cache: + raise ValueError("No cache is set for this graph. Cannot clear cache.") + nodes = nodes or self.nodes.keys() + # collect namespaces to clear + namespaces: list[tuple[str, ...]] = [] + for node in nodes: + if node in self.nodes: + namespaces.append( + ( + CACHE_NS_WRITES, + (identifier(self.nodes[node]) or "__dynamic__"), + node, + ), + ) + # clear cache + await self.cache.aclear(namespaces) + + +def _trigger_to_nodes(nodes: dict[str, PregelNode]) -> Mapping[str, Sequence[str]]: + """Index from a trigger to nodes that depend on it.""" + trigger_to_nodes: defaultdict[str, list[str]] = defaultdict(list) + for name, node in nodes.items(): + for trigger in node.triggers: + trigger_to_nodes[trigger].append(name) + return dict(trigger_to_nodes) + + +def _output( + stream_mode: StreamMode | Sequence[StreamMode], + print_mode: StreamMode | Sequence[StreamMode], + stream_subgraphs: bool, + getter: Callable[[], tuple[tuple[str, ...], str, Any]], + empty_exc: type[Exception], +) -> Iterator: + while True: + try: + ns, mode, payload = getter() + except empty_exc: + break + if mode in print_mode: + if stream_subgraphs and ns: + print( + " ".join( + ( + get_bolded_text(f"[{mode}]"), + get_colored_text(f"[graph={ns}]", color="yellow"), + repr(payload), + ) + ) + ) + else: + print( + " ".join( + ( + get_bolded_text(f"[{mode}]"), + repr(payload), + ) + ) + ) + if mode in stream_mode: + if stream_subgraphs and isinstance(stream_mode, list): + yield (ns, mode, payload) + elif isinstance(stream_mode, list): + yield (mode, payload) + elif stream_subgraphs: + yield (ns, payload) + else: + yield payload diff --git a/libs/langgraph/langgraph/pregel/protocol.py b/libs/langgraph/langgraph/pregel/protocol.py index 654c08fe1..5b5f83c70 100644 --- a/libs/langgraph/langgraph/pregel/protocol.py +++ b/libs/langgraph/langgraph/pregel/protocol.py @@ -2,19 +2,19 @@ from __future__ import annotations from abc import abstractmethod from collections.abc import AsyncIterator, Iterator, Sequence -from typing import Any, Generic +from typing import Any, Callable, Generic, cast from langchain_core.runnables import Runnable, RunnableConfig from langchain_core.runnables.graph import Graph as DrawableGraph from typing_extensions import Self -from langgraph.pregel.types import All, StateSnapshot, StateUpdate, StreamMode -from langgraph.types import Command -from langgraph.typing import InputT, OutputT, StateT +from langgraph.types import All, Command, StateSnapshot, StateUpdate, StreamMode +from langgraph.typing import ContextT, InputT, OutputT, StateT + +__all__ = ("PregelProtocol", "StreamProtocol") -# TODO: remove Runnable inheritance here! -class PregelProtocol(Runnable[InputT, Any], Generic[StateT, InputT, OutputT]): +class PregelProtocol(Runnable[InputT, Any], Generic[StateT, ContextT, InputT, OutputT]): @abstractmethod def with_config( self, config: RunnableConfig | None = None, **kwargs: Any @@ -102,6 +102,7 @@ class PregelProtocol(Runnable[InputT, Any], Generic[StateT, InputT, OutputT]): input: InputT | Command | None, config: RunnableConfig | None = None, *, + context: ContextT | None = None, stream_mode: StreamMode | list[StreamMode] | None = None, interrupt_before: All | Sequence[str] | None = None, interrupt_after: All | Sequence[str] | None = None, @@ -114,6 +115,7 @@ class PregelProtocol(Runnable[InputT, Any], Generic[StateT, InputT, OutputT]): input: InputT | Command | None, config: RunnableConfig | None = None, *, + context: ContextT | None = None, stream_mode: StreamMode | list[StreamMode] | None = None, interrupt_before: All | Sequence[str] | None = None, interrupt_after: All | Sequence[str] | None = None, @@ -126,6 +128,7 @@ class PregelProtocol(Runnable[InputT, Any], Generic[StateT, InputT, OutputT]): input: InputT | Command | None, config: RunnableConfig | None = None, *, + context: ContextT | None = None, interrupt_before: All | Sequence[str] | None = None, interrupt_after: All | Sequence[str] | None = None, ) -> dict[str, Any] | Any: ... @@ -136,6 +139,26 @@ class PregelProtocol(Runnable[InputT, Any], Generic[StateT, InputT, OutputT]): input: InputT | Command | None, config: RunnableConfig | None = None, *, + context: ContextT | None = None, interrupt_before: All | Sequence[str] | None = None, interrupt_after: All | Sequence[str] | None = None, ) -> dict[str, Any] | Any: ... + + +StreamChunk = tuple[tuple[str, ...], str, Any] + + +class StreamProtocol: + __slots__ = ("modes", "__call__") + + modes: set[StreamMode] + + __call__: Callable[[Self, StreamChunk], None] + + def __init__( + self, + __call__: Callable[[StreamChunk], None], + modes: set[StreamMode], + ) -> None: + self.__call__ = cast(Callable[[Self, StreamChunk], None], __call__) + self.modes = modes diff --git a/libs/langgraph/langgraph/pregel/py.typed b/libs/langgraph/langgraph/pregel/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/libs/langgraph/langgraph/pregel/remote.py b/libs/langgraph/langgraph/pregel/remote.py index 30b6eed8d..6cc51b908 100644 --- a/libs/langgraph/langgraph/pregel/remote.py +++ b/libs/langgraph/langgraph/pregel/remote.py @@ -29,8 +29,8 @@ from langgraph_sdk.schema import Command as CommandSDK from langgraph_sdk.schema import StreamMode as StreamModeSDK from typing_extensions import Self -from langgraph.checkpoint.base import CheckpointMetadata -from langgraph.constants import ( +from langgraph._internal._config import merge_configs +from langgraph._internal._constants import ( CONF, CONFIG_KEY_CHECKPOINT_ID, CONFIG_KEY_CHECKPOINT_MAP, @@ -40,13 +40,21 @@ from langgraph.constants import ( INTERRUPT, NS_SEP, ) +from langgraph.checkpoint.base import CheckpointMetadata from langgraph.errors import GraphInterrupt, ParentCommand -from langgraph.pregel.protocol import PregelProtocol -from langgraph.pregel.types import All, PregelTask, StateSnapshot, StreamMode -from langgraph.types import Command, Interrupt, StreamProtocol -from langgraph.utils.config import merge_configs +from langgraph.pregel.protocol import PregelProtocol, StreamProtocol +from langgraph.types import ( + All, + Command, + Interrupt, + PregelTask, + StateSnapshot, + StreamMode, +) -CONF_DROPLIST = frozenset( +__all__ = ("RemoteGraph", "RemoteException") + +_CONF_DROPLIST = frozenset( ( CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_CHECKPOINT_ID, @@ -56,7 +64,7 @@ CONF_DROPLIST = frozenset( ) -def sanitize_config_value(v: Any) -> Any: +def _sanitize_config_value(v: Any) -> Any: """Recursively sanitize a config value to ensure it contains only primitives.""" if isinstance(v, (str, int, float, bool)): return v @@ -64,14 +72,14 @@ def sanitize_config_value(v: Any) -> Any: sanitized_dict = {} for k, val in v.items(): if isinstance(k, str): - sanitized_value = sanitize_config_value(val) + sanitized_value = _sanitize_config_value(val) if sanitized_value is not None: sanitized_dict[k] = sanitized_value return sanitized_dict elif isinstance(v, (list, tuple)): sanitized_list = [] for item in v: - sanitized_item = sanitize_config_value(item) + sanitized_item = _sanitize_config_value(item) if sanitized_item is not None: sanitized_list.append(sanitized_item) return sanitized_list @@ -252,9 +260,9 @@ class RemoteGraph(PregelProtocol): def _create_state_snapshot(self, state: ThreadState) -> StateSnapshot: tasks: list[PregelTask] = [] for task in state["tasks"]: - interrupts = [] - for interrupt in task["interrupts"]: - interrupts.append(Interrupt(**interrupt)) + interrupts = tuple( + Interrupt(**interrupt) for interrupt in task["interrupts"] + ) tasks.append( PregelTask( @@ -262,7 +270,7 @@ class RemoteGraph(PregelProtocol): name=task["name"], path=tuple(), error=Exception(task["error"]) if task["error"] else None, - interrupts=tuple(interrupts), + interrupts=interrupts, state=( self._create_state_snapshot(task["state"]) if task["state"] @@ -347,7 +355,7 @@ class RemoteGraph(PregelProtocol): for k, v in config["metadata"].items(): if ( isinstance(k, str) - and (sanitized_value := sanitize_config_value(v)) is not None + and (sanitized_value := _sanitize_config_value(v)) is not None ): sanitized["metadata"][k] = sanitized_value @@ -356,8 +364,8 @@ class RemoteGraph(PregelProtocol): for k, v in config["configurable"].items(): if ( isinstance(k, str) - and k not in CONF_DROPLIST - and (sanitized_value := sanitize_config_value(v)) is not None + and k not in _CONF_DROPLIST + and (sanitized_value := _sanitize_config_value(v)) is not None ): sanitized["configurable"][k] = sanitized_value diff --git a/libs/langgraph/langgraph/pregel/types.py b/libs/langgraph/langgraph/pregel/types.py index 212c0c4af..39a36df68 100644 --- a/libs/langgraph/langgraph/pregel/types.py +++ b/libs/langgraph/langgraph/pregel/types.py @@ -25,3 +25,14 @@ __all__ = [ "StreamWriter", "default_retry_on", ] + +from warnings import warn + +from langgraph.warnings import LangGraphDeprecatedSinceV10 + +warn( + "Importing from langgraph.pregel.types is deprecated. " + "Please use 'from langgraph.types import ...' instead.", + LangGraphDeprecatedSinceV10, + stacklevel=2, +) diff --git a/libs/langgraph/langgraph/runtime.py b/libs/langgraph/langgraph/runtime.py new file mode 100644 index 000000000..c819f1d4e --- /dev/null +++ b/libs/langgraph/langgraph/runtime.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from dataclasses import dataclass, field, replace +from typing import Any, Generic, cast + +from typing_extensions import TypedDict, Unpack + +from langgraph._internal._constants import CONF, CONFIG_KEY_RUNTIME +from langgraph.config import get_config +from langgraph.store.base import BaseStore +from langgraph.types import _DC_KWARGS, StreamWriter +from langgraph.typing import ContextT + + +def _no_op_stream_writer(_: Any) -> None: ... + + +class _RuntimeOverrides(TypedDict, Generic[ContextT], total=False): + context: ContextT + store: BaseStore | None + stream_writer: StreamWriter + previous: Any + + +@dataclass(**_DC_KWARGS) +class Runtime(Generic[ContextT]): + """Convenience class that bundles run-scoped context and graph configuration. + + !!! version-added "Added in version 1.0.0." + """ + + context: ContextT = field(default=None) # type: ignore[assignment] + """Static context for the graph run, like user_id, db_conn, etc. + + Can also be thought of as 'run dependencies'.""" + + store: BaseStore | None = field(default=None) + """Store for the graph run, enabling persistence and memory.""" + + stream_writer: StreamWriter = field(default=_no_op_stream_writer) + """Function that writes to the custom stream.""" + + previous: Any = field(default=None) + """The previous return value for the given thread. + + Only available with the functional API when a checkpointer is provided. + """ + + def merge(self, other: Runtime[ContextT]) -> Runtime[ContextT]: + """Merge two runtimes together. + + If a value is not provided in the other runtime, the value from the current runtime is used. + """ + return Runtime( + context=other.context or self.context, + store=other.store or self.store, + stream_writer=other.stream_writer + if other.stream_writer is not _no_op_stream_writer + else self.stream_writer, + previous=other.previous or self.previous, + ) + + def override( + self, **overrides: Unpack[_RuntimeOverrides[ContextT]] + ) -> Runtime[ContextT]: + """Replace the runtime with a new runtime with the given overrides.""" + return replace(self, **overrides) + + +DEFAULT_RUNTIME = Runtime( + context=None, + store=None, + stream_writer=_no_op_stream_writer, + previous=None, +) + + +def get_runtime(context_schema: type[ContextT] | None = None) -> Runtime[ContextT]: + """Get the runtime for the current graph run.""" + + # TODO: in an ideal world, we would have a context manager for + # the runtime that's independent of the config. this will follow + # from the removal of the configurable packing + runtime = cast(Runtime[ContextT], get_config()[CONF].get(CONFIG_KEY_RUNTIME)) + return runtime diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py index d71271f29..867991b35 100644 --- a/libs/langgraph/langgraph/types.py +++ b/libs/langgraph/langgraph/types.py @@ -1,9 +1,9 @@ from __future__ import annotations -import dataclasses import sys from collections import deque from collections.abc import Hashable, Sequence +from dataclasses import asdict, dataclass from typing import ( TYPE_CHECKING, Any, @@ -14,16 +14,20 @@ from typing import ( NamedTuple, TypeVar, Union, - cast, + final, ) +from warnings import warn from langchain_core.runnables import Runnable, RunnableConfig -from typing_extensions import Self +from typing_extensions import Unpack, deprecated from xxhash import xxh3_128_hexdigest +from langgraph._internal._cache import default_cache_key +from langgraph._internal._fields import get_cached_annotated_keys, get_update_as_tuples +from langgraph._internal._retry import default_retry_on +from langgraph._internal._typing import MISSING, DeprecatedKwargs from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata -from langgraph.utils.cache import default_cache_key -from langgraph.utils.fields import get_cached_annotated_keys, get_update_as_tuples +from langgraph.warnings import LangGraphDeprecatedSinceV10 if TYPE_CHECKING: from langgraph.pregel.protocol import PregelProtocol @@ -37,6 +41,30 @@ except ImportError: pass +__all__ = ( + "All", + "Checkpointer", + "StreamMode", + "StreamWriter", + "RetryPolicy", + "CachePolicy", + "Interrupt", + "StateUpdate", + "PregelTask", + "PregelExecutableTask", + "StateSnapshot", + "Send", + "Command", + "Durability", + "interrupt", +) + +Durability = Literal["sync", "async", "exit"] +"""Durability mode for the graph execution. +- `"sync"`: Changes are persisted synchronously before the next step starts. +- `"async"`: Changes are persisted asynchronously while the next step executes. +- `"exit"`: Changes are persisted only when the graph exits.""" + All = Literal["*"] """Special value to indicate that graph should interrupt on all nodes.""" @@ -68,42 +96,13 @@ Always injected into nodes if requested as a keyword argument, but it's a no-op when not using stream_mode="custom".""" if sys.version_info >= (3, 10): + _DC_SLOTS = {"slots": True} _DC_KWARGS = {"kw_only": True, "slots": True, "frozen": True} else: + _DC_SLOTS = {} _DC_KWARGS = {"frozen": True} -def default_retry_on(exc: Exception) -> bool: - import httpx - import requests - - if isinstance(exc, ConnectionError): - return True - if isinstance(exc, httpx.HTTPStatusError): - return 500 <= exc.response.status_code < 600 - if isinstance(exc, requests.HTTPError): - return 500 <= exc.response.status_code < 600 if exc.response else True - if isinstance( - exc, - ( - ValueError, - TypeError, - ArithmeticError, - ImportError, - LookupError, - NameError, - SyntaxError, - RuntimeError, - ReferenceError, - StopIteration, - StopAsyncIteration, - OSError, - ), - ): - return False - return True - - class RetryPolicy(NamedTuple): """Configuration for retrying nodes. @@ -129,7 +128,7 @@ class RetryPolicy(NamedTuple): KeyFuncT = TypeVar("KeyFuncT", bound=Callable[..., Union[str, bytes]]) -@dataclasses.dataclass(**_DC_KWARGS) +@dataclass(**_DC_KWARGS) class CachePolicy(Generic[KeyFuncT]): """Configuration for caching nodes.""" @@ -141,7 +140,11 @@ class CachePolicy(Generic[KeyFuncT]): """Time to live for the cache entry in seconds. If None, the entry never expires.""" -@dataclasses.dataclass(**_DC_KWARGS) +_DEFAULT_INTERRUPT_ID = "placeholder-id" + + +@final +@dataclass(init=False, **_DC_SLOTS) class Interrupt: """Information about an interrupt that occurred in a node. @@ -149,16 +152,41 @@ class Interrupt: """ value: Any - resumable: bool = False - ns: Sequence[str] | None = None - when: Literal["during"] = dataclasses.field(default="during", repr=False) + id: str + + def __init__( + self, + value: Any, + id: str = _DEFAULT_INTERRUPT_ID, + **deprecated_kwargs: Unpack[DeprecatedKwargs], + ) -> None: + self.value = value + + if ( + (ns := deprecated_kwargs.get("ns", MISSING)) is not MISSING + and (id == _DEFAULT_INTERRUPT_ID) + and (isinstance(ns, Sequence)) + ): + self.id = xxh3_128_hexdigest("|".join(ns).encode()) + else: + self.id = id + + @classmethod + def from_ns(cls, value: Any, ns: str) -> Interrupt: + return cls(value=value, id=xxh3_128_hexdigest(ns.encode())) @property + @deprecated( + "`interrupt_id` is deprecated. Use `id` instead.", + stacklevel=2, + ) def interrupt_id(self) -> str: - """Generate a unique ID for the interrupt based on its namespace.""" - if self.ns is None: - return "placeholder-id" - return xxh3_128_hexdigest("|".join(self.ns).encode()) + warn( + "`interrupt_id` is deprecated. Use `id` instead.", + LangGraphDeprecatedSinceV10, + stacklevel=2, + ) + return self.id class StateUpdate(NamedTuple): @@ -196,7 +224,7 @@ class CacheKey(NamedTuple): """Time to live for the cache entry in seconds.""" -@dataclasses.dataclass(**_T_DC_KWARGS) +@dataclass(**_T_DC_KWARGS) class PregelExecutableTask: name: str input: Any @@ -307,7 +335,7 @@ class Send: N = TypeVar("N", bound=Hashable) -@dataclasses.dataclass(**_DC_KWARGS) +@dataclass(**_DC_KWARGS) class Command(Generic[N], ToolOutputMixin): """One or more commands to update the graph's state and send messages to nodes. @@ -340,9 +368,7 @@ class Command(Generic[N], ToolOutputMixin): def __repr__(self) -> str: # get all non-None values contents = ", ".join( - f"{key}={value!r}" - for key, value in dataclasses.asdict(self).items() - if value + f"{key}={value!r}" for key, value in asdict(self).items() if value ) return f"Command({contents})" @@ -364,39 +390,6 @@ class Command(Generic[N], ToolOutputMixin): PARENT: ClassVar[Literal["__parent__"]] = "__parent__" -StreamChunk = tuple[tuple[str, ...], str, Any] - - -class StreamProtocol: - __slots__ = ("modes", "__call__") - - modes: set[StreamMode] - - __call__: Callable[[Self, StreamChunk], None] - - def __init__( - self, - __call__: Callable[[StreamChunk], None], - modes: set[StreamMode], - ) -> None: - self.__call__ = cast(Callable[[Self, StreamChunk], None], __call__) - self.modes = modes - - -@dataclasses.dataclass(**_DC_KWARGS) -class PregelScratchpad: - step: int - stop: int - # call - call_counter: Callable[[], int] - # interrupt - interrupt_counter: Callable[[], int] - get_null_resume: Callable[[bool], Any] - resume: list[Any] - # subgraph - subgraph_counter: Callable[[], int] - - def interrupt(value: Any) -> Any: """Interrupt the graph with a resumable exception from within a node. @@ -465,22 +458,16 @@ def interrupt(value: Any) -> Any: for chunk in graph.stream({\"foo\": \"abc\"}, config): print(chunk) - ``` - ```pycon - {'__interrupt__': (Interrupt(value='what is your age?', resumable=True, ns=['node:62e598fa-8653-9d6d-2046-a70203020e37'], when='during'),)} - ``` + # > {'__interrupt__': (Interrupt(value='what is your age?', id='45fda8478b2ef754419799e10992af06'),)} - ```python command = Command(resume=\"some input from a human!!!\") for chunk in graph.stream(Command(resume=\"some input from a human!!!\"), config): print(chunk) - ``` - ```pycon - Received an input from the interrupt: some input from a human!!! - {'node': {'human_value': 'some input from a human!!!'}} + # > Received an input from the interrupt: some input from a human!!! + # > {'node': {'human_value': 'some input from a human!!!'}} ``` Args: @@ -492,19 +479,18 @@ def interrupt(value: Any) -> Any: Raises: GraphInterrupt: On the first invocation within the node, halts execution and surfaces the provided value to the client. """ - from langgraph.config import get_config - from langgraph.constants import ( + from langgraph._internal._constants import ( CONFIG_KEY_CHECKPOINT_NS, CONFIG_KEY_SCRATCHPAD, CONFIG_KEY_SEND, - NS_SEP, RESUME, ) + from langgraph.config import get_config from langgraph.errors import GraphInterrupt conf = get_config()["configurable"] # track interrupt index - scratchpad: PregelScratchpad = conf[CONFIG_KEY_SCRATCHPAD] + scratchpad = conf[CONFIG_KEY_SCRATCHPAD] idx = scratchpad.interrupt_counter() # find previous resume values if scratchpad.resume: @@ -520,10 +506,9 @@ def interrupt(value: Any) -> Any: # no resume value found raise GraphInterrupt( ( - Interrupt( + Interrupt.from_ns( value=value, - resumable=True, - ns=cast(str, conf[CONFIG_KEY_CHECKPOINT_NS]).split(NS_SEP), + ns=conf[CONFIG_KEY_CHECKPOINT_NS], ), ) ) diff --git a/libs/langgraph/langgraph/typing.py b/libs/langgraph/langgraph/typing.py index 01ea27adf..c3ba65939 100644 --- a/libs/langgraph/langgraph/typing.py +++ b/libs/langgraph/langgraph/typing.py @@ -4,7 +4,16 @@ from typing import Union from typing_extensions import TypeVar -from langgraph._typing import StateLike +from langgraph._internal._typing import StateLike + +__all__ = ( + "StateT", + "StateT_co", + "StateT_contra", + "InputT", + "OutputT", + "ContextT", +) StateT = TypeVar("StateT", bound=StateLike) """Type variable used to represent the state in a graph.""" @@ -13,18 +22,29 @@ StateT_co = TypeVar("StateT_co", bound=StateLike, covariant=True) StateT_contra = TypeVar("StateT_contra", bound=StateLike, contravariant=True) +ContextT = TypeVar("ContextT", bound=Union[StateLike, None], default=None) +"""Type variable used to represent graph run scoped context. + +Defaults to `None`. +""" + +ContextT_contra = TypeVar( + "ContextT_contra", bound=Union[StateLike, None], contravariant=True, default=None +) + InputT = TypeVar("InputT", bound=StateLike, default=StateT) """Type variable used to represent the input to a state graph. Defaults to `StateT`. """ -ResolvedInputT = TypeVar("ResolvedInputT", bound=StateLike) -"""Type variable used to represent the resolved input to a state graph. +OutputT = TypeVar("OutputT", bound=StateLike, default=StateT) +"""Type variable used to represent the output of a state graph. -No default. +Defaults to `StateT`. """ +NodeInputT = TypeVar("NodeInputT", bound=StateLike) +"""Type variable used to represent the input to a node.""" -OutputT = TypeVar("OutputT", bound=Union[StateLike, None], default=StateT) -"""Type variable used to represent the output of a state graph.""" +NodeInputT_contra = TypeVar("NodeInputT_contra", bound=StateLike, contravariant=True) diff --git a/libs/langgraph/langgraph/utils/py.typed b/libs/langgraph/langgraph/utils/py.typed deleted file mode 100644 index e69de29bb..000000000 diff --git a/libs/langgraph/langgraph/version.py b/libs/langgraph/langgraph/version.py index f5cb757f5..a81f647c7 100644 --- a/libs/langgraph/langgraph/version.py +++ b/libs/langgraph/langgraph/version.py @@ -2,6 +2,8 @@ from importlib import metadata +__all__ = ("__version__",) + try: __version__ = metadata.version(__package__) except metadata.PackageNotFoundError: diff --git a/libs/langgraph/langgraph/warnings.py b/libs/langgraph/langgraph/warnings.py index e8fd59d88..638f247e3 100644 --- a/libs/langgraph/langgraph/warnings.py +++ b/libs/langgraph/langgraph/warnings.py @@ -2,6 +2,12 @@ from __future__ import annotations +__all__ = ( + "LangGraphDeprecationWarning", + "LangGraphDeprecatedSinceV05", + "LangGraphDeprecatedSinceV10", +) + class LangGraphDeprecationWarning(DeprecationWarning): """A LangGraph specific deprecation warning. @@ -46,3 +52,10 @@ class LangGraphDeprecatedSinceV05(LangGraphDeprecationWarning): def __init__(self, message: str, *args: object) -> None: super().__init__(message, *args, since=(0, 5), expected_removal=(2, 0)) + + +class LangGraphDeprecatedSinceV10(LangGraphDeprecationWarning): + """A specific `LangGraphDeprecationWarning` subclass defining functionality deprecated since LangGraph v1.0.0""" + + def __init__(self, message: str, *args: object) -> None: + super().__init__(message, *args, since=(1, 0), expected_removal=(2, 0)) diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 2a4b96228..065815c66 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "langgraph" -version = "0.5.4" +version = "0.6.0a1" description = "Building stateful, multi-actor applications with LLMs" authors = [] requires-python = ">=3.9" @@ -14,7 +14,7 @@ license-files = ['LICENSE'] dependencies = [ "langchain-core>=0.1", "langgraph-checkpoint>=2.1.0,<3.0.0", - "langgraph-sdk>=0.1.42,<0.2.0", + "langgraph-sdk>=0.2.0,<0.3.0", "langgraph-prebuilt>=0.5.0,<0.6.0", "xxhash>=3.5.0", "pydantic>=2.7.4", @@ -60,6 +60,7 @@ langgraph-checkpoint = { path = "../checkpoint", editable = true } langgraph-checkpoint-sqlite = { path = "../checkpoint-sqlite", editable = true } langgraph-checkpoint-postgres = { path = "../checkpoint-postgres", editable = true } langgraph-sdk = { path = "../sdk-py", editable = true } +langgraph-cli = { path = "../cli", editable = true } [tool.ruff] lint.select = [ "E", "F", "I", "TID251", "UP" ] diff --git a/libs/langgraph/tests/__snapshots__/test_large_cases.ambr b/libs/langgraph/tests/__snapshots__/test_large_cases.ambr index 3b3b383ff..2e26861ac 100644 --- a/libs/langgraph/tests/__snapshots__/test_large_cases.ambr +++ b/libs/langgraph/tests/__snapshots__/test_large_cases.ambr @@ -1,105 +1,4 @@ # serializer version: 1 -# name: test_conditional_graph[memory] - ''' - { - "nodes": [ - { - "id": "agent", - "type": "runnable", - "data": { - "id": [ - "langchain", - "schema", - "runnable", - "RunnableAssign" - ], - "name": "agent" - } - }, - { - "id": "tools", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "runnable", - "RunnableCallable" - ], - "name": "tools" - }, - "metadata": { - "parents": {}, - "version": 2, - "variant": "b" - } - }, - { - "id": "__start__" - }, - { - "id": "__end__" - } - ], - "edges": [ - { - "source": "__start__", - "target": "agent" - }, - { - "source": "agent", - "target": "__end__", - "data": "exit", - "conditional": true - }, - { - "source": "agent", - "target": "tools", - "data": "continue", - "conditional": true - }, - { - "source": "tools", - "target": "agent" - } - ] - } - ''' -# --- -# name: test_conditional_graph[memory].1 - ''' - graph TD; - __start__ --> agent; - agent -.  exit  .-> __end__; - agent -.  continue  .-> tools; - tools --> agent; - - ''' -# --- -# name: test_conditional_graph[memory].2 - ''' - --- - config: - flowchart: - curve: linear - --- - graph TD; - agent(agent) - tools(tools
parents = {} - version = 2 - variant = b) - __start__([

__start__

]):::first - __end__([

__end__

]):::last - __start__ --> agent; - agent -.  exit  .-> __end__; - agent -.  continue  .-> tools; - tools --> agent; - classDef default fill:#f2f0ff,line-height:1.2 - classDef first fill-opacity:0 - classDef last fill:#bfb6fc - - ''' -# --- # name: test_conditional_state_graph[memory] '{"$defs": {"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.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"additionalProperties": true, "title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "title": "Agent Outcome"}, "intermediate_steps": {"items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "title": "AgentState", "type": "object"}' # --- @@ -116,8 +15,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "__start__" @@ -142,8 +41,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "tools" @@ -204,8 +103,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "__start__" @@ -291,8 +190,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "__start__" @@ -304,8 +203,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "agent" diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index 0c7ffba54..5167d8924 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -1,93 +1,4 @@ # serializer version: 1 -# name: test_conditional_entrypoint_graph - '{"title": "LangGraphInput"}' -# --- -# name: test_conditional_entrypoint_graph.1 - '{"title": "LangGraphOutput"}' -# --- -# name: test_conditional_entrypoint_graph.2 - ''' - { - "nodes": [ - { - "id": "left", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "runnable", - "RunnableCallable" - ], - "name": "left" - } - }, - { - "id": "right", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "runnable", - "RunnableCallable" - ], - "name": "right" - } - }, - { - "id": "__start__", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "runnable", - "RunnableCallable" - ], - "name": "__start__" - } - }, - { - "id": "__end__" - } - ], - "edges": [ - { - "source": "__start__", - "target": "left", - "data": "go-left", - "conditional": true - }, - { - "source": "__start__", - "target": "right", - "data": "go-right", - "conditional": true - }, - { - "source": "left", - "target": "__end__", - "conditional": true - }, - { - "source": "right", - "target": "__end__" - } - ] - } - ''' -# --- -# name: test_conditional_entrypoint_graph.3 - ''' - graph TD; - __start__ -.  go-left  .-> left; - __start__ -.  go-right  .-> right; - left -.-> __end__; - right --> __end__; - - ''' -# --- # name: test_conditional_entrypoint_graph_state '{"properties": {"input": {"title": "Input", "type": "string"}, "output": {"title": "Output", "type": "string"}, "steps": {"items": {"type": "string"}, "title": "Steps", "type": "array"}}, "title": "AgentState", "type": "object"}' # --- @@ -104,8 +15,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "__start__" @@ -117,8 +28,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "left" @@ -130,8 +41,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "right" @@ -193,8 +104,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "__start__" @@ -206,8 +117,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "get_weather" @@ -249,8 +160,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "__start__" @@ -262,8 +173,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "A" @@ -275,8 +186,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "B" @@ -327,8 +238,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "__start__" @@ -340,8 +251,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "human" @@ -353,8 +264,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "agent" @@ -406,8 +317,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "__start__" @@ -461,8 +372,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "__start__" @@ -474,8 +385,8 @@ "data": { "id": [ "langgraph", - "utils", - "runnable", + "_internal", + "_runnable", "RunnableCallable" ], "name": "worker_node" @@ -767,8 +678,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': '__start__', @@ -780,8 +691,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'tool_one', @@ -793,8 +704,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'tool_three', @@ -809,8 +720,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'tool_two:__start__', @@ -822,8 +733,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'tool_two:tool_two_slow', @@ -835,8 +746,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'tool_two:tool_two_fast', @@ -914,7 +825,7 @@ ''' # --- # name: test_state_graph_w_config_inherited_state_keys - '{"$defs": {"Config": {"properties": {"tools": {"items": {"type": "string"}, "title": "Tools", "type": "array"}}, "title": "Config", "type": "object"}}, "properties": {"configurable": {"$ref": "#/$defs/Config", "default": null}}, "title": "LangGraphConfig", "type": "object"}' + '{"properties": {"tools": {"items": {"type": "string"}, "title": "Tools", "type": "array"}}, "title": "Context", "type": "object"}' # --- # name: test_state_graph_w_config_inherited_state_keys.1 '{"$defs": {"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.", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"anyOf": [{"type": "string"}, {"additionalProperties": true, "type": "object"}], "title": "Tool Input"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentAction", "default": "AgentAction", "title": "Type", "type": "string"}}, "required": ["tool", "tool_input", "log"], "title": "AgentAction", "type": "object"}, "AgentFinish": {"description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "properties": {"return_values": {"additionalProperties": true, "title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"const": "AgentFinish", "default": "AgentFinish", "title": "Type", "type": "string"}}, "required": ["return_values", "log"], "title": "AgentFinish", "type": "object"}}, "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"anyOf": [{"$ref": "#/$defs/AgentAction"}, {"$ref": "#/$defs/AgentFinish"}, {"type": "null"}], "title": "Agent Outcome"}, "intermediate_steps": {"items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"$ref": "#/$defs/AgentAction"}, {"type": "string"}], "type": "array"}, "title": "Intermediate Steps", "type": "array"}}, "required": ["input", "agent_outcome"], "title": "AgentState", "type": "object"}' @@ -1020,8 +931,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': '__start__', @@ -1033,8 +944,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'ask_question', @@ -1046,8 +957,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'answer_question', @@ -1087,8 +998,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': '__start__', @@ -1100,8 +1011,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'generate_analysts', @@ -1126,8 +1037,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'generate_sections', @@ -1185,8 +1096,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': '__start__', @@ -1198,8 +1109,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'generate_analysts', @@ -1211,8 +1122,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'generate_sections', @@ -1227,8 +1138,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'conduct_interview:__start__', @@ -1240,8 +1151,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'conduct_interview:ask_question', @@ -1253,8 +1164,8 @@ 'data': dict({ 'id': list([ 'langgraph', - 'utils', - 'runnable', + '_internal', + '_runnable', 'RunnableCallable', ]), 'name': 'conduct_interview:answer_question', diff --git a/libs/langgraph/tests/conftest.py b/libs/langgraph/tests/conftest.py index 269c3e66b..d82239aa5 100644 --- a/libs/langgraph/tests/conftest.py +++ b/libs/langgraph/tests/conftest.py @@ -10,6 +10,7 @@ from langgraph.cache.memory import InMemoryCache from langgraph.cache.sqlite import SqliteCache from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.store.base import BaseStore +from langgraph.types import Durability from tests.conftest_checkpointer import ( _checkpointer_memory, _checkpointer_memory_migrate_sends, @@ -49,8 +50,8 @@ def deterministic_uuids(mocker: MockerFixture) -> MockerFixture: return mocker.patch("uuid.uuid4", side_effect=side_effect) -@pytest.fixture(params=[True, False]) -def checkpoint_during(request: pytest.FixtureRequest) -> bool: +@pytest.fixture(params=["sync", "async", "exit"]) +def durability(request: pytest.FixtureRequest) -> Durability: return request.param diff --git a/libs/langgraph/tests/test_algo.py b/libs/langgraph/tests/test_algo.py index 4dbdffeee..0bf988173 100644 --- a/libs/langgraph/tests/test_algo.py +++ b/libs/langgraph/tests/test_algo.py @@ -1,6 +1,6 @@ -from langgraph.constants import PULL, PUSH -from langgraph.pregel.algo import prepare_next_tasks, task_path_str -from langgraph.pregel.checkpoint import channels_from_checkpoint, empty_checkpoint +from langgraph._internal._constants import PULL, PUSH +from langgraph.pregel._algo import prepare_next_tasks, task_path_str +from langgraph.pregel._checkpoint import channels_from_checkpoint, empty_checkpoint def test_prepare_next_tasks() -> None: diff --git a/libs/langgraph/tests/test_channels.py b/libs/langgraph/tests/test_channels.py index c8d679ab8..76254c504 100644 --- a/libs/langgraph/tests/test_channels.py +++ b/libs/langgraph/tests/test_channels.py @@ -4,10 +4,10 @@ from typing import Union import pytest +from langgraph._internal._typing import MISSING from langgraph.channels.binop import BinaryOperatorAggregate from langgraph.channels.last_value import LastValue from langgraph.channels.topic import Topic -from langgraph.constants import MISSING from langgraph.errors import EmptyChannelError, InvalidUpdateError pytestmark = pytest.mark.anyio diff --git a/libs/langgraph/tests/test_checkpoint_migration.py b/libs/langgraph/tests/test_checkpoint_migration.py index 1f3b47c13..f8b4adbdc 100644 --- a/libs/langgraph/tests/test_checkpoint_migration.py +++ b/libs/langgraph/tests/test_checkpoint_migration.py @@ -7,11 +7,11 @@ from typing import Annotated, Literal, Optional, Union import pytest from typing_extensions import TypedDict +from langgraph._internal._config import patch_configurable from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointTuple from langgraph.graph.state import StateGraph -from langgraph.pregel.checkpoint import copy_checkpoint +from langgraph.pregel._checkpoint import copy_checkpoint from langgraph.types import Command, Interrupt, PregelTask, StateSnapshot, interrupt -from langgraph.utils.config import patch_configurable from tests.any_int import AnyInt from tests.any_str import AnyDict, AnyObject, AnyStr @@ -92,8 +92,7 @@ def get_expected_history(*, exc_task_results: int = 0) -> list[StateSnapshot]: else ( Interrupt( value="", - resumable=True, - ns=[AnyStr("qa:")], + id=AnyStr(), ), ), state=None, @@ -107,8 +106,7 @@ def get_expected_history(*, exc_task_results: int = 0) -> list[StateSnapshot]: else ( Interrupt( value="", - resumable=True, - ns=[AnyStr("qa:")], + id=AnyStr(), ), ), ), @@ -412,8 +410,8 @@ SAVED_CHECKPOINTS = { [ Interrupt( value="", - resumable=True, - ns=["qa:2430f303-da9f-2e3e-738c-2e8ea28e8973"], + resumable=True, # type: ignore[arg-type] + ns=["qa:2430f303-da9f-2e3e-738c-2e8ea28e8973"], # type: ignore[arg-type] ) ], ), @@ -786,8 +784,8 @@ SAVED_CHECKPOINTS = { [ Interrupt( value="", - resumable=True, - ns=["qa:4ee8637e-0a95-285e-75bc-4da721c0beab"], + resumable=True, # type: ignore[arg-type] + ns=["qa:4ee8637e-0a95-285e-75bc-4da721c0beab"], # type: ignore[arg-type] ) ], ), @@ -1173,7 +1171,7 @@ SAVED_CHECKPOINTS = { Interrupt( value="", resumable=True, - ns=["qa:369e94b1-77d1-d67a-ab59-23d1ba20ee73"], + ns=["qa:369e94b1-77d1-d67a-ab59-23d1ba20ee73"], # type: ignore[arg-type] ) ], ), @@ -1515,7 +1513,7 @@ def test_latest_checkpoint_state_graph( config = {"configurable": {"thread_id": "1"}} assert [ - *app.stream({"query": "what is weather in sf"}, config, checkpoint_during=True) + *app.stream({"query": "what is weather in sf"}, config, durability="async") ] == [ {"rewrite_query": {"query": "query: what is weather in sf"}}, {"analyzer_one": {"query": "analyzed: query: what is weather in sf"}}, @@ -1525,14 +1523,13 @@ def test_latest_checkpoint_state_graph( "__interrupt__": ( Interrupt( value="", - resumable=True, - ns=[AnyStr("qa:")], + id=AnyStr(), ), ) }, ] - assert [*app.stream(Command(resume=""), config, checkpoint_during=True)] == [ + assert [*app.stream(Command(resume=""), config, durability="async")] == [ {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] @@ -1559,7 +1556,7 @@ async def test_latest_checkpoint_state_graph_async( assert [ c async for c in app.astream( - {"query": "what is weather in sf"}, config, checkpoint_during=True + {"query": "what is weather in sf"}, config, durability="async" ) ] == [ {"rewrite_query": {"query": "query: what is weather in sf"}}, @@ -1570,15 +1567,14 @@ async def test_latest_checkpoint_state_graph_async( "__interrupt__": ( Interrupt( value="", - resumable=True, - ns=[AnyStr("qa:")], + id=AnyStr(), ), ) }, ] assert [ - c async for c in app.astream(Command(resume=""), config, checkpoint_during=True) + c async for c in app.astream(Command(resume=""), config, durability="async") ] == [ {"qa": {"answer": "doc1,doc2,doc3,doc4"}}, ] diff --git a/libs/langgraph/tests/test_config_async.py b/libs/langgraph/tests/test_config_async.py index e57433a28..a65a5f2af 100644 --- a/libs/langgraph/tests/test_config_async.py +++ b/libs/langgraph/tests/test_config_async.py @@ -1,7 +1,7 @@ import pytest from langchain_core.callbacks import AsyncCallbackManager -from langgraph.utils.config import get_async_callback_manager_for_config +from langgraph._internal._config import get_async_callback_manager_for_config pytestmark = pytest.mark.anyio diff --git a/libs/langgraph/tests/test_deprecation.py b/libs/langgraph/tests/test_deprecation.py index 2a217edae..d33b1a8ac 100644 --- a/libs/langgraph/tests/test_deprecation.py +++ b/libs/langgraph/tests/test_deprecation.py @@ -1,10 +1,14 @@ import pytest +from pytest_mock import MockerFixture from typing_extensions import TypedDict +from langgraph.channels.last_value import LastValue +from langgraph.errors import NodeInterrupt from langgraph.func import entrypoint, task from langgraph.graph import StateGraph -from langgraph.types import RetryPolicy -from langgraph.warnings import LangGraphDeprecatedSinceV05 +from langgraph.pregel import NodeBuilder, Pregel +from langgraph.types import Interrupt, RetryPolicy +from langgraph.warnings import LangGraphDeprecatedSinceV05, LangGraphDeprecatedSinceV10 class PlainState(TypedDict): ... @@ -66,3 +70,101 @@ def test_add_node_input_schema() -> None: match="`input` is deprecated and will be removed. Please use `input_schema` instead.", ): builder.add_node("test_node", lambda state: state, input=PlainState) # type: ignore[arg-type] + + +def test_constants_deprecation() -> None: + with pytest.warns( + LangGraphDeprecatedSinceV10, + match="Importing Send from langgraph.constants is deprecated. Please use 'from langgraph.types import Send' instead.", + ): + from langgraph.constants import Send # noqa: F401 + + with pytest.warns( + LangGraphDeprecatedSinceV10, + match="Importing Interrupt from langgraph.constants is deprecated. Please use 'from langgraph.types import Interrupt' instead.", + ): + from langgraph.constants import Interrupt # noqa: F401 + + +def test_pregel_types_deprecation() -> None: + with pytest.warns( + LangGraphDeprecatedSinceV10, + match="Importing from langgraph.pregel.types is deprecated. Please use 'from langgraph.types import ...' instead.", + ): + from langgraph.pregel.types import StateSnapshot # noqa: F401 + + +@pytest.mark.filterwarnings("ignore:`config_schema` is deprecated") +@pytest.mark.filterwarnings("ignore:`get_config_jsonschema` is deprecated") +def test_config_schema_deprecation() -> None: + with pytest.warns( + LangGraphDeprecatedSinceV10, + match="`config_schema` is deprecated and will be removed. Please use `context_schema` instead.", + ): + builder = StateGraph(PlainState, config_schema=PlainState) + + builder.add_node("test_node", lambda state: state) + builder.set_entry_point("test_node") + graph = builder.compile() + + with pytest.warns( + LangGraphDeprecatedSinceV10, + match="`config_schema` is deprecated. Use `get_context_jsonschema` for the relevant schema instead.", + ): + graph.config_schema() + + with pytest.warns( + LangGraphDeprecatedSinceV10, + match="`get_config_jsonschema` is deprecated. Use `get_context_jsonschema` instead.", + ): + graph.get_config_jsonschema() + + +def test_config_type_deprecation_pregel(mocker: MockerFixture) -> None: + add_one = mocker.Mock(side_effect=lambda x: x + 1) + chain = NodeBuilder().subscribe_only("input").do(add_one).write_to("output") + + with pytest.warns( + LangGraphDeprecatedSinceV10, + match="`config_type` is deprecated and will be removed. Please use `context_schema` instead.", + ): + Pregel( + nodes={ + "one": chain, + }, + channels={ + "input": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", + config_type=PlainState, + ) + + +@pytest.mark.filterwarnings("ignore:`interrupt_id` is deprecated. Use `id` instead.") +def test_interrupt_attributes_deprecation() -> None: + interrupt = Interrupt(value="question", id="abc") + + with pytest.warns( + LangGraphDeprecatedSinceV10, + match="`interrupt_id` is deprecated. Use `id` instead.", + ): + interrupt.interrupt_id + + +@pytest.mark.filterwarnings("ignore:NodeInterrupt is deprecated.") +def test_node_interrupt_deprecation() -> None: + with pytest.warns( + LangGraphDeprecatedSinceV10, + match="NodeInterrupt is deprecated. Please use `langgraph.types.interrupt` instead.", + ): + NodeInterrupt(value="test") + + +def test_deprecated_import() -> None: + with pytest.warns( + LangGraphDeprecatedSinceV10, + match="Importing PREVIOUS from langgraph.constants is deprecated. This constant is now private and should not be used directly.", + ): + from langgraph.constants import PREVIOUS # noqa: F401 diff --git a/libs/langgraph/tests/test_interrupt_migration.py b/libs/langgraph/tests/test_interrupt_migration.py new file mode 100644 index 000000000..8149e0f85 --- /dev/null +++ b/libs/langgraph/tests/test_interrupt_migration.py @@ -0,0 +1,50 @@ +import warnings + +import pytest + +from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer +from langgraph.types import Interrupt +from langgraph.warnings import LangGraphDeprecatedSinceV10 + + +@pytest.mark.filterwarnings("ignore:LangGraphDeprecatedSinceV10") +def test_interrupt_legacy_ns() -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=LangGraphDeprecatedSinceV10) + + old_interrupt = Interrupt( + value="abc", resumable=True, when="during", ns=["a:b", "c:d"] + ) + + new_interrupt = Interrupt.from_ns(value="abc", ns="a:b|c:d") + assert new_interrupt.value == old_interrupt.value + assert new_interrupt.id == old_interrupt.id + + +serializer = JsonPlusSerializer() + + +def test_serialization_roundtrip() -> None: + """Test that the legacy interrupt (pre v1) can be reserialized as the modern interrupt without id corruption.""" + + # generated with: + # JsonPlusSerializer().dumps(Interrupt(value="legacy_test", ns=["legacy_test"], resumable=True, when="during")) + legacy_interrupt_bytes = b'{"lc": 2, "type": "constructor", "id": ["langgraph", "types", "Interrupt"], "kwargs": {"value": "legacy_test", "resumable": true, "ns": ["legacy_test"], "when": "during"}}' + legacy_interrupt_id = "f1fa625689ec006a5b32b76863e22a6c" + + interrupt = serializer.loads(legacy_interrupt_bytes) + assert interrupt.id == legacy_interrupt_id + assert interrupt.value == "legacy_test" + + +def test_serialization_roundtrip_complex_ns() -> None: + """Test that the legacy interrupt (pre v1), with a more complex ns can be reserialized as the modern interrupt without id corruption.""" + + # generated with: + # JsonPlusSerializer().dumps(Interrupt(value="legacy_test", ns=["legacy:test", "with:complex", "name:space"], resumable=True, when="during")) + legacy_interrupt_bytes = b'{"lc": 2, "type": "constructor", "id": ["langgraph", "types", "Interrupt"], "kwargs": {"value": "legacy_test", "resumable": true, "ns": ["legacy:test", "with:complex", "name:space"], "when": "during"}}' + legacy_interrupt_id = "e69356a9ee3630ee7f4f597f2693000c" + + interrupt = serializer.loads(legacy_interrupt_bytes) + assert interrupt.id == legacy_interrupt_id + assert interrupt.value == "legacy_test" diff --git a/libs/langgraph/tests/test_interruption.py b/libs/langgraph/tests/test_interruption.py index 6b86129fc..9e5f928ce 100644 --- a/libs/langgraph/tests/test_interruption.py +++ b/libs/langgraph/tests/test_interruption.py @@ -3,12 +3,13 @@ from typing_extensions import TypedDict from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.graph import END, START, StateGraph +from langgraph.types import Durability pytestmark = pytest.mark.anyio def test_interruption_without_state_updates( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: """Test interruption without state updates. This test confirms that interrupting doesn't require a state key having been updated in the prev step""" @@ -33,24 +34,24 @@ def test_interruption_without_state_updates( initial_input = {"input": "hello world"} thread = {"configurable": {"thread_id": "1"}} - graph.invoke(initial_input, thread, checkpoint_during=checkpoint_during) + graph.invoke(initial_input, thread, durability=durability) assert graph.get_state(thread).next == ("step_2",) n_checkpoints = len([c for c in graph.get_state_history(thread)]) - assert n_checkpoints == (3 if checkpoint_during else 1) + assert n_checkpoints == (3 if durability != "exit" else 1) - graph.invoke(None, thread, checkpoint_during=checkpoint_during) + graph.invoke(None, thread, durability=durability) assert graph.get_state(thread).next == ("step_3",) n_checkpoints = len([c for c in graph.get_state_history(thread)]) - assert n_checkpoints == (4 if checkpoint_during else 2) + assert n_checkpoints == (4 if durability != "exit" else 2) - graph.invoke(None, thread, checkpoint_during=checkpoint_during) + graph.invoke(None, thread, durability=durability) assert graph.get_state(thread).next == () n_checkpoints = len([c for c in graph.get_state_history(thread)]) - assert n_checkpoints == (5 if checkpoint_during else 3) + assert n_checkpoints == (5 if durability != "exit" else 3) async def test_interruption_without_state_updates_async( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: """Test interruption without state updates. This test confirms that interrupting doesn't require a state key having been updated in the prev step""" @@ -75,17 +76,17 @@ async def test_interruption_without_state_updates_async( initial_input = {"input": "hello world"} thread = {"configurable": {"thread_id": "1"}} - await graph.ainvoke(initial_input, thread, checkpoint_during=checkpoint_during) + await graph.ainvoke(initial_input, thread, durability=durability) assert (await graph.aget_state(thread)).next == ("step_2",) n_checkpoints = len([c async for c in graph.aget_state_history(thread)]) - assert n_checkpoints == (3 if checkpoint_during else 1) + assert n_checkpoints == (3 if durability != "exit" else 1) - await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during) + await graph.ainvoke(None, thread, durability=durability) assert (await graph.aget_state(thread)).next == ("step_3",) n_checkpoints = len([c async for c in graph.aget_state_history(thread)]) - assert n_checkpoints == (4 if checkpoint_during else 2) + assert n_checkpoints == (4 if durability != "exit" else 2) - await graph.ainvoke(None, thread, checkpoint_during=checkpoint_during) + await graph.ainvoke(None, thread, durability=durability) assert (await graph.aget_state(thread)).next == () n_checkpoints = len([c async for c in graph.aget_state_history(thread)]) - assert n_checkpoints == (5 if checkpoint_during else 3) + assert n_checkpoints == (5 if durability != "exit" else 3) diff --git a/libs/langgraph/tests/test_large_cases.py b/libs/langgraph/tests/test_large_cases.py index 62cf5ffa1..4f7845bba 100644 --- a/libs/langgraph/tests/test_large_cases.py +++ b/libs/langgraph/tests/test_large_cases.py @@ -11,12 +11,12 @@ from pytest_mock import MockerFixture from syrupy import SnapshotAssertion from typing_extensions import TypedDict +from langgraph._internal._constants import PULL, PUSH from langgraph.channels.last_value import LastValue from langgraph.channels.untracked_value import UntrackedValue from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.checkpoint.memory import InMemorySaver -from langgraph.constants import END, PULL, PUSH, START -from langgraph.errors import NodeInterrupt +from langgraph.constants import END, START from langgraph.graph import StateGraph from langgraph.graph.message import MessageGraph, MessagesState, add_messages from langgraph.prebuilt.chat_agent_executor import create_react_agent @@ -24,6 +24,7 @@ from langgraph.prebuilt.tool_node import ToolNode from langgraph.pregel import NodeBuilder, Pregel from langgraph.types import ( Command, + Durability, Interrupt, PregelTask, RetryPolicy, @@ -69,7 +70,7 @@ def test_invoke_two_processes_in_out_interrupt( thread2 = {"configurable": {"thread_id": "2"}} # start execution, stop at inbox - assert app.invoke(2, thread1, checkpoint_during=True) is None + assert app.invoke(2, thread1, durability="async") is None # inbox == 3 checkpoint = sync_checkpointer.get(thread1) @@ -77,10 +78,10 @@ def test_invoke_two_processes_in_out_interrupt( assert checkpoint["channel_values"]["inbox"] == 3 # resume execution, finish - assert app.invoke(None, thread1, checkpoint_during=True) == 4 + assert app.invoke(None, thread1, durability="async") == 4 # start execution again, stop at inbox - assert app.invoke(20, thread1, checkpoint_during=True) is None + assert app.invoke(20, thread1, durability="async") is None # inbox == 21 checkpoint = sync_checkpointer.get(thread1) @@ -88,11 +89,11 @@ def test_invoke_two_processes_in_out_interrupt( assert checkpoint["channel_values"]["inbox"] == 21 # send a new value in, interrupting the previous execution - assert app.invoke(3, thread1, checkpoint_during=True) is None - assert app.invoke(None, thread1, checkpoint_during=True) == 5 + assert app.invoke(3, thread1, durability="async") is None + assert app.invoke(None, thread1, durability="async") == 5 # start execution again, stopping at inbox - assert app.invoke(20, thread2, checkpoint_during=True) is None + assert app.invoke(20, thread2, durability="async") is None # inbox == 21 snapshot = app.get_state(thread2) @@ -299,9 +300,7 @@ def test_fork_always_re_runs_nodes( # start execution, stop at inbox assert [ - *graph.stream( - 1, thread1, stream_mode=["values", "updates"], checkpoint_during=True - ) + *graph.stream(1, thread1, stream_mode=["values", "updates"], durability="async") ] == [ ("values", 1), ("updates", {"add_one": 1}), @@ -666,7 +665,7 @@ def test_conditional_state_graph( assert [ c for c in app_w_interrupt.stream( - {"input": "what is weather in sf"}, config, checkpoint_during=False + {"input": "what is weather in sf"}, config, durability="exit" ) ] == [ { @@ -836,7 +835,7 @@ def test_conditional_state_graph( assert [ c for c in app_w_interrupt.stream( - {"input": "what is weather in sf"}, config, checkpoint_during=False + {"input": "what is weather in sf"}, config, durability="exit" ) ] == [ { @@ -1003,7 +1002,7 @@ def test_conditional_state_graph( assert [ c for c in app_w_interrupt.stream( - {"input": "what is weather in sf"}, config, checkpoint_during=False + {"input": "what is weather in sf"}, config, durability="exit" ) ] == [ {"__interrupt__": ()}, @@ -1150,7 +1149,7 @@ def test_conditional_state_graph( assert [ c for c in app_w_interrupt.stream( - {"input": "what is weather in sf"}, config, checkpoint_during=False + {"input": "what is weather in sf"}, config, durability="exit" ) ] == [ { @@ -1851,7 +1850,7 @@ def test_state_graph_packets( for c in app_w_interrupt.stream( {"messages": HumanMessage(content="what is weather in sf")}, config, - checkpoint_during=False, + durability="exit", ) ] == [ { @@ -2116,7 +2115,7 @@ def test_state_graph_packets( for c in app_w_interrupt.stream( {"messages": HumanMessage(content="what is weather in sf")}, config, - checkpoint_during=False, + durability="exit", ) ] == [ { @@ -2584,7 +2583,7 @@ def test_message_graph( assert [ c for c in app_w_interrupt.stream( - ("human", "what is weather in sf"), config, checkpoint_during=False + ("human", "what is weather in sf"), config, durability="exit" ) ] == [ { @@ -2809,7 +2808,7 @@ def test_message_graph( assert [ c for c in app_w_interrupt.stream( - "what is weather in sf", config, checkpoint_during=False + "what is weather in sf", config, durability="exit" ) ] == [ { @@ -3306,7 +3305,7 @@ def test_root_graph( assert [ c for c in app_w_interrupt.stream( - ("human", "what is weather in sf"), config, checkpoint_during=False + ("human", "what is weather in sf"), config, durability="exit" ) ] == [ { @@ -3533,7 +3532,7 @@ def test_root_graph( assert [ c for c in app_w_interrupt.stream( - "what is weather in sf", config, checkpoint_during=False + "what is weather in sf", config, durability="exit" ) ] == [ { @@ -4173,9 +4172,7 @@ def test_dynamic_interrupt(sync_checkpointer: BaseCheckpointSaver) -> None: ) == { "my_key": "value", "market": "DE", - "__interrupt__": [ - Interrupt(value="Just because...", resumable=True, ns=[AnyStr("tool_two:")]) - ], + "__interrupt__": [Interrupt(value="Just because...", id=AnyStr())], } assert tool_two_node_count == 1, "interrupts aren't retried" assert len(tracer.runs) == 1 @@ -4205,8 +4202,7 @@ def test_dynamic_interrupt(sync_checkpointer: BaseCheckpointSaver) -> None: "__interrupt__": ( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ) }, @@ -4220,13 +4216,11 @@ def test_dynamic_interrupt(sync_checkpointer: BaseCheckpointSaver) -> None: thread1 = {"configurable": {"thread_id": "1"}} # stop when about to enter node assert tool_two.invoke( - {"my_key": "value ⛰️", "market": "DE"}, thread1, checkpoint_during=False + {"my_key": "value ⛰️", "market": "DE"}, thread1, durability="exit" ) == { "my_key": "value ⛰️", "market": "DE", - "__interrupt__": [ - Interrupt(value="Just because...", resumable=True, ns=[AnyStr("tool_two:")]) - ], + "__interrupt__": [Interrupt(value="Just because...", id=AnyStr())], } assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ @@ -4248,8 +4242,7 @@ def test_dynamic_interrupt(sync_checkpointer: BaseCheckpointSaver) -> None: interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ), ), @@ -4271,8 +4264,7 @@ def test_dynamic_interrupt(sync_checkpointer: BaseCheckpointSaver) -> None: interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ), ) @@ -4336,9 +4328,7 @@ def test_partial_pending_checkpoint(sync_checkpointer: BaseCheckpointSaver) -> N ) == { "my_key": "value one", "market": "DE", - "__interrupt__": [ - Interrupt(value="Just because...", resumable=True, ns=[AnyStr("tool_two:")]) - ], + "__interrupt__": [Interrupt(value="Just because...", id=AnyStr())], } assert tool_two_node_count == 1, "interrupts aren't retried" assert len(tracer.runs) == 1 @@ -4371,8 +4361,7 @@ def test_partial_pending_checkpoint(sync_checkpointer: BaseCheckpointSaver) -> N "__interrupt__": ( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ) }, @@ -4387,13 +4376,11 @@ def test_partial_pending_checkpoint(sync_checkpointer: BaseCheckpointSaver) -> N thread1 = {"configurable": {"thread_id": "1"}} # stop when about to enter node assert tool_two.invoke( - {"my_key": "value ⛰️", "market": "DE"}, thread1, checkpoint_during=False + {"my_key": "value ⛰️", "market": "DE"}, thread1, durability="exit" ) == { "my_key": "value ⛰️ one", "market": "DE", - "__interrupt__": [ - Interrupt(value="Just because...", resumable=True, ns=[AnyStr("tool_two:")]) - ], + "__interrupt__": [Interrupt(value="Just because...", id=AnyStr())], } assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ { @@ -4420,8 +4407,7 @@ def test_partial_pending_checkpoint(sync_checkpointer: BaseCheckpointSaver) -> N interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ), ), @@ -4443,8 +4429,7 @@ def test_partial_pending_checkpoint(sync_checkpointer: BaseCheckpointSaver) -> N interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ), ) @@ -4513,8 +4498,7 @@ def test_dynamic_interrupt_subgraph(sync_checkpointer: BaseCheckpointSaver) -> N "__interrupt__": [ Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:"), AnyStr("do:")], + id=AnyStr(), ) ], } @@ -4546,8 +4530,7 @@ def test_dynamic_interrupt_subgraph(sync_checkpointer: BaseCheckpointSaver) -> N "__interrupt__": ( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:"), AnyStr("do:")], + id=AnyStr(), ), ) }, @@ -4561,15 +4544,14 @@ def test_dynamic_interrupt_subgraph(sync_checkpointer: BaseCheckpointSaver) -> N thread1 = {"configurable": {"thread_id": "1"}} # stop when about to enter node assert tool_two.invoke( - {"my_key": "value ⛰️", "market": "DE"}, thread1, checkpoint_during=False + {"my_key": "value ⛰️", "market": "DE"}, thread1, durability="exit" ) == { "my_key": "value ⛰️", "market": "DE", "__interrupt__": [ Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:"), AnyStr("do:")], + id=AnyStr(), ) ], } @@ -4598,8 +4580,7 @@ def test_dynamic_interrupt_subgraph(sync_checkpointer: BaseCheckpointSaver) -> N interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:"), AnyStr("do:")], + id=AnyStr(), ), ), state={ @@ -4627,8 +4608,7 @@ def test_dynamic_interrupt_subgraph(sync_checkpointer: BaseCheckpointSaver) -> N interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:"), AnyStr("do:")], + id=AnyStr(), ), ), ) @@ -4664,7 +4644,7 @@ def test_dynamic_interrupt_subgraph(sync_checkpointer: BaseCheckpointSaver) -> N def test_send_dedupe_on_resume( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class InterruptOnce: ticks: int = 0 @@ -4672,7 +4652,7 @@ def test_send_dedupe_on_resume( def __call__(self, state): self.ticks += 1 if self.ticks == 1: - raise NodeInterrupt("Bahh") + interrupt("Bahh") return ["|".join(("flaky", str(state)))] class Node: @@ -4718,12 +4698,11 @@ def test_send_dedupe_on_resume( graph = builder.compile(checkpointer=sync_checkpointer) thread1 = {"configurable": {"thread_id": "1"}} - assert graph.invoke(["0"], thread1, checkpoint_during=checkpoint_during) == { + assert graph.invoke(["0"], thread1, durability=durability) == { "__interrupt__": [ Interrupt( value="Bahh", - resumable=False, - ns=None, + id=AnyStr(), ), ], } @@ -4734,10 +4713,10 @@ def test_send_dedupe_on_resume( assert state.next == ("flaky",) # check history history = [c for c in graph.get_state_history(thread1)] - assert len(history) == (4 if checkpoint_during else 1) + assert len(history) == (4 if durability != "exit" else 1) # resume execution - assert graph.invoke(None, thread1, checkpoint_during=checkpoint_during) == [ + assert graph.invoke(None, thread1, durability=durability) == [ "0", "1", "3.1", @@ -4757,7 +4736,7 @@ def test_send_dedupe_on_resume( assert state.next == () # check history history = [c for c in graph.get_state_history(thread1)] - assert len(history) == (6 if checkpoint_during else 2) + assert len(history) == (6 if durability != "exit" else 2) expected_history = [ StateSnapshot( values=[ @@ -4884,9 +4863,9 @@ def test_send_dedupe_on_resume( name="flaky", path=("__pregel_push", 1, False), error=None, - interrupts=(Interrupt(value="Bahh", resumable=False, ns=None),), + interrupts=(Interrupt(value="Bahh", id=AnyStr()),), state=None, - result=["flaky|4"] if checkpoint_during else None, + result=["flaky|4"] if durability != "exit" else None, ), PregelTask( id=AnyStr(), @@ -4898,7 +4877,7 @@ def test_send_dedupe_on_resume( result=["3"], ), ), - interrupts=(Interrupt(value="Bahh", resumable=False, ns=None),), + interrupts=(Interrupt(value="Bahh", id=AnyStr()),), ), StateSnapshot( values=["0", "1"], @@ -5021,7 +5000,7 @@ def test_send_dedupe_on_resume( ), ), ] - if checkpoint_during: + if durability != "exit": assert history == expected_history else: assert history[0] == expected_history[0]._replace( @@ -5079,7 +5058,7 @@ def test_nested_graph_state(sync_checkpointer: BaseCheckpointSaver) -> None: app = graph.compile(checkpointer=sync_checkpointer) config = {"configurable": {"thread_id": "1"}} - app.invoke({"my_key": "my value"}, config, checkpoint_during=False) + app.invoke({"my_key": "my value"}, config, durability="exit") # test state w/ nested subgraph state (right after interrupt) # first get_state without subgraph state expected = StateSnapshot( @@ -5203,7 +5182,7 @@ def test_nested_graph_state(sync_checkpointer: BaseCheckpointSaver) -> None: assert child_history == expected_child_history # resume - app.invoke(None, config, checkpoint_during=False) + app.invoke(None, config, durability="exit") # test state w/ nested subgraph state (after resuming from interrupt) assert app.get_state(config) == StateSnapshot( values={"my_key": "hi my value here and there and back again"}, @@ -5359,7 +5338,7 @@ def test_doubly_nested_graph_state( assert [ c for c in app.stream( - {"my_key": "my value"}, config, subgraphs=True, checkpoint_during=False + {"my_key": "my value"}, config, subgraphs=True, durability="exit" ) ] == [ ((), {"parent_1": {"my_key": "hi my value"}}), @@ -5578,9 +5557,7 @@ def test_doubly_nested_graph_state( interrupts=(), ) # # resume - assert [ - c for c in app.stream(None, config, subgraphs=True, checkpoint_during=False) - ] == [ + assert [c for c in app.stream(None, config, subgraphs=True, durability="exit")] == [ ( (AnyStr("child:"), AnyStr("child_1:")), {"grandchild_2": {"my_key": "hi my value here and there"}}, @@ -5938,7 +5915,7 @@ def test_send_react_interrupt( graph = builder.compile(checkpointer=sync_checkpointer, interrupt_before=["foo"]) thread1 = {"configurable": {"thread_id": "2"}} assert graph.invoke( - {"messages": [HumanMessage("hello")]}, thread1, checkpoint_during=False + {"messages": [HumanMessage("hello")]}, thread1, durability="exit" ) == { "messages": [ _AnyIdHumanMessage(content="hello"), @@ -6062,7 +6039,7 @@ def test_send_react_interrupt( graph = builder.compile(checkpointer=sync_checkpointer, interrupt_before=["foo"]) thread1 = {"configurable": {"thread_id": "3"}} assert graph.invoke( - {"messages": [HumanMessage("hello")]}, thread1, checkpoint_during=False + {"messages": [HumanMessage("hello")]}, thread1, durability="exit" ) == { "messages": [ _AnyIdHumanMessage(content="hello"), @@ -6328,7 +6305,7 @@ def test_send_react_interrupt_control( graph = builder.compile(checkpointer=sync_checkpointer, interrupt_before=["foo"]) thread1 = {"configurable": {"thread_id": "2"}} assert graph.invoke( - {"messages": [HumanMessage("hello")]}, thread1, checkpoint_during=False + {"messages": [HumanMessage("hello")]}, thread1, durability="exit" ) == { "messages": [ _AnyIdHumanMessage(content="hello"), @@ -6587,7 +6564,7 @@ def test_weather_subgraph( config=config, stream_mode="updates", subgraphs=True, - checkpoint_during=False, + durability="exit", ) ] == [ ((), {"router_node": {"route": "weather"}}), @@ -6674,7 +6651,7 @@ def test_weather_subgraph( config=config, stream_mode="updates", subgraphs=True, - checkpoint_during=False, + durability="exit", ) ] == [ ((), {"router_node": {"route": "weather"}}), diff --git a/libs/langgraph/tests/test_large_cases_async.py b/libs/langgraph/tests/test_large_cases_async.py index 29f92d8b9..5e7245a40 100644 --- a/libs/langgraph/tests/test_large_cases_async.py +++ b/libs/langgraph/tests/test_large_cases_async.py @@ -16,10 +16,11 @@ from langchain_core.runnables import RunnableConfig, RunnablePick from pytest_mock import MockerFixture from typing_extensions import TypedDict +from langgraph._internal._constants import PULL, PUSH from langgraph.channels.last_value import LastValue from langgraph.channels.untracked_value import UntrackedValue from langgraph.checkpoint.base import BaseCheckpointSaver -from langgraph.constants import END, PULL, PUSH, START +from langgraph.constants import END, START from langgraph.graph.message import MessageGraph, add_messages from langgraph.graph.state import StateGraph from langgraph.prebuilt.chat_agent_executor import create_react_agent @@ -62,7 +63,7 @@ async def test_invoke_two_processes_in_out_interrupt( thread2 = {"configurable": {"thread_id": "2"}} # start execution, stop at inbox - assert await app.ainvoke(2, thread1, checkpoint_during=True) is None + assert await app.ainvoke(2, thread1, durability="async") is None # inbox == 3 checkpoint = await async_checkpointer.aget(thread1) @@ -70,10 +71,10 @@ async def test_invoke_two_processes_in_out_interrupt( assert checkpoint["channel_values"]["inbox"] == 3 # resume execution, finish - assert await app.ainvoke(None, thread1, checkpoint_during=True) == 4 + assert await app.ainvoke(None, thread1, durability="async") == 4 # start execution again, stop at inbox - assert await app.ainvoke(20, thread1, checkpoint_during=True) is None + assert await app.ainvoke(20, thread1, durability="async") is None # inbox == 21 checkpoint = await async_checkpointer.aget(thread1) @@ -81,11 +82,11 @@ async def test_invoke_two_processes_in_out_interrupt( assert checkpoint["channel_values"]["inbox"] == 21 # send a new value in, interrupting the previous execution - assert await app.ainvoke(3, thread1, checkpoint_during=True) is None - assert await app.ainvoke(None, thread1, checkpoint_during=True) == 5 + assert await app.ainvoke(3, thread1, durability="async") is None + assert await app.ainvoke(None, thread1, durability="async") == 5 # start execution again, stopping at inbox - assert await app.ainvoke(20, thread2, checkpoint_during=True) is None + assert await app.ainvoke(20, thread2, durability="async") is None # inbox == 21 snapshot = await app.aget_state(thread2) @@ -300,7 +301,7 @@ async def test_fork_always_re_runs_nodes( assert [ c async for c in graph.astream( - 1, thread1, stream_mode=["values", "updates"], checkpoint_during=True + 1, thread1, stream_mode=["values", "updates"], durability="async" ) ] == [ ("values", 1), @@ -683,7 +684,7 @@ async def test_conditional_graph_state(async_checkpointer: BaseCheckpointSaver) assert [ c async for c in app_w_interrupt.astream( - {"input": "what is weather in sf"}, config, checkpoint_during=False + {"input": "what is weather in sf"}, config, durability="exit" ) ] == [ { @@ -858,7 +859,7 @@ async def test_conditional_graph_state(async_checkpointer: BaseCheckpointSaver) assert [ c async for c in app_w_interrupt.astream( - {"input": "what is weather in sf"}, config, checkpoint_during=False + {"input": "what is weather in sf"}, config, durability="exit" ) ] == [ { @@ -1576,7 +1577,7 @@ async def test_state_graph_packets(async_checkpointer: BaseCheckpointSaver) -> N async for c in app_w_interrupt.astream( {"messages": HumanMessage(content="what is weather in sf")}, config, - checkpoint_during=False, + durability="exit", ) ] == [ { @@ -1827,7 +1828,7 @@ async def test_state_graph_packets(async_checkpointer: BaseCheckpointSaver) -> N async for c in app_w_interrupt.astream( {"messages": HumanMessage(content="what is weather in sf")}, config, - checkpoint_during=False, + durability="exit", ) ] == [ { @@ -2256,7 +2257,7 @@ async def test_message_graph(async_checkpointer: BaseCheckpointSaver) -> None: async for c in app_w_interrupt.astream( HumanMessage(content="what is weather in sf"), config, - checkpoint_during=False, + durability="exit", ) ] == [ { @@ -2739,7 +2740,7 @@ async def test_nested_graph_state(async_checkpointer: BaseCheckpointSaver) -> No app = graph.compile(checkpointer=async_checkpointer) config = {"configurable": {"thread_id": "1"}} - await app.ainvoke({"my_key": "my value"}, config, checkpoint_during=False) + await app.ainvoke({"my_key": "my value"}, config, durability="exit") # test state w/ nested subgraph state (right after interrupt) # first get_state without subgraph state expected = StateSnapshot( @@ -2870,7 +2871,7 @@ async def test_nested_graph_state(async_checkpointer: BaseCheckpointSaver) -> No assert child_history == expected_child_history # resume - await app.ainvoke(None, config, checkpoint_during=False) + await app.ainvoke(None, config, durability="exit") # 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"}, @@ -3029,7 +3030,7 @@ async def test_doubly_nested_graph_state( assert [ c async for c in app.astream( - {"my_key": "my value"}, config, subgraphs=True, checkpoint_during=False + {"my_key": "my value"}, config, subgraphs=True, durability="exit" ) ] == [ ((), {"parent_1": {"my_key": "hi my value"}}), @@ -3249,10 +3250,7 @@ async def test_doubly_nested_graph_state( ) # resume assert [ - c - async for c in app.astream( - None, config, subgraphs=True, checkpoint_during=False - ) + c async for c in app.astream(None, config, subgraphs=True, durability="exit") ] == [ ( (AnyStr("child:"), AnyStr("child_1:")), @@ -3661,7 +3659,7 @@ async def test_weather_subgraph( config=config, stream_mode="updates", subgraphs=True, - checkpoint_during=False, + durability="exit", ) ] == [ ((), {"router_node": {"route": "weather"}}), @@ -3750,7 +3748,7 @@ async def test_weather_subgraph( config=config, stream_mode="updates", subgraphs=True, - checkpoint_during=False, + durability="exit", ) ] == [ ((), {"router_node": {"route": "weather"}}), diff --git a/libs/langgraph/tests/test_messages_state.py b/libs/langgraph/tests/test_messages_state.py index a481123a4..0a1e78ecb 100644 --- a/libs/langgraph/tests/test_messages_state.py +++ b/libs/langgraph/tests/test_messages_state.py @@ -14,9 +14,10 @@ from langchain_core.messages import ( from pydantic import BaseModel from typing_extensions import TypedDict +from langgraph.constants import END, START from langgraph.graph import add_messages from langgraph.graph.message import REMOVE_ALL_MESSAGES, MessagesState, push_message -from langgraph.graph.state import END, START, StateGraph +from langgraph.graph.state import StateGraph from tests.messages import _AnyIdHumanMessage _, CORE_MINOR, CORE_PATCH = (int(v) for v in langchain_core.__version__.split(".")) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 9df854e0e..cd01a3388 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -7,7 +7,6 @@ import operator import threading import time import uuid -import warnings from collections import Counter, deque from collections.abc import Sequence from concurrent.futures import ThreadPoolExecutor @@ -29,6 +28,7 @@ from pytest_mock import MockerFixture from syrupy import SnapshotAssertion from typing_extensions import TypedDict +from langgraph._internal._constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL from langgraph.cache.base import BaseCache from langgraph.channels.binop import BinaryOperatorAggregate from langgraph.channels.ephemeral_value import EphemeralValue @@ -42,28 +42,27 @@ from langgraph.checkpoint.base import ( ) from langgraph.checkpoint.memory import InMemorySaver from langgraph.config import get_stream_writer -from langgraph.constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL, START -from langgraph.errors import InvalidUpdateError, ParentCommand +from langgraph.errors import GraphRecursionError, InvalidUpdateError, ParentCommand from langgraph.func import entrypoint, task -from langgraph.graph import END, StateGraph +from langgraph.graph import END, START, StateGraph from langgraph.graph.message import MessageGraph, MessagesState, add_messages from langgraph.prebuilt.tool_node import ToolNode from langgraph.pregel import ( - GraphRecursionError, NodeBuilder, Pregel, - StateSnapshot, ) -from langgraph.pregel.loop import SyncPregelLoop -from langgraph.pregel.retry import RetryPolicy -from langgraph.pregel.runner import PregelRunner +from langgraph.pregel._loop import SyncPregelLoop +from langgraph.pregel._runner import PregelRunner from langgraph.store.base import BaseStore from langgraph.types import ( CachePolicy, Command, + Durability, Interrupt, PregelTask, + RetryPolicy, Send, + StateSnapshot, StateUpdate, StreamWriter, interrupt, @@ -187,11 +186,11 @@ def test_checkpoint_errors() -> None: graph = builder.compile(checkpointer=FaultyPutWritesCheckpointer()) with pytest.raises(ValueError, match="Faulty put_writes"): graph.invoke( - "", {"configurable": {"thread_id": "thread-1"}}, checkpoint_during=True + "", {"configurable": {"thread_id": "thread-1"}}, durability="async" ) -def test_config_json_schema() -> None: +def test_context_json_schema() -> None: """Test that config json schema is generated properly.""" chain = NodeBuilder().subscribe_only("input").write_to("output") @@ -211,37 +210,25 @@ def test_config_json_schema() -> None: }, input_channels=["input", "ephemeral"], output_channels="output", - config_type=Foo, + context_schema=Foo, ) - assert app.get_config_jsonschema() == { - "$defs": { - "Foo": { - "properties": { - "x": { - "title": "X", - "type": "integer", - }, - "y": { - "default": "foo", - "title": "Y", - "type": "string", - }, - }, - "required": [ - "x", - ], - "title": "Foo", - "type": "object", - }, - }, + assert app.get_context_jsonschema() == { "properties": { - "configurable": { - "$ref": "#/$defs/Foo", - "default": None, + "x": { + "title": "X", + "type": "integer", + }, + "y": { + "default": "foo", + "title": "Y", + "type": "string", }, }, - "title": "LangGraphConfig", + "required": [ + "x", + ], + "title": "Foo", "type": "object", } @@ -424,13 +411,7 @@ def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: "title": "LangGraphOutput", "type": "integer", } - with warnings.catch_warnings(): - warnings.simplefilter("error") # raise warnings as errors - assert app.config_schema().model_json_schema() == { - "properties": {}, - "title": "LangGraphConfig", - "type": "object", - } + assert app.get_context_jsonschema() is None assert app.invoke(2) == 3 assert app.invoke(2, output_keys=["output"]) == {"output": 3} @@ -590,7 +571,7 @@ def test_run_from_checkpoint_id_retains_previous_writes( thread_id = uuid.uuid4() thread1 = {"configurable": {"thread_id": str(thread_id)}} - result = graph.invoke({"myval": 1}, thread1, checkpoint_during=True) + result = graph.invoke({"myval": 1}, thread1, durability="async") assert result["myval"] == 4 history = [c for c in graph.get_state_history(thread1)] @@ -847,7 +828,7 @@ def test_invoke_checkpoint_two( def test_pending_writes_resume( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class State(TypedDict): value: Annotated[int, operator.add] @@ -884,7 +865,7 @@ def test_pending_writes_resume( thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} with pytest.raises(ConnectionError, match="I'm not good"): - graph.invoke({"value": 1}, thread1, checkpoint_during=checkpoint_during) + graph.invoke({"value": 1}, thread1, durability=durability) # both nodes should have been called once assert one.calls == 1 @@ -928,7 +909,7 @@ def test_pending_writes_resume( # resume execution with pytest.raises(ConnectionError, match="I'm not good"): - graph.invoke(None, thread1, checkpoint_during=checkpoint_during) + graph.invoke(None, thread1, durability=durability) # node "one" succeeded previously, so shouldn't be called again assert one.calls == 1 @@ -942,14 +923,12 @@ def test_pending_writes_resume( # resume execution, without exception two.rtn = {"value": 3} # both the pending write and the new write were applied, 1 + 2 + 3 = 6 - assert graph.invoke(None, thread1, checkpoint_during=checkpoint_during) == { - "value": 6 - } + assert graph.invoke(None, thread1, durability=durability) == {"value": 6} # check all final checkpoints checkpoints = [c for c in sync_checkpointer.list(thread1)] # we should have 3 - assert len(checkpoints) == (3 if checkpoint_during else 2) + assert len(checkpoints) == (3 if durability != "exit" else 2) # the last one not too interesting for this test assert checkpoints[0] == CheckpointTuple( config={ @@ -1050,7 +1029,7 @@ def test_pending_writes_resume( ), } } - if checkpoint_during + if durability != "exit" else None, pending_writes=( UnsortedSequence( @@ -1058,7 +1037,7 @@ def test_pending_writes_resume( (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), (AnyStr(), "value", 3), ) - if checkpoint_during + if durability != "exit" else UnsortedSequence( (AnyStr(), "value", 2), (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), @@ -1067,7 +1046,7 @@ def test_pending_writes_resume( ) ), ) - if not checkpoint_during: + if durability == "exit": return assert checkpoints[2] == CheckpointTuple( config={ @@ -1224,11 +1203,11 @@ def test_send_sequences() -> None: def test_imp_task( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: mapper_calls = 0 - class Configurable(TypedDict): + class Context(TypedDict): model: str @task() @@ -1238,7 +1217,7 @@ def test_imp_task( time.sleep(input / 100) return str(input) * 2 - @entrypoint(checkpointer=sync_checkpointer, config_schema=Configurable) + @entrypoint(checkpointer=sync_checkpointer, context_schema=Context) def graph(input: list[int]) -> list[str]: futures = [mapper(i) for i in input] mapped = [f.result() for f in futures] @@ -1255,44 +1234,29 @@ def test_imp_task( "items": {"type": "string"}, "title": "LangGraphOutput", } - assert graph.get_config_jsonschema() == { - "$defs": { - "Configurable": { - "properties": { - "model": {"title": "Model", "type": "string"}, - }, - "required": ["model"], - "title": "Configurable", - "type": "object", - } - }, - "properties": { - "configurable": {"$ref": "#/$defs/Configurable", "default": None} - }, - "title": "LangGraphConfig", + assert graph.get_context_jsonschema() == { + "properties": {"model": {"title": "Model", "type": "string"}}, + "required": ["model"], + "title": "Context", "type": "object", } thread1 = {"configurable": {"thread_id": "1"}} - assert [*graph.stream([0, 1], thread1, checkpoint_during=checkpoint_during)] == [ + assert [*graph.stream([0, 1], thread1, durability=durability)] == [ {"mapper": "00"}, {"mapper": "11"}, { "__interrupt__": ( Interrupt( value="question", - resumable=True, - ns=[AnyStr("graph:")], - when="during", + id=AnyStr(), ), ) }, ] assert mapper_calls == 2 - assert graph.invoke( - Command(resume="answer"), thread1, checkpoint_during=checkpoint_during - ) == [ + assert graph.invoke(Command(resume="answer"), thread1, durability=durability) == [ "00answer", "11answer", ] @@ -1300,7 +1264,7 @@ def test_imp_task( def test_imp_nested( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: def mynode(input: list[str]) -> list[str]: return [it + "a" for it in input] @@ -1341,7 +1305,7 @@ def test_imp_nested( } thread1 = {"configurable": {"thread_id": "1"}} - assert [*graph.stream([0, 1], thread1, checkpoint_during=checkpoint_during)] == [ + assert [*graph.stream([0, 1], thread1, durability=durability)] == [ {"submapper": "0"}, {"mapper": "00"}, {"submapper": "1"}, @@ -1350,24 +1314,20 @@ def test_imp_nested( "__interrupt__": ( Interrupt( value="question", - resumable=True, - ns=[AnyStr("graph:")], - when="during", + id=AnyStr(), ), ) }, ] - assert graph.invoke( - Command(resume="answer"), thread1, checkpoint_during=checkpoint_during - ) == [ + assert graph.invoke(Command(resume="answer"), thread1, durability=durability) == [ "00answera", "11answera", ] def test_imp_stream_order( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: @task() def foo(state: dict) -> tuple: @@ -1389,10 +1349,7 @@ def test_imp_stream_order( return fut_baz.result() thread1 = {"configurable": {"thread_id": "1"}} - assert [ - c - for c in graph.stream({"a": "0"}, thread1, checkpoint_during=checkpoint_during) - ] == [ + assert [c for c in graph.stream({"a": "0"}, thread1, durability=durability)] == [ { "foo": ( "0foo", @@ -1440,7 +1397,7 @@ def test_invoke_checkpoint_three( thread_1 = {"configurable": {"thread_id": "1"}} # total starts out as 0, so output is 0+2=2 - assert app.invoke(2, thread_1, checkpoint_during=True) == 2 + assert app.invoke(2, thread_1, durability="async") == 2 state = app.get_state(thread_1) assert state is not None assert state.values.get("total") == 2 @@ -1450,7 +1407,7 @@ def test_invoke_checkpoint_three( == sync_checkpointer.get(thread_1)["id"] ) # total is now 2, so output is 2+3=5 - assert app.invoke(3, thread_1, checkpoint_during=True) == 5 + assert app.invoke(3, thread_1, durability="async") == 5 state = app.get_state(thread_1) assert state is not None assert state.values.get("total") == 7 @@ -1460,7 +1417,7 @@ def test_invoke_checkpoint_three( ) # 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_during=True) + app.invoke(4, thread_1, durability="async") # checkpoint is updated with new input state = app.get_state(thread_1) assert state is not None @@ -1468,7 +1425,7 @@ def test_invoke_checkpoint_three( 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, checkpoint_during=True) == 9 + assert app.invoke(2, thread_1, durability="async") == 9 state = app.get_state(thread_1) assert state is not None assert state.values.get("total") == 16, "total is now 7+9=16" @@ -1775,7 +1732,7 @@ def test_state_graph_w_config_inherited_state_keys(snapshot: SnapshotAssertion) "intermediate_steps", } - class Config(TypedDict, total=False): + class Context(TypedDict, total=False): tools: list[str] # Assemble the tools @@ -1832,7 +1789,7 @@ def test_state_graph_w_config_inherited_state_keys(snapshot: SnapshotAssertion) return "continue" # Define a new graph - builder = StateGraph(AgentState, Config) + builder = StateGraph(AgentState, Context) builder.add_node("agent", agent) builder.add_node("tools", execute_tools) @@ -1847,7 +1804,7 @@ def test_state_graph_w_config_inherited_state_keys(snapshot: SnapshotAssertion) app = builder.compile() - assert json.dumps(app.config_schema().model_json_schema()) == snapshot + assert json.dumps(app.get_context_jsonschema()) == snapshot assert json.dumps(app.get_input_jsonschema()) == snapshot assert json.dumps(app.get_output_jsonschema()) == snapshot @@ -3211,7 +3168,7 @@ def test_nested_graph(snapshot: SnapshotAssertion) -> None: def test_subgraph_checkpoint_true( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class InnerState(TypedDict): my_key: Annotated[str, operator.add] @@ -3245,7 +3202,7 @@ def test_subgraph_checkpoint_true( assert [ c for c in app.stream( - {"my_key": ""}, config, subgraphs=True, checkpoint_during=checkpoint_during + {"my_key": ""}, config, subgraphs=True, durability=durability ) ] == [ (("inner",), {"inner_1": {"my_key": " got here", "my_other_key": ""}}), @@ -3272,13 +3229,13 @@ def test_subgraph_checkpoint_true( ] checkpoints = list(app.get_state_history(config)) - if checkpoint_during: + if durability != "exit": assert len(checkpoints) == 4 else: assert len(checkpoints) == 1 -def test_subgraph_checkpoint_during_false_inherited() -> None: +def test_subgraph_durability_inherited(durability: Durability) -> None: sync_checkpointer = InMemorySaver() class InnerState(TypedDict): @@ -3309,22 +3266,19 @@ def test_subgraph_checkpoint_during_false_inherited() -> None: "inner", lambda s: "inner" if s["my_key"].count("there") < 2 else END ) app = graph.compile(checkpointer=sync_checkpointer) - for checkpoint_during in [True, False]: - thread_id = str(uuid.uuid4()) - config = {"configurable": {"thread_id": thread_id}} - app.invoke( - {"my_key": ""}, config, subgraphs=True, checkpoint_during=checkpoint_during - ) - if checkpoint_during: - checkpoints = list(sync_checkpointer.list(config)) - assert len(checkpoints) == 12 - else: - checkpoints = list(sync_checkpointer.list(config)) - assert len(checkpoints) == 1 + thread_id = str(uuid.uuid4()) + config = {"configurable": {"thread_id": thread_id}} + app.invoke({"my_key": ""}, config, subgraphs=True, durability=durability) + if durability != "exit": + checkpoints = list(sync_checkpointer.list(config)) + assert len(checkpoints) == 12 + else: + checkpoints = list(sync_checkpointer.list(config)) + assert len(checkpoints) == 1 def test_subgraph_checkpoint_true_interrupt( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: # Define subgraph class SubgraphState(TypedDict): @@ -3365,24 +3319,21 @@ def test_subgraph_checkpoint_true_interrupt( graph = builder.compile(checkpointer=sync_checkpointer) config = {"configurable": {"thread_id": "1"}} - assert graph.invoke( - {"foo": "foo"}, config, checkpoint_during=checkpoint_during - ) == { + assert graph.invoke({"foo": "foo"}, config, durability=durability) == { "foo": "hi! foo", "__interrupt__": [ Interrupt( value="Provide baz value", - resumable=True, - ns=[AnyStr("node_2"), AnyStr("subgraph_node_1:")], + id=AnyStr(), ) ], } assert graph.get_state(config, subgraphs=True).tasks[0].state.values == { "bar": "hi! foo" } - assert graph.invoke( - Command(resume="baz"), config, checkpoint_during=checkpoint_during - ) == {"foo": "hi! foobaz"} + assert graph.invoke(Command(resume="baz"), config, durability=durability) == { + "foo": "hi! foobaz" + } def test_stream_subgraphs_during_execution( @@ -3491,7 +3442,7 @@ def test_stream_buffering_single_node(sync_checkpointer: BaseCheckpointSaver) -> def test_nested_graph_interrupts_parallel( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class InnerState(TypedDict): my_key: Annotated[str, operator.add] @@ -3537,11 +3488,11 @@ def test_nested_graph_interrupts_parallel( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} - assert app.invoke({"my_key": ""}, config, checkpoint_during=checkpoint_during) == { + assert app.invoke({"my_key": ""}, config, durability=durability) == { "my_key": " and parallel", } - assert app.invoke(None, config, checkpoint_during=checkpoint_during) == { + assert app.invoke(None, config, durability=durability) == { "my_key": "got here and there and parallel and back again", } @@ -3551,16 +3502,14 @@ def test_nested_graph_interrupts_parallel( # test stream updates w/ nested interrupt config = {"configurable": {"thread_id": "2"}} assert [ - *app.stream( - {"my_key": ""}, config, subgraphs=True, checkpoint_during=checkpoint_during - ) + *app.stream({"my_key": ""}, config, subgraphs=True, durability=durability) ] == [ # we got to parallel node first ((), {"outer_1": {"my_key": " and parallel"}}), ((AnyStr("inner:"),), {"inner_1": {"my_key": "got here", "my_other_key": ""}}), ((), {"__interrupt__": ()}), ] - assert [*app.stream(None, config, checkpoint_during=checkpoint_during)] == [ + assert [*app.stream(None, config, durability=durability)] == [ {"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}}, {"inner": {"my_key": "got here and there"}}, {"outer_2": {"my_key": " and back again"}}, @@ -3573,17 +3522,13 @@ def test_nested_graph_interrupts_parallel( {"my_key": ""}, config, stream_mode="values", - checkpoint_during=checkpoint_during, + durability=durability, ) ] == [ {"my_key": ""}, {"my_key": " and parallel"}, ] - assert [ - *app.stream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during - ) - ] == [ + assert [*app.stream(None, config, stream_mode="values", durability=durability)] == [ {"my_key": ""}, {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, @@ -3597,23 +3542,15 @@ def test_nested_graph_interrupts_parallel( {"my_key": ""}, config, stream_mode="values", - checkpoint_during=checkpoint_during, + durability=durability, ) ] == [{"my_key": ""}] # while we're waiting for the node w/ interrupt inside to finish - assert [ - *app.stream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during - ) - ] == [ + assert [*app.stream(None, config, stream_mode="values", durability=durability)] == [ {"my_key": ""}, {"my_key": " and parallel"}, ] - assert [ - *app.stream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during - ) - ] == [ + assert [*app.stream(None, config, stream_mode="values", durability=durability)] == [ {"my_key": ""}, {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, @@ -3627,32 +3564,24 @@ def test_nested_graph_interrupts_parallel( {"my_key": ""}, config, stream_mode="values", - checkpoint_during=checkpoint_during, + durability=durability, ) ] == [ {"my_key": ""}, {"my_key": " and parallel"}, ] - assert [ - *app.stream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during - ) - ] == [ + assert [*app.stream(None, config, stream_mode="values", durability=durability)] == [ {"my_key": ""}, {"my_key": "got here and there and parallel"}, ] - assert [ - *app.stream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during - ) - ] == [ + assert [*app.stream(None, config, stream_mode="values", durability=durability)] == [ {"my_key": "got here and there and parallel"}, {"my_key": "got here and there and parallel and back again"}, ] def test_doubly_nested_graph_interrupts( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class State(TypedDict): my_key: str @@ -3705,13 +3634,11 @@ def test_doubly_nested_graph_interrupts( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} - assert app.invoke( - {"my_key": "my value"}, config, checkpoint_during=checkpoint_during - ) == { + assert app.invoke({"my_key": "my value"}, config, durability=durability) == { "my_key": "hi my value", } - assert app.invoke(None, config, checkpoint_during=checkpoint_during) == { + assert app.invoke(None, config, durability=durability) == { "my_key": "hi my value here and there and back again", } @@ -3720,14 +3647,12 @@ def test_doubly_nested_graph_interrupts( config = { "configurable": {"thread_id": "2", CONFIG_KEY_NODE_FINISHED: nodes.append} } - assert [ - *app.stream({"my_key": "my value"}, config, checkpoint_during=checkpoint_during) - ] == [ + assert [*app.stream({"my_key": "my value"}, config, durability=durability)] == [ {"parent_1": {"my_key": "hi my value"}}, {"__interrupt__": ()}, ] assert nodes == ["parent_1", "grandchild_1"] - assert [*app.stream(None, config, checkpoint_during=checkpoint_during)] == [ + assert [*app.stream(None, config, durability=durability)] == [ {"child": {"my_key": "hi my value here and there"}}, {"parent_2": {"my_key": "hi my value here and there and back again"}}, ] @@ -3747,17 +3672,13 @@ def test_doubly_nested_graph_interrupts( {"my_key": "my value"}, config, stream_mode="values", - checkpoint_during=checkpoint_during, + durability=durability, ) ] == [ {"my_key": "my value"}, {"my_key": "hi my value"}, ] - assert [ - *app.stream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during - ) - ] == [ + assert [*app.stream(None, config, stream_mode="values", durability=durability)] == [ {"my_key": "hi my value"}, {"my_key": "hi my value here and there"}, {"my_key": "hi my value here and there and back again"}, @@ -4408,7 +4329,7 @@ def test_debug_retry(sync_checkpointer: BaseCheckpointSaver): graph = builder.compile(checkpointer=sync_checkpointer) config = {"configurable": {"thread_id": "1"}} - graph.invoke({"messages": []}, config=config, checkpoint_during=True) + graph.invoke({"messages": []}, config=config, durability="async") # re-run step: 1 target_config = next( @@ -4420,7 +4341,7 @@ def test_debug_retry(sync_checkpointer: BaseCheckpointSaver): events = [ *graph.stream( - None, config=update_config, stream_mode="debug", checkpoint_during=True + None, config=update_config, stream_mode="debug", durability="async" ) ] @@ -4453,7 +4374,7 @@ def test_debug_retry(sync_checkpointer: BaseCheckpointSaver): def test_debug_subgraphs( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ): class State(TypedDict): messages: Annotated[list[str], operator.add] @@ -4487,14 +4408,14 @@ def test_debug_subgraphs( {"messages": []}, config=config, stream_mode="debug", - checkpoint_during=checkpoint_during, + durability=durability, ) ] checkpoint_events = list( reversed([e["payload"] for e in events if e["type"] == "checkpoint"]) ) - if not checkpoint_during: + if durability == "exit": checkpoint_events = checkpoint_events[:1] checkpoint_history = list(graph.get_state_history(config)) @@ -4525,7 +4446,7 @@ def test_debug_subgraphs( def test_debug_nested_subgraphs( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ): from collections import defaultdict @@ -4569,7 +4490,7 @@ def test_debug_nested_subgraphs( config=config, stream_mode="debug", subgraphs=True, - checkpoint_during=checkpoint_during, + durability=durability, ) ] @@ -4609,9 +4530,9 @@ def test_debug_nested_subgraphs( for checkpoint_events, checkpoint_history, ns in zip( stream_ns.values(), history_ns.values(), stream_ns.keys() ): - if not checkpoint_during: + if durability == "exit": checkpoint_events = checkpoint_events[-1:] - if ns: # Save no checkpoints for subgraphs when checkpoint_during=False + if ns: # Save no checkpoints for subgraphs when durability="exit" assert not checkpoint_history continue assert len(checkpoint_events) == len(checkpoint_history) @@ -4813,7 +4734,7 @@ def test_parent_command( config = {"configurable": {"thread_id": "1"}} assert graph.invoke( - {"messages": [("user", "get user name")]}, config, checkpoint_during=False + {"messages": [("user", "get user name")]}, config, durability="exit" ) == { "messages": [ _AnyIdHumanMessage( @@ -4901,9 +4822,7 @@ def test_interrupt_multiple(sync_checkpointer: BaseCheckpointSaver): "__interrupt__": ( Interrupt( value={"value": 1}, - resumable=True, - ns=[AnyStr("node:")], - when="during", + id=AnyStr(), ), ) } @@ -4919,9 +4838,7 @@ def test_interrupt_multiple(sync_checkpointer: BaseCheckpointSaver): "__interrupt__": ( Interrupt( value={"value": 2}, - resumable=True, - ns=[AnyStr("node:")], - when="during", + id=AnyStr(), ), ) } @@ -4969,9 +4886,7 @@ def test_interrupt_loop(sync_checkpointer: BaseCheckpointSaver): "__interrupt__": ( Interrupt( value="How old are you?", - resumable=True, - ns=[AnyStr("node:")], - when="during", + id=AnyStr(), ), ) } @@ -4988,9 +4903,7 @@ def test_interrupt_loop(sync_checkpointer: BaseCheckpointSaver): "__interrupt__": ( Interrupt( value="invalid response", - resumable=True, - ns=[AnyStr("node:")], - when="during", + id=AnyStr(), ), ) } @@ -5007,9 +4920,7 @@ def test_interrupt_loop(sync_checkpointer: BaseCheckpointSaver): "__interrupt__": ( Interrupt( value="invalid response", - resumable=True, - ns=[AnyStr("node:")], - when="during", + id=AnyStr(), ), ) } @@ -5045,8 +4956,7 @@ def test_interrupt_functional( "__interrupt__": [ Interrupt( value="Provide value for bar:", - resumable=True, - ns=[AnyStr("graph:")], + id=AnyStr(), ) ] } @@ -5079,8 +4989,7 @@ def test_interrupt_task_functional( "__interrupt__": [ Interrupt( value="Provide value for bar:", - resumable=True, - ns=[AnyStr("graph:"), AnyStr("bar:")], + id=AnyStr(), ), ] } @@ -5103,8 +5012,7 @@ def test_interrupt_task_functional( "__interrupt__": [ Interrupt( value="Provide value for bar:", - resumable=True, - ns=[AnyStr("graph:"), AnyStr("bar:")], + id=AnyStr(), ), ] } @@ -5412,7 +5320,7 @@ def test_concurrent_execution_thread_safety(): def test_checkpoint_recovery( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ): """Test recovery from checkpoints after failures.""" @@ -5443,7 +5351,7 @@ def test_checkpoint_recovery( graph.invoke( {"steps": ["start"], "attempt": 1}, config, - checkpoint_during=checkpoint_during, + durability=durability, ) # Verify checkpoint state @@ -5454,14 +5362,12 @@ def test_checkpoint_recovery( assert "RuntimeError('Simulated failure')" in state.tasks[0].error # Retry with updated attempt count - result = graph.invoke( - {"steps": [], "attempt": 2}, config, checkpoint_during=checkpoint_during - ) + result = graph.invoke({"steps": [], "attempt": 2}, config, durability=durability) assert result == {"steps": ["start", "node1", "node2"], "attempt": 2} # Verify checkpoint history shows both attempts history = list(graph.get_state_history(config)) - if checkpoint_during: + if durability != "exit": assert len(history) == 6 # Initial + failed attempt + successful attempt else: assert len(history) == 2 # error + success @@ -5544,7 +5450,7 @@ def test_falsy_return_from_task(sync_checkpointer: BaseCheckpointSaver): assert [ chunk for chunk in graph.stream( - {"a": 5}, configurable, stream_mode="debug", checkpoint_during=False + {"a": 5}, configurable, stream_mode="debug", durability="exit" ) ] == [ { @@ -5629,12 +5535,8 @@ def test_falsy_return_from_task(sync_checkpointer: BaseCheckpointSaver): "id": AnyStr(), "interrupts": [ { - "ns": [ - AnyStr(), - ], - "resumable": True, + "id": AnyStr(), "value": "test", - "when": "during", }, ], "name": "graph", @@ -5651,7 +5553,7 @@ def test_falsy_return_from_task(sync_checkpointer: BaseCheckpointSaver): Command(resume="123"), configurable, stream_mode="debug", - checkpoint_during=False, + durability="exit", ) ] == [ { @@ -5677,12 +5579,8 @@ def test_falsy_return_from_task(sync_checkpointer: BaseCheckpointSaver): "id": AnyStr(), "interrupts": ( { - "ns": [ - AnyStr(), - ], - "resumable": True, + "id": AnyStr(), "value": "test", - "when": "during", }, ), "name": "graph", @@ -5902,9 +5800,7 @@ def test_double_interrupt_subgraph(sync_checkpointer: BaseCheckpointSaver) -> No "__interrupt__": ( Interrupt( value="interrupt node 1", - resumable=True, - ns=[AnyStr("node_1:")], - when="during", + id=AnyStr(), ), ) }, @@ -5918,9 +5814,7 @@ def test_double_interrupt_subgraph(sync_checkpointer: BaseCheckpointSaver) -> No "__interrupt__": ( Interrupt( value="interrupt node 2", - resumable=True, - ns=[AnyStr("node_2:")], - when="during", + id=AnyStr(), ), ) }, @@ -5951,9 +5845,7 @@ def test_double_interrupt_subgraph(sync_checkpointer: BaseCheckpointSaver) -> No "__interrupt__": ( Interrupt( value="interrupt node 1", - resumable=True, - ns=[AnyStr("invoke_sub_agent:"), AnyStr("node_1:")], - when="during", + id=AnyStr(), ), ) }, @@ -5965,9 +5857,7 @@ def test_double_interrupt_subgraph(sync_checkpointer: BaseCheckpointSaver) -> No "__interrupt__": ( Interrupt( value="interrupt node 2", - resumable=True, - ns=[AnyStr("invoke_sub_agent:"), AnyStr("node_2:")], - when="during", + id=AnyStr(), ), ) } @@ -6046,7 +5936,7 @@ def test_multi_resume(sync_checkpointer: BaseCheckpointSaver) -> None: assert interrupt_values == set(prompts) resume_map: dict[str, str] = { - i.interrupt_id: f"human input for prompt {i.value}" + i.id: f"human input for prompt {i.value}" for i in parent_graph.get_state(thread_config).interrupts } @@ -7124,8 +7014,7 @@ def test_interrupt_subgraph_reenter_checkpointer_true( "__interrupt__": [ Interrupt( value="Provide value", - resumable=True, - ns=[AnyStr("call_subgraph"), AnyStr("subnode_2:")], + id=AnyStr(), ) ], } @@ -7135,8 +7024,7 @@ def test_interrupt_subgraph_reenter_checkpointer_true( "__interrupt__": [ Interrupt( value="Provide value", - resumable=True, - ns=[AnyStr("call_subgraph"), AnyStr("subnode_2:")], + id=AnyStr(), ) ], } @@ -7166,8 +7054,7 @@ def test_interrupt_subgraph_reenter_checkpointer_true( "__interrupt__": [ Interrupt( value="Provide value", - resumable=True, - ns=[AnyStr("call_subgraph"), AnyStr("subnode_2:")], + id=AnyStr(), ) ], } @@ -7313,7 +7200,7 @@ def test_parallel_interrupts(sync_checkpointer: BaseCheckpointSaver) -> None: # assume that it breaks here, because it is an interrupt # get human input and resume - if any(i.resumable for i in current_interrupts): + if len(current_interrupts) > 0: current_input = Command(resume=f"Resume #{invokes}") # not more human input required, must be completed @@ -7330,11 +7217,7 @@ def test_parallel_interrupts(sync_checkpointer: BaseCheckpointSaver) -> None: "__interrupt__": ( Interrupt( value="a", - resumable=True, - ns=[ - AnyStr("child_graph:"), - AnyStr("get_human_input:"), - ], + id=AnyStr(), ), ) }, @@ -7342,11 +7225,7 @@ def test_parallel_interrupts(sync_checkpointer: BaseCheckpointSaver) -> None: "__interrupt__": ( Interrupt( value="b", - resumable=True, - ns=[ - AnyStr("child_graph:"), - AnyStr("get_human_input:"), - ], + id=AnyStr(), ), ) }, @@ -7357,11 +7236,7 @@ def test_parallel_interrupts(sync_checkpointer: BaseCheckpointSaver) -> None: "__interrupt__": ( Interrupt( value="a", - resumable=True, - ns=[ - AnyStr("child_graph:"), - AnyStr("get_human_input:"), - ], + id=AnyStr(), ), ) }, @@ -7372,11 +7247,7 @@ def test_parallel_interrupts(sync_checkpointer: BaseCheckpointSaver) -> None: "__interrupt__": ( Interrupt( value="b", - resumable=True, - ns=[ - AnyStr("child_graph:"), - AnyStr("get_human_input:"), - ], + id=AnyStr(), ), ) }, @@ -7490,7 +7361,7 @@ def test_parallel_interrupts_double(sync_checkpointer: BaseCheckpointSaver) -> N # assume that it breaks here, because it is an interrupt # get human input and resume - if any(i.resumable for i in current_interrupts): + if len(current_interrupts) > 0: current_input = Command(resume=f"Resume #{invokes}") # not more human input required, must be completed @@ -7669,7 +7540,7 @@ def test_pregel_node_copy() -> None: def test_update_as_input( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class State(TypedDict): foo: str @@ -7692,13 +7563,13 @@ def test_update_as_input( assert graph.invoke( {"foo": "input"}, {"configurable": {"thread_id": "1"}}, - checkpoint_during=checkpoint_during, + durability=durability, ) == {"foo": "tool"} assert graph.invoke( {"foo": "input"}, {"configurable": {"thread_id": "1"}}, - checkpoint_during=checkpoint_during, + durability=durability, ) == {"foo": "tool"} def map_snapshot(i: StateSnapshot) -> dict: @@ -7737,14 +7608,14 @@ def test_update_as_input( for s in graph.get_state_history({"configurable": {"thread_id": "2"}}) ] - if checkpoint_during: + if durability != "exit": assert new_history == history else: assert [new_history[0], new_history[4]] == history def test_batch_update_as_input( - sync_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + sync_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class State(TypedDict): foo: str @@ -7779,7 +7650,7 @@ def test_batch_update_as_input( assert graph.invoke( {"foo": "input"}, {"configurable": {"thread_id": "1"}}, - checkpoint_during=checkpoint_during, + durability=durability, ) == { "foo": "map", "tasks": [0, 1, 2], @@ -7833,7 +7704,7 @@ def test_batch_update_as_input( for s in graph.get_state_history({"configurable": {"thread_id": "2"}}) ] - if checkpoint_during: + if durability != "exit": assert new_history == history else: assert new_history[:1] == history diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 76d650ce4..9b7fa4628 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -28,6 +28,7 @@ from pytest_mock import MockerFixture from syrupy import SnapshotAssertion from typing_extensions import TypedDict +from langgraph._internal._constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL from langgraph.cache.base import BaseCache from langgraph.channels.binop import BinaryOperatorAggregate from langgraph.channels.last_value import LastValue @@ -41,23 +42,28 @@ from langgraph.checkpoint.base import ( ) from langgraph.checkpoint.memory import InMemorySaver from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer -from langgraph.constants import CONFIG_KEY_NODE_FINISHED, ERROR, PULL, START -from langgraph.errors import InvalidUpdateError, NodeInterrupt, ParentCommand +from langgraph.errors import ( + GraphRecursionError, + InvalidUpdateError, + ParentCommand, +) from langgraph.func import entrypoint, task -from langgraph.graph import END, StateGraph +from langgraph.graph import END, START, StateGraph from langgraph.graph.message import MessagesState, add_messages from langgraph.prebuilt.tool_node import ToolNode -from langgraph.pregel import GraphRecursionError, NodeBuilder, Pregel, StateSnapshot -from langgraph.pregel.loop import AsyncPregelLoop -from langgraph.pregel.retry import RetryPolicy -from langgraph.pregel.runner import PregelRunner +from langgraph.pregel import NodeBuilder, Pregel +from langgraph.pregel._loop import AsyncPregelLoop +from langgraph.pregel._runner import PregelRunner from langgraph.store.base import BaseStore from langgraph.types import ( CachePolicy, Command, + Durability, Interrupt, PregelTask, + RetryPolicy, Send, + StateSnapshot, StateUpdate, StreamWriter, interrupt, @@ -172,11 +178,11 @@ async def test_checkpoint_errors() -> None: graph = builder.compile(checkpointer=FaultyPutWritesCheckpointer()) with pytest.raises(ValueError, match="Faulty put_writes"): await graph.ainvoke( - "", {"configurable": {"thread_id": "thread-1"}}, checkpoint_during=True + "", {"configurable": {"thread_id": "thread-1"}}, durability="async" ) with pytest.raises(ValueError, match="Faulty put_writes"): async for _ in graph.astream( - "", {"configurable": {"thread_id": "thread-2"}}, checkpoint_during=True + "", {"configurable": {"thread_id": "thread-2"}}, durability="async" ): pass with pytest.raises(ValueError, match="Faulty put_writes"): @@ -184,7 +190,7 @@ async def test_checkpoint_errors() -> None: "", {"configurable": {"thread_id": "thread-3"}}, version="v2", - checkpoint_during=True, + durability="async", ): pass @@ -311,7 +317,7 @@ async def test_checkpoint_put_after_cancellation() -> None: # start the task t = asyncio.create_task( - graph.ainvoke({"hello": "world"}, thread1, checkpoint_during=False) + graph.ainvoke({"hello": "world"}, thread1, durability="exit") ) # cancel after 0.2 seconds await asyncio.sleep(0.2) @@ -378,7 +384,7 @@ async def test_checkpoint_put_after_cancellation_stream_anext() -> None: thread1 = {"configurable": {"thread_id": "1"}} # start the task - s = graph.astream({"hello": "world"}, thread1, checkpoint_during=False) + s = graph.astream({"hello": "world"}, thread1, durability="exit") t = asyncio.create_task(s.__anext__()) # cancel after 0.2 seconds await asyncio.sleep(0.2) @@ -450,7 +456,7 @@ async def test_checkpoint_put_after_cancellation_stream_events_anext() -> None: thread1, version="v2", include_names=["LangGraph"], - checkpoint_during=False, + durability="exit", ) # skip first event (happens right away) await s.__anext__() @@ -586,9 +592,7 @@ async def test_dynamic_interrupt(async_checkpointer: BaseCheckpointSaver) -> Non ) == { "my_key": "value", "market": "DE", - "__interrupt__": [ - Interrupt(value="Just because...", resumable=True, ns=[AnyStr("tool_two:")]) - ], + "__interrupt__": [Interrupt(value="Just because...", id=AnyStr())], } assert tool_two_node_count == 1, "interrupts aren't retried" assert len(tracer.runs) == 1 @@ -619,8 +623,7 @@ async def test_dynamic_interrupt(async_checkpointer: BaseCheckpointSaver) -> Non "__interrupt__": ( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ) }, @@ -638,15 +641,14 @@ async def test_dynamic_interrupt(async_checkpointer: BaseCheckpointSaver) -> Non assert [ c async for c in tool_two.astream( - {"my_key": "value ⛰️", "market": "DE"}, thread1, checkpoint_during=False + {"my_key": "value ⛰️", "market": "DE"}, thread1, durability="exit" ) ] == [ { "__interrupt__": ( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ) }, @@ -670,8 +672,7 @@ async def test_dynamic_interrupt(async_checkpointer: BaseCheckpointSaver) -> Non interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ), ), @@ -687,8 +688,7 @@ async def test_dynamic_interrupt(async_checkpointer: BaseCheckpointSaver) -> Non interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ), ) @@ -756,8 +756,7 @@ async def test_dynamic_interrupt_subgraph( "__interrupt__": [ Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:"), AnyStr("do:")], + id=AnyStr(), ) ], } @@ -790,8 +789,7 @@ async def test_dynamic_interrupt_subgraph( "__interrupt__": ( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:"), AnyStr("do:")], + id=AnyStr(), ), ) }, @@ -810,15 +808,14 @@ async def test_dynamic_interrupt_subgraph( assert [ c async for c in tool_two.astream( - {"my_key": "value ⛰️", "market": "DE"}, thread1, checkpoint_during=False + {"my_key": "value ⛰️", "market": "DE"}, thread1, durability="exit" ) ] == [ { "__interrupt__": ( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:"), AnyStr("do:")], + id=AnyStr(), ), ) }, @@ -842,8 +839,7 @@ async def test_dynamic_interrupt_subgraph( interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:"), AnyStr("do:")], + id=AnyStr(), ), ), state={ @@ -865,8 +861,7 @@ async def test_dynamic_interrupt_subgraph( interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:"), AnyStr("do:")], + id=AnyStr(), ), ), ) @@ -932,9 +927,7 @@ async def test_partial_pending_checkpoint( ) == { "my_key": "value one", "market": "DE", - "__interrupt__": [ - Interrupt(value="Just because...", resumable=True, ns=[AnyStr("tool_two:")]) - ], + "__interrupt__": [Interrupt(value="Just because...", id=AnyStr())], } assert tool_two_node_count == 1, "interrupts aren't retried" assert len(tracer.runs) == 1 @@ -965,8 +958,7 @@ async def test_partial_pending_checkpoint( "__interrupt__": ( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ) }, @@ -989,15 +981,14 @@ async def test_partial_pending_checkpoint( thread1 = {"configurable": {"thread_id": "1"}} # stop when about to enter node assert await tool_two.ainvoke( - {"my_key": "value ⛰️", "market": "DE"}, thread1, checkpoint_during=False + {"my_key": "value ⛰️", "market": "DE"}, thread1, durability="exit" ) == { "my_key": "value ⛰️ one", "market": "DE", "__interrupt__": [ Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ) ], } @@ -1031,8 +1022,7 @@ async def test_partial_pending_checkpoint( interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ), ), @@ -1048,8 +1038,7 @@ async def test_partial_pending_checkpoint( interrupts=( Interrupt( value="Just because...", - resumable=True, - ns=[AnyStr("tool_two:")], + id=AnyStr(), ), ), ) @@ -1113,8 +1102,7 @@ async def test_node_not_cancelled_on_other_node_interrupted( "__interrupt__": [ Interrupt( value="I am bad", - resumable=True, - ns=[AnyStr("bad:")], + id=AnyStr(), ) ], } @@ -1127,8 +1115,7 @@ async def test_node_not_cancelled_on_other_node_interrupted( "__interrupt__": [ Interrupt( value="I am bad", - resumable=True, - ns=[AnyStr("bad:")], + id=AnyStr(), ) ], } @@ -1777,7 +1764,7 @@ async def test_invoke_checkpoint( async def test_pending_writes_resume( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class State(TypedDict): value: Annotated[int, operator.add] @@ -1814,7 +1801,7 @@ async def test_pending_writes_resume( thread1: RunnableConfig = {"configurable": {"thread_id": "1"}} with pytest.raises(ConnectionError, match="I'm not good"): - await graph.ainvoke({"value": 1}, thread1, checkpoint_during=checkpoint_during) + await graph.ainvoke({"value": 1}, thread1, durability=durability) # both nodes should have been called once assert one.calls == 1 @@ -1863,7 +1850,7 @@ async def test_pending_writes_resume( # resume execution with pytest.raises(ConnectionError, match="I'm not good"): - await graph.ainvoke(None, thread1, checkpoint_during=checkpoint_during) + await graph.ainvoke(None, thread1, durability=durability) # node "one" succeeded previously, so shouldn't be called again assert one.calls == 1 @@ -1877,14 +1864,12 @@ async def test_pending_writes_resume( # resume execution, without exception two.rtn = {"value": 3} # both the pending write and the new write were applied, 1 + 2 + 3 = 6 - assert await graph.ainvoke(None, thread1, checkpoint_during=checkpoint_during) == { - "value": 6 - } + assert await graph.ainvoke(None, thread1, durability=durability) == {"value": 6} # check all final checkpoints checkpoints = [c async for c in async_checkpointer.alist(thread1)] # we should have 3 - assert len(checkpoints) == (3 if checkpoint_during else 2) + assert len(checkpoints) == (3 if durability != "exit" else 2) # the last one not too interesting for this test assert checkpoints[0] == CheckpointTuple( config={ @@ -1983,14 +1968,14 @@ async def test_pending_writes_resume( "checkpoint_id": checkpoints[2].config["configurable"]["checkpoint_id"], } } - if checkpoint_during + if durability != "exit" else None, pending_writes=UnsortedSequence( (AnyStr(), "value", 2), (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), (AnyStr(), "value", 3), ) - if checkpoint_during + if durability != "exit" else UnsortedSequence( (AnyStr(), "value", 2), (AnyStr(), "__error__", 'ConnectionError("I\'m not good")'), @@ -1998,7 +1983,7 @@ async def test_pending_writes_resume( # produced in a run where only the next checkpoint (the last) is saved ), ) - if not checkpoint_during: + if durability == "exit": return assert checkpoints[2] == CheckpointTuple( config={ @@ -2072,7 +2057,7 @@ async def test_run_from_checkpoint_id_retains_previous_writes( thread_id = uuid.uuid4() thread1 = {"configurable": {"thread_id": str(thread_id)}} - result = await graph.ainvoke({"myval": 1}, thread1, checkpoint_during=True) + result = await graph.ainvoke({"myval": 1}, thread1, durability="async") assert result["myval"] == 4 history = [c async for c in graph.aget_state_history(thread1)] @@ -2254,7 +2239,7 @@ async def test_send_sequences(async_checkpointer: BaseCheckpointSaver) -> None: @NEEDS_CONTEXTVARS async def test_imp_task( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: mapper_calls = 0 @@ -2274,21 +2259,14 @@ async def test_imp_task( tracer = FakeTracer() thread1 = {"configurable": {"thread_id": "1"}, "callbacks": [tracer]} - assert [ - c - async for c in graph.astream( - [0, 1], thread1, checkpoint_during=checkpoint_during - ) - ] == [ + assert [c async for c in graph.astream([0, 1], thread1, durability=durability)] == [ {"mapper": "00"}, {"mapper": "11"}, { "__interrupt__": ( Interrupt( value="question", - resumable=True, - ns=[AnyStr("graph:")], - when="during", + id=AnyStr(), ), ) }, @@ -2304,7 +2282,7 @@ async def test_imp_task( assert any(r.inputs == {"input": 1} for r in mapper_runs) assert await graph.ainvoke( - Command(resume="answer"), thread1, checkpoint_during=checkpoint_during + Command(resume="answer"), thread1, durability=durability ) == [ "00answer", "11answer", @@ -2314,7 +2292,7 @@ async def test_imp_task( @NEEDS_CONTEXTVARS async def test_imp_nested( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: async def mynode(input: list[str]) -> list[str]: return [it + "a" for it in input] @@ -2353,12 +2331,7 @@ async def test_imp_nested( } thread1 = {"configurable": {"thread_id": "1"}} - assert [ - c - async for c in graph.astream( - [0, 1], thread1, checkpoint_during=checkpoint_during - ) - ] == [ + assert [c async for c in graph.astream([0, 1], thread1, durability=durability)] == [ {"submapper": "0"}, {"mapper": "00"}, {"submapper": "1"}, @@ -2367,16 +2340,14 @@ async def test_imp_nested( "__interrupt__": ( Interrupt( value="question", - resumable=True, - ns=[AnyStr("graph:")], - when="during", + id=AnyStr(), ), ) }, ] assert await graph.ainvoke( - Command(resume="answer"), thread1, checkpoint_during=checkpoint_during + Command(resume="answer"), thread1, durability=durability ) == [ "00answera", "11answera", @@ -2385,7 +2356,7 @@ async def test_imp_nested( @NEEDS_CONTEXTVARS async def test_imp_task_cancel( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: mapper_calls = 0 mapper_cancels = 0 @@ -2411,20 +2382,13 @@ async def test_imp_task_cancel( return [m + answer for m in mapped] thread1 = {"configurable": {"thread_id": "1"}} - assert [ - c - async for c in graph.astream( - [0, 1], thread1, checkpoint_during=checkpoint_during - ) - ] == [ + assert [c async for c in graph.astream([0, 1], thread1, durability=durability)] == [ {"mapper": "00"}, { "__interrupt__": ( Interrupt( value="question", - resumable=True, - ns=[AnyStr("graph:")], - when="during", + id=AnyStr(), ), ) }, @@ -2433,7 +2397,7 @@ async def test_imp_task_cancel( assert mapper_cancels == 1 assert await graph.ainvoke( - Command(resume="answer"), thread1, checkpoint_during=checkpoint_during + Command(resume="answer"), thread1, durability=durability ) == [ "00answer", ] @@ -2443,7 +2407,7 @@ async def test_imp_task_cancel( @NEEDS_CONTEXTVARS async def test_imp_sync_from_async( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: @task() def foo(state: dict) -> dict: @@ -2466,10 +2430,7 @@ async def test_imp_sync_from_async( thread1 = {"configurable": {"thread_id": "1"}} assert [ - c - async for c in graph.astream( - {"a": "0"}, thread1, checkpoint_during=checkpoint_during - ) + c async for c in graph.astream({"a": "0"}, thread1, durability=durability) ] == [ {"foo": {"a": "0foo", "b": "bar"}}, {"bar": {"a": "0foobar", "c": "bark"}}, @@ -2480,7 +2441,7 @@ async def test_imp_sync_from_async( @NEEDS_CONTEXTVARS async def test_imp_stream_order( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: @task() async def foo(state: dict) -> dict: @@ -2504,10 +2465,7 @@ async def test_imp_stream_order( thread1 = {"configurable": {"thread_id": "1"}} assert [ - c - async for c in graph.astream( - {"a": "0"}, thread1, checkpoint_during=checkpoint_during - ) + c async for c in graph.astream({"a": "0"}, thread1, durability=durability) ] == [ {"foo": {"a": "0foo", "b": "bar"}}, {"bar": {"a": "0foobar", "c": "bark"}}, @@ -2516,8 +2474,12 @@ async def test_imp_stream_order( ] +@pytest.mark.skipif( + sys.version_info < (3, 11), + reason="Requires Python 3.11 or higher for context management", +) async def test_send_dedupe_on_resume( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class InterruptOnce: ticks: int = 0 @@ -2525,7 +2487,7 @@ async def test_send_dedupe_on_resume( def __call__(self, state): self.ticks += 1 if self.ticks == 1: - raise NodeInterrupt("Bahh") + interrupt("Bahh") return ["|".join(("flaky", str(state)))] class Node: @@ -2568,19 +2530,18 @@ async def test_send_dedupe_on_resume( graph = builder.compile(checkpointer=async_checkpointer) thread1 = {"configurable": {"thread_id": "1"}} - assert await graph.ainvoke(["0"], thread1, checkpoint_during=checkpoint_during) == { + assert await graph.ainvoke(["0"], thread1, durability=durability) == { "__interrupt__": [ Interrupt( value="Bahh", - resumable=False, - ns=None, + id=AnyStr(), ), ], } assert builder.nodes["2"].runnable.func.ticks == 3 assert builder.nodes["flaky"].runnable.func.ticks == 1 # resume execution - assert await graph.ainvoke(None, thread1, checkpoint_during=checkpoint_during) == [ + assert await graph.ainvoke(None, thread1, durability=durability) == [ "0", "1", "3.1", @@ -2597,7 +2558,7 @@ async def test_send_dedupe_on_resume( assert builder.nodes["flaky"].runnable.func.ticks == 2 # check history history = [c async for c in graph.aget_state_history(thread1)] - assert len(history) == (6 if checkpoint_during else 2) + assert len(history) == (6 if durability != "exit" else 2) expected_history = [ StateSnapshot( values=[ @@ -2724,9 +2685,9 @@ async def test_send_dedupe_on_resume( name="flaky", path=("__pregel_push", 1, False), error=None, - interrupts=(Interrupt(value="Bahh", resumable=False, ns=None),), + interrupts=(Interrupt(value="Bahh", id=AnyStr()),), state=None, - result=["flaky|4"] if checkpoint_during else None, + result=["flaky|4"] if durability != "exit" else None, ), PregelTask( id=AnyStr(), @@ -2738,7 +2699,7 @@ async def test_send_dedupe_on_resume( result=["3"], ), ), - interrupts=(Interrupt(value="Bahh", resumable=False, ns=None),), + interrupts=(Interrupt(value="Bahh", id=AnyStr()),), ), StateSnapshot( values=["0", "1"], @@ -2861,7 +2822,7 @@ async def test_send_dedupe_on_resume( interrupts=(), ), ] - if checkpoint_during: + if durability != "exit": assert history == expected_history else: assert history[0] == expected_history[0]._replace( @@ -2972,7 +2933,7 @@ async def test_send_react_interrupt(async_checkpointer: BaseCheckpointSaver) -> graph = builder.compile(checkpointer=async_checkpointer, interrupt_before=["foo"]) thread1 = {"configurable": {"thread_id": "2"}} assert await graph.ainvoke( - {"messages": [HumanMessage("hello")]}, thread1, checkpoint_during=False + {"messages": [HumanMessage("hello")]}, thread1, durability="exit" ) == { "messages": [ _AnyIdHumanMessage(content="hello"), @@ -3096,7 +3057,7 @@ async def test_send_react_interrupt(async_checkpointer: BaseCheckpointSaver) -> graph = builder.compile(checkpointer=async_checkpointer, interrupt_before=["foo"]) thread1 = {"configurable": {"thread_id": "3"}} assert await graph.ainvoke( - {"messages": [HumanMessage("hello")]}, thread1, checkpoint_during=False + {"messages": [HumanMessage("hello")]}, thread1, durability="exit" ) == { "messages": [ _AnyIdHumanMessage(content="hello"), @@ -3361,7 +3322,7 @@ async def test_send_react_interrupt_control( graph = builder.compile(checkpointer=async_checkpointer, interrupt_before=["foo"]) thread1 = {"configurable": {"thread_id": "2"}} assert await graph.ainvoke( - {"messages": [HumanMessage("hello")]}, thread1, checkpoint_during=False + {"messages": [HumanMessage("hello")]}, thread1, durability="exit" ) == { "messages": [ _AnyIdHumanMessage(content="hello"), @@ -3648,7 +3609,7 @@ async def test_invoke_checkpoint_three( thread_1 = {"configurable": {"thread_id": "1"}} # total starts out as 0, so output is 0+2=2 - assert await app.ainvoke(2, thread_1, checkpoint_during=True) == 2 + assert await app.ainvoke(2, thread_1, durability="async") == 2 state = await app.aget_state(thread_1) assert state is not None assert state.values.get("total") == 2 @@ -3657,7 +3618,7 @@ async def test_invoke_checkpoint_three( == (await async_checkpointer.aget(thread_1))["id"] ) # total is now 2, so output is 2+3=5 - assert await app.ainvoke(3, thread_1, checkpoint_during=True) == 5 + assert await app.ainvoke(3, thread_1, durability="async") == 5 state = await app.aget_state(thread_1) assert state is not None assert state.values.get("total") == 7 @@ -3667,7 +3628,7 @@ async def test_invoke_checkpoint_three( ) # 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_during=True) + await app.ainvoke(4, thread_1, durability="async") # checkpoint is not updated state = await app.aget_state(thread_1) assert state is not None @@ -3675,7 +3636,7 @@ async def test_invoke_checkpoint_three( 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, checkpoint_during=True) == 9 + assert await app.ainvoke(2, thread_1, durability="async") == 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" @@ -4977,7 +4938,7 @@ async def test_nested_graph(snapshot: SnapshotAssertion) -> None: async def test_subgraph_checkpoint_true( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class InnerState(TypedDict): my_key: Annotated[str, operator.add] @@ -5015,7 +4976,7 @@ async def test_subgraph_checkpoint_true( {"my_key": ""}, config, subgraphs=True, - checkpoint_during=checkpoint_during, + durability=durability, ) ] == [ (("inner",), {"inner_1": {"my_key": " got here", "my_other_key": ""}}), @@ -5042,7 +5003,9 @@ async def test_subgraph_checkpoint_true( ] -async def test_subgraph_checkpoint_during_false_inherited() -> None: +async def test_subgraph_durability_inherited( + durability: Durability, +) -> None: async_checkpointer = InMemorySaver() class InnerState(TypedDict): @@ -5073,23 +5036,20 @@ async def test_subgraph_checkpoint_during_false_inherited() -> None: "inner", lambda s: "inner" if s["my_key"].count("there") < 2 else END ) app = graph.compile(checkpointer=async_checkpointer) - for checkpoint_during in [True, False]: - thread_id = str(uuid.uuid4()) - config = {"configurable": {"thread_id": thread_id}} - await app.ainvoke( - {"my_key": ""}, config, subgraphs=True, checkpoint_during=checkpoint_during - ) - if checkpoint_during: - checkpoints = list(async_checkpointer.list(config)) - assert len(checkpoints) == 12 - else: - checkpoints = list(async_checkpointer.list(config)) - assert len(checkpoints) == 1 + thread_id = str(uuid.uuid4()) + config = {"configurable": {"thread_id": thread_id}} + await app.ainvoke({"my_key": ""}, config, subgraphs=True, durability=durability) + if durability != "exit": + checkpoints = list(async_checkpointer.list(config)) + assert len(checkpoints) == 12 + else: + checkpoints = list(async_checkpointer.list(config)) + assert len(checkpoints) == 1 @NEEDS_CONTEXTVARS async def test_subgraph_checkpoint_true_interrupt( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: # Define subgraph class SubgraphState(TypedDict): @@ -5130,15 +5090,12 @@ async def test_subgraph_checkpoint_true_interrupt( graph = builder.compile(checkpointer=async_checkpointer) config = {"configurable": {"thread_id": "1"}} - assert await graph.ainvoke( - {"foo": "foo"}, config, checkpoint_during=checkpoint_during - ) == { + assert await graph.ainvoke({"foo": "foo"}, config, durability=durability) == { "foo": "hi! foo", "__interrupt__": [ Interrupt( value="Provide baz value", - resumable=True, - ns=[AnyStr("node_2"), AnyStr("subgraph_node_1:")], + id=AnyStr(), ) ], } @@ -5146,7 +5103,7 @@ async def test_subgraph_checkpoint_true_interrupt( "bar": "hi! foo" } assert await graph.ainvoke( - Command(resume="baz"), config, checkpoint_during=checkpoint_during + Command(resume="baz"), config, durability=durability ) == {"foo": "hi! foobaz"} @@ -5260,7 +5217,7 @@ async def test_stream_buffering_single_node( async def test_nested_graph_interrupts_parallel( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class InnerState(TypedDict): my_key: Annotated[str, operator.add] @@ -5309,13 +5266,11 @@ async def test_nested_graph_interrupts_parallel( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} - assert await app.ainvoke( - {"my_key": ""}, config, checkpoint_during=checkpoint_during - ) == { + assert await app.ainvoke({"my_key": ""}, config, durability=durability) == { "my_key": " and parallel", } - assert await app.ainvoke(None, config, checkpoint_during=checkpoint_during) == { + assert await app.ainvoke(None, config, durability=durability) == { "my_key": "got here and there and parallel and back again", } @@ -5330,7 +5285,7 @@ async def test_nested_graph_interrupts_parallel( {"my_key": ""}, config, subgraphs=True, - checkpoint_during=checkpoint_during, + durability=durability, ) ] == [ # we got to parallel node first @@ -5341,9 +5296,7 @@ async def test_nested_graph_interrupts_parallel( ), ((), {"__interrupt__": ()}), ] - assert [ - c async for c in app.astream(None, config, checkpoint_during=checkpoint_during) - ] == [ + assert [c async for c in app.astream(None, config, durability=durability)] == [ {"outer_1": {"my_key": " and parallel"}, "__metadata__": {"cached": True}}, {"inner": {"my_key": "got here and there"}}, {"outer_2": {"my_key": " and back again"}}, @@ -5357,7 +5310,7 @@ async def test_nested_graph_interrupts_parallel( {"my_key": ""}, config, stream_mode="values", - checkpoint_during=checkpoint_during, + durability=durability, ) ] == [ {"my_key": ""}, @@ -5366,7 +5319,7 @@ async def test_nested_graph_interrupts_parallel( assert [ c async for c in app.astream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during + None, config, stream_mode="values", durability=durability ) ] == [ {"my_key": ""}, @@ -5383,7 +5336,7 @@ async def test_nested_graph_interrupts_parallel( {"my_key": ""}, config, stream_mode="values", - checkpoint_during=checkpoint_during, + durability=durability, ) ] == [ {"my_key": ""}, @@ -5392,7 +5345,7 @@ async def test_nested_graph_interrupts_parallel( assert [ c async for c in app.astream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during + None, config, stream_mode="values", durability=durability ) ] == [ {"my_key": ""}, @@ -5401,7 +5354,7 @@ async def test_nested_graph_interrupts_parallel( assert [ c async for c in app.astream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during + None, config, stream_mode="values", durability=durability ) ] == [ {"my_key": ""}, @@ -5418,7 +5371,7 @@ async def test_nested_graph_interrupts_parallel( {"my_key": ""}, config, stream_mode="values", - checkpoint_during=checkpoint_during, + durability=durability, ) ] == [ {"my_key": ""}, @@ -5427,7 +5380,7 @@ async def test_nested_graph_interrupts_parallel( assert [ c async for c in app.astream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during + None, config, stream_mode="values", durability=durability ) ] == [ {"my_key": ""}, @@ -5436,7 +5389,7 @@ async def test_nested_graph_interrupts_parallel( assert [ c async for c in app.astream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during + None, config, stream_mode="values", durability=durability ) ] == [ {"my_key": "got here and there and parallel"}, @@ -5445,7 +5398,7 @@ async def test_nested_graph_interrupts_parallel( async def test_doubly_nested_graph_interrupts( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class State(TypedDict): my_key: str @@ -5498,13 +5451,11 @@ async def test_doubly_nested_graph_interrupts( # test invoke w/ nested interrupt config = {"configurable": {"thread_id": "1"}} - assert await app.ainvoke( - {"my_key": "my value"}, config, checkpoint_during=checkpoint_during - ) == { + assert await app.ainvoke({"my_key": "my value"}, config, durability=durability) == { "my_key": "hi my value", } - assert await app.ainvoke(None, config, checkpoint_during=checkpoint_during) == { + assert await app.ainvoke(None, config, durability=durability) == { "my_key": "hi my value here and there and back again", } @@ -5516,16 +5467,14 @@ async def test_doubly_nested_graph_interrupts( assert [ c async for c in app.astream( - {"my_key": "my value"}, config, checkpoint_during=checkpoint_during + {"my_key": "my value"}, config, durability=durability ) ] == [ {"parent_1": {"my_key": "hi my value"}}, {"__interrupt__": ()}, ] assert nodes == ["parent_1", "grandchild_1"] - assert [ - c async for c in app.astream(None, config, checkpoint_during=checkpoint_during) - ] == [ + assert [c async for c in app.astream(None, config, durability=durability)] == [ {"child": {"my_key": "hi my value here and there"}}, {"parent_2": {"my_key": "hi my value here and there and back again"}}, ] @@ -5546,7 +5495,7 @@ async def test_doubly_nested_graph_interrupts( {"my_key": "my value"}, config, stream_mode="values", - checkpoint_during=checkpoint_during, + durability=durability, ) ] == [ {"my_key": "my value"}, @@ -5555,7 +5504,7 @@ async def test_doubly_nested_graph_interrupts( assert [ c async for c in app.astream( - None, config, stream_mode="values", checkpoint_during=checkpoint_during + None, config, stream_mode="values", durability=durability ) ] == [ {"my_key": "hi my value"}, @@ -5855,7 +5804,7 @@ async def test_debug_retry(async_checkpointer: BaseCheckpointSaver): graph = builder.compile(checkpointer=async_checkpointer) config = {"configurable": {"thread_id": "1"}} - await graph.ainvoke({"messages": []}, config=config, checkpoint_during=True) + await graph.ainvoke({"messages": []}, config=config, durability="async") # re-run step: 1 async for c in async_checkpointer.alist(config): @@ -5869,7 +5818,7 @@ async def test_debug_retry(async_checkpointer: BaseCheckpointSaver): events = [ c async for c in graph.astream( - None, config=update_config, stream_mode="debug", checkpoint_during=True + None, config=update_config, stream_mode="debug", durability="async" ) ] @@ -5902,7 +5851,7 @@ async def test_debug_retry(async_checkpointer: BaseCheckpointSaver): async def test_debug_subgraphs( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ): class State(TypedDict): messages: Annotated[list[str], operator.add] @@ -5937,14 +5886,14 @@ async def test_debug_subgraphs( {"messages": []}, config=config, stream_mode="debug", - checkpoint_during=checkpoint_during, + durability=durability, ) ] checkpoint_events = list( reversed([e["payload"] for e in events if e["type"] == "checkpoint"]) ) - if not checkpoint_during: + if durability == "exit": checkpoint_events = checkpoint_events[:1] checkpoint_history = [c async for c in graph.aget_state_history(config)] @@ -5973,7 +5922,7 @@ async def test_debug_subgraphs( async def test_debug_nested_subgraphs( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: from collections import defaultdict @@ -6018,7 +5967,7 @@ async def test_debug_nested_subgraphs( config=config, stream_mode="debug", subgraphs=True, - checkpoint_during=checkpoint_during, + durability=durability, ) ] @@ -6063,9 +6012,9 @@ async def test_debug_nested_subgraphs( for checkpoint_events, checkpoint_history, ns in zip( stream_ns.values(), history_ns.values(), stream_ns.keys() ): - if not checkpoint_during: + if durability == "exit": checkpoint_events = checkpoint_events[-1:] - if ns: # Save no checkpoints for subgraphs when checkpoint_during=False + if ns: # Save no checkpoints for subgraphs when durability="exit" assert not checkpoint_history continue assert len(checkpoint_events) == len(checkpoint_history) @@ -6119,7 +6068,7 @@ async def test_parent_command( config = {"configurable": {"thread_id": "1"}} assert await graph.ainvoke( - {"messages": [("user", "get user name")]}, config, checkpoint_during=False + {"messages": [("user", "get user name")]}, config, durability="exit" ) == { "messages": [ _AnyIdHumanMessage( @@ -6214,9 +6163,7 @@ async def test_interrupt_multiple(async_checkpointer: BaseCheckpointSaver): "__interrupt__": ( Interrupt( value={"value": 1}, - resumable=True, - ns=[AnyStr("node:")], - when="during", + id=AnyStr(), ), ) } @@ -6234,9 +6181,7 @@ async def test_interrupt_multiple(async_checkpointer: BaseCheckpointSaver): "__interrupt__": ( Interrupt( value={"value": 2}, - resumable=True, - ns=[AnyStr("node:")], - when="during", + id=AnyStr(), ), ) } @@ -6284,9 +6229,7 @@ async def test_interrupt_loop(async_checkpointer: BaseCheckpointSaver) -> None: "__interrupt__": ( Interrupt( value="How old are you?", - resumable=True, - ns=[AnyStr("node:")], - when="during", + id=AnyStr(), ), ) } @@ -6303,9 +6246,7 @@ async def test_interrupt_loop(async_checkpointer: BaseCheckpointSaver) -> None: "__interrupt__": ( Interrupt( value="invalid response", - resumable=True, - ns=[AnyStr("node:")], - when="during", + id=AnyStr(), ), ) } @@ -6322,9 +6263,7 @@ async def test_interrupt_loop(async_checkpointer: BaseCheckpointSaver) -> None: "__interrupt__": ( Interrupt( value="invalid response", - resumable=True, - ns=[AnyStr("node:")], - when="during", + id=AnyStr(), ), ) } @@ -6386,8 +6325,7 @@ async def test_interrupt_task_functional( "__interrupt__": [ Interrupt( value="Provide value for bar:", - resumable=True, - ns=[AnyStr("graph:"), AnyStr("bar:")], + id=AnyStr(), ), ] } @@ -6635,7 +6573,7 @@ async def test_concurrent_execution(): async def test_checkpoint_recovery_async( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: """Test recovery from checkpoints after failures with async nodes.""" @@ -6668,7 +6606,7 @@ async def test_checkpoint_recovery_async( await graph.ainvoke( {"steps": ["start"], "attempt": 1}, config, - checkpoint_during=checkpoint_during, + durability=durability, ) # Verify checkpoint state @@ -6679,13 +6617,13 @@ async def test_checkpoint_recovery_async( # Retry with updated attempt count result = await graph.ainvoke( - {"steps": [], "attempt": 2}, config, checkpoint_during=checkpoint_during + {"steps": [], "attempt": 2}, config, durability=durability ) assert result == {"steps": ["start", "node1", "node2"], "attempt": 2} # Verify checkpoint history shows both attempts history = [c async for c in graph.aget_state_history(config)] - if checkpoint_during: + if durability != "exit": assert len(history) == 6 # Initial + failed attempt + successful attempt else: assert len(history) == 2 # error + success @@ -6908,9 +6846,7 @@ async def test_double_interrupt_subgraph( "__interrupt__": ( Interrupt( value="interrupt node 1", - resumable=True, - ns=[AnyStr("node_1:")], - when="during", + id=AnyStr(), ), ) }, @@ -6924,9 +6860,7 @@ async def test_double_interrupt_subgraph( "__interrupt__": ( Interrupt( value="interrupt node 2", - resumable=True, - ns=[AnyStr("node_2:")], - when="during", + id=AnyStr(), ), ) }, @@ -6958,9 +6892,7 @@ async def test_double_interrupt_subgraph( "__interrupt__": ( Interrupt( value="interrupt node 1", - resumable=True, - ns=[AnyStr("invoke_sub_agent:"), AnyStr("node_1:")], - when="during", + id=AnyStr(), ), ) }, @@ -6972,9 +6904,7 @@ async def test_double_interrupt_subgraph( "__interrupt__": ( Interrupt( value="interrupt node 2", - resumable=True, - ns=[AnyStr("invoke_sub_agent:"), AnyStr("node_2:")], - when="during", + id=AnyStr(), ), ) } @@ -7837,8 +7767,7 @@ async def test_interrupt_subgraph_reenter_checkpointer_true( "__interrupt__": [ Interrupt( value="Provide value", - resumable=True, - ns=[AnyStr("call_subgraph"), AnyStr("subnode_2:")], + id=AnyStr(), ) ], } @@ -7848,8 +7777,7 @@ async def test_interrupt_subgraph_reenter_checkpointer_true( "__interrupt__": [ Interrupt( value="Provide value", - resumable=True, - ns=[AnyStr("call_subgraph"), AnyStr("subnode_2")], + id=AnyStr(), ) ], } @@ -7879,8 +7807,7 @@ async def test_interrupt_subgraph_reenter_checkpointer_true( "__interrupt__": [ Interrupt( value="Provide value", - resumable=True, - ns=[AnyStr("call_subgraph"), AnyStr("subnode_2:")], + id=AnyStr(), ) ], } @@ -7917,8 +7844,7 @@ async def test_handles_multiple_interrupts_from_tasks( "__interrupt__": [ Interrupt( value="Hey do you want to add James?", - resumable=True, - ns=[AnyStr("program:"), AnyStr("add_participant:")], + id=AnyStr(), ), ] } @@ -7926,10 +7852,6 @@ async def test_handles_multiple_interrupts_from_tasks( state = await program.aget_state(config=config) assert len(state.tasks[0].interrupts) == 1 task_interrupt = state.tasks[0].interrupts[0] - assert task_interrupt.resumable is True - assert len(task_interrupt.ns) == 2 - assert task_interrupt.ns[0].startswith("program:") - assert task_interrupt.ns[1].startswith("add_participant:") assert task_interrupt.value == "Hey do you want to add James?" result = await program.ainvoke(Command(resume=True), config=config) @@ -7937,8 +7859,7 @@ async def test_handles_multiple_interrupts_from_tasks( "__interrupt__": [ Interrupt( value="Hey do you want to add Will?", - resumable=True, - ns=[AnyStr("program:"), AnyStr("add_participant:")], + id=AnyStr(), ), ] } @@ -7946,10 +7867,6 @@ async def test_handles_multiple_interrupts_from_tasks( state = await program.aget_state(config=config) assert len(state.tasks[0].interrupts) == 1 task_interrupt = state.tasks[0].interrupts[0] - assert task_interrupt.resumable is True - assert len(task_interrupt.ns) == 2 - assert task_interrupt.ns[0].startswith("program:") - assert task_interrupt.ns[1].startswith("add_participant:") assert task_interrupt.value == "Hey do you want to add Will?" result = await program.ainvoke(Command(resume=True), config=config) @@ -7993,10 +7910,6 @@ async def test_interrupts_in_tasks_surfaced_once( state = await program.aget_state(config=config) assert len(state.tasks[0].interrupts) == 1 task_interrupt = state.tasks[0].interrupts[0] - assert task_interrupt.resumable is True - assert len(task_interrupt.ns) == 2 - assert task_interrupt.ns[0].startswith("program:") - assert task_interrupt.ns[1].startswith("add_participant:") assert task_interrupt.value == "Hey do you want to add James?" interrupts = [ @@ -8009,10 +7922,6 @@ async def test_interrupts_in_tasks_surfaced_once( state = await program.aget_state(config=config) assert len(state.tasks[0].interrupts) == 1 task_interrupt = state.tasks[0].interrupts[0] - assert task_interrupt.resumable is True - assert len(task_interrupt.ns) == 2 - assert task_interrupt.ns[0].startswith("program:") - assert task_interrupt.ns[1].startswith("add_participant:") assert task_interrupt.value == "Hey do you want to add Will?" result = await program.ainvoke(Command(resume=True), config=config) @@ -8170,7 +8079,7 @@ async def test_bulk_state_updates(async_checkpointer: BaseCheckpointSaver) -> No async def test_update_as_input( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class State(TypedDict): foo: str @@ -8193,13 +8102,13 @@ async def test_update_as_input( assert await graph.ainvoke( {"foo": "input"}, {"configurable": {"thread_id": "1"}}, - checkpoint_during=checkpoint_during, + durability=durability, ) == {"foo": "tool"} assert await graph.ainvoke( {"foo": "input"}, {"configurable": {"thread_id": "1"}}, - checkpoint_during=checkpoint_during, + durability=durability, ) == {"foo": "tool"} def map_snapshot(i: StateSnapshot) -> dict: @@ -8238,14 +8147,14 @@ async def test_update_as_input( async for s in graph.aget_state_history({"configurable": {"thread_id": "2"}}) ] - if checkpoint_during: + if durability != "exit": assert new_history == history else: assert [new_history[0], new_history[4]] == history async def test_batch_update_as_input( - async_checkpointer: BaseCheckpointSaver, checkpoint_during: bool + async_checkpointer: BaseCheckpointSaver, durability: Durability ) -> None: class State(TypedDict): foo: str @@ -8280,7 +8189,7 @@ async def test_batch_update_as_input( assert await graph.ainvoke( {"foo": "input"}, {"configurable": {"thread_id": "1"}}, - checkpoint_during=checkpoint_during, + durability=durability, ) == {"foo": "map", "tasks": [0, 1, 2]} def map_snapshot(i: StateSnapshot) -> dict: @@ -8331,7 +8240,7 @@ async def test_batch_update_as_input( async for s in graph.aget_state_history({"configurable": {"thread_id": "2"}}) ] - if checkpoint_during: + if durability != "exit": assert new_history == history else: assert new_history[:1] == history @@ -8381,7 +8290,7 @@ async def test_draw_invalid(): "id": "__start__", "type": "runnable", "data": { - "id": ["langgraph", "utils", "runnable", "RunnableCallable"], + "id": ["langgraph", "_internal", "_runnable", "RunnableCallable"], "name": "__start__", }, }, @@ -8389,7 +8298,7 @@ async def test_draw_invalid(): "id": "agent", "type": "runnable", "data": { - "id": ["langgraph", "utils", "runnable", "RunnableCallable"], + "id": ["langgraph", "_internal", "_runnable", "RunnableCallable"], "name": "agent", }, }, @@ -8397,7 +8306,7 @@ async def test_draw_invalid(): "id": "tool", "type": "runnable", "data": { - "id": ["langgraph", "utils", "runnable", "RunnableCallable"], + "id": ["langgraph", "_internal", "_runnable", "RunnableCallable"], "name": "tool", }, }, @@ -8405,7 +8314,7 @@ async def test_draw_invalid(): "id": "nothing", "type": "runnable", "data": { - "id": ["langgraph", "utils", "runnable", "RunnableCallable"], + "id": ["langgraph", "_internal", "_runnable", "RunnableCallable"], "name": "nothing", }, }, diff --git a/libs/langgraph/tests/test_pydantic.py b/libs/langgraph/tests/test_pydantic.py index 8298970ba..e3340f3ec 100644 --- a/libs/langgraph/tests/test_pydantic.py +++ b/libs/langgraph/tests/test_pydantic.py @@ -21,9 +21,9 @@ from pydantic import ( model_validator, ) +from langgraph._internal._pydantic import is_supported_by_pydantic from langgraph.constants import END, START from langgraph.graph.state import StateGraph -from langgraph.utils.pydantic import is_supported_by_pydantic def test_is_supported_by_pydantic() -> None: diff --git a/libs/langgraph/tests/test_remote_graph.py b/libs/langgraph/tests/test_remote_graph.py index 16f3dc503..615a5a833 100644 --- a/libs/langgraph/tests/test_remote_graph.py +++ b/libs/langgraph/tests/test_remote_graph.py @@ -15,8 +15,8 @@ from langgraph.errors import GraphInterrupt from langgraph.graph import StateGraph, add_messages from langgraph.pregel import Pregel from langgraph.pregel.remote import RemoteGraph -from langgraph.pregel.types import StateSnapshot -from langgraph.types import Interrupt +from langgraph.types import Interrupt, StateSnapshot +from tests.any_str import AnyStr from tests.conftest import NO_DOCKER from tests.example_app.example_graph import app @@ -460,9 +460,7 @@ def test_stream(): "__interrupt__": [ { "value": {"question": "Does this look good?"}, - "resumable": True, - "ns": ["some_ns"], - "when": "during", + "id": AnyStr(), } ] }, @@ -490,9 +488,7 @@ def test_stream(): assert exc.value.args[0] == [ Interrupt( value={"question": "Does this look good?"}, - resumable=True, - ns=["some_ns"], - when="during", + id=AnyStr(), ) ] @@ -633,9 +629,7 @@ async def test_astream(): "__interrupt__": [ { "value": {"question": "Does this look good?"}, - "resumable": True, - "ns": ["some_ns"], - "when": "during", + "id": AnyStr(), } ] }, @@ -664,9 +658,7 @@ async def test_astream(): assert exc.value.args[0] == [ Interrupt( value={"question": "Does this look good?"}, - resumable=True, - ns=["some_ns"], - when="during", + id=AnyStr(), ) ] diff --git a/libs/langgraph/tests/test_retry.py b/libs/langgraph/tests/test_retry.py index 6eae7ee5c..ac37bea91 100644 --- a/libs/langgraph/tests/test_retry.py +++ b/libs/langgraph/tests/test_retry.py @@ -4,7 +4,7 @@ import pytest from typing_extensions import TypedDict from langgraph.graph import START, StateGraph -from langgraph.pregel.retry import _should_retry_on +from langgraph.pregel._retry import _should_retry_on from langgraph.types import RetryPolicy diff --git a/libs/langgraph/tests/test_runnable.py b/libs/langgraph/tests/test_runnable.py index d36755416..b27f6f625 100644 --- a/libs/langgraph/tests/test_runnable.py +++ b/libs/langgraph/tests/test_runnable.py @@ -4,9 +4,10 @@ from typing import Any, Optional import pytest +from langgraph._internal._runnable import RunnableCallable +from langgraph.runtime import Runtime from langgraph.store.base import BaseStore from langgraph.types import StreamWriter -from langgraph.utils.runnable import RunnableCallable pytestmark = pytest.mark.anyio @@ -90,7 +91,22 @@ def test_runnable_callable_injectable_arguments() -> None: assert store is None return "success" - assert RunnableCallable(func_optional_store).invoke({"x": "1"}) == "success" + assert ( + RunnableCallable(func_optional_store).invoke( + {"x": "1"}, + config={ + "configurable": { + "__pregel_runtime": Runtime( + store=None, + context=None, + stream_writer=lambda _: None, + previous=None, + ) + } + }, + ) + == "success" + ) # Test BaseStore annotation def func_required_store(inputs: Any, store: BaseStore) -> str: @@ -108,7 +124,17 @@ def test_runnable_callable_injectable_arguments() -> None: # Specify a value for store in the config assert ( RunnableCallable(func_required_store).invoke( - {}, config={"configurable": {"__pregel_store": None}} + {}, + config={ + "configurable": { + "__pregel_runtime": Runtime( + store=None, + context=None, + stream_writer=lambda _: None, + previous=None, + ) + } + }, ) == "success" ) @@ -118,7 +144,16 @@ def test_runnable_callable_injectable_arguments() -> None: RunnableCallable(func_optional_store).invoke( {"x": "1"}, store=None, - config={"configurable": {"__pregel_store": "foobar"}}, + config={ + "configurable": { + "__pregel_runtime": Runtime( + store="foobar", # type: ignore[assignment] + context=None, + stream_writer=lambda _: None, + previous=None, + ) + } + }, ) == "success" ) @@ -134,7 +169,17 @@ def test_runnable_callable_injectable_arguments() -> None: assert ( RunnableCallable(func_required_store_v2).invoke( - {}, config={"configurable": {"__pregel_store": "foobar"}} + {}, + config={ + "configurable": { + "__pregel_runtime": Runtime( + store="foobar", # type: ignore[assignment] + context=None, + stream_writer=lambda _: None, + previous=None, + ) + } + }, ) == "success" ) @@ -143,7 +188,16 @@ def test_runnable_callable_injectable_arguments() -> None: # And manual override takes precedence. {}, store="foobar", - config={"configurable": {"__pregel_store": "barbar"}}, + config={ + "configurable": { + "__pregel_runtime": Runtime( + store="foobar", # type: ignore[assignment] + context=None, + stream_writer=lambda _: None, + previous=None, + ) + } + }, ) @@ -192,7 +246,9 @@ async def test_runnable_callable_injectable_arguments_async() -> None: assert ( await RunnableCallable( func=func_required_store, afunc=afunc_required_store - ).ainvoke({}) + ).ainvoke( + {}, + ) == "success" ) @@ -200,7 +256,20 @@ async def test_runnable_callable_injectable_arguments_async() -> None: assert ( await RunnableCallable( func=func_required_store, afunc=afunc_required_store - ).ainvoke({}, store=None) + ).ainvoke( + {}, + store=None, + config={ + "configurable": { + "__pregel_runtime": Runtime( + store=None, + context=None, + stream_writer=lambda _: None, + previous=None, + ) + } + }, + ) == "success" ) @@ -208,7 +277,19 @@ async def test_runnable_callable_injectable_arguments_async() -> None: assert ( await RunnableCallable( func=func_required_store, afunc=afunc_required_store - ).ainvoke({}, config={"configurable": {"__pregel_store": None}}) + ).ainvoke( + {}, + config={ + "configurable": { + "__pregel_runtime": Runtime( + store=None, + context=None, + stream_writer=lambda _: None, + previous=None, + ) + } + }, + ) == "success" ) @@ -219,7 +300,16 @@ async def test_runnable_callable_injectable_arguments_async() -> None: ).ainvoke( {"x": "1"}, store=None, - config={"configurable": {"__pregel_store": "foobar"}}, + config={ + "configurable": { + "__pregel_runtime": Runtime( + store="foobar", + context=None, + stream_writer=lambda _: None, + previous=None, + ) + } + }, ) == "success" ) @@ -244,7 +334,19 @@ async def test_runnable_callable_injectable_arguments_async() -> None: assert ( await RunnableCallable( func=func_required_store_v2, afunc=afunc_required_store_v2 - ).ainvoke({}, config={"configurable": {"__pregel_store": "foobar"}}) + ).ainvoke( + {}, + config={ + "configurable": { + "__pregel_runtime": Runtime( + store="foobar", + context=None, + stream_writer=lambda _: None, + previous=None, + ) + } + }, + ) == "success" ) @@ -255,7 +357,16 @@ async def test_runnable_callable_injectable_arguments_async() -> None: # And manual override takes precedence. {}, store="foobar", - config={"configurable": {"__pregel_store": "barbar"}}, + config={ + "configurable": { + "__pregel_runtime": Runtime( + store="foobar", + context=None, + stream_writer=lambda _: None, + previous=None, + ) + } + }, ) == "success" ) diff --git a/libs/langgraph/tests/test_runtime.py b/libs/langgraph/tests/test_runtime.py new file mode 100644 index 000000000..cfbf361b7 --- /dev/null +++ b/libs/langgraph/tests/test_runtime.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass +from typing import Any + +from typing_extensions import TypedDict + +from langgraph.graph import END, START, StateGraph +from langgraph.runtime import Runtime, get_runtime + + +@dataclass +class Context: + api_key: str + + +class State(TypedDict): + message: str + + +def test_injected_runtime() -> None: + def injected_runtime(state: State, runtime: Runtime[Context]) -> dict[str, Any]: + return {"message": f"api key: {runtime.context.api_key}"} + + graph = StateGraph(state_schema=State, context_schema=Context) + graph.add_node("injected_runtime", injected_runtime) + graph.add_edge(START, "injected_runtime") + graph.add_edge("injected_runtime", END) + compiled = graph.compile() + result = compiled.invoke( + {"message": "hello world"}, context=Context(api_key="sk_123456") + ) + assert result == {"message": "api key: sk_123456"} + + +def test_context_runtime() -> None: + def context_runtime(state: State) -> dict[str, Any]: + runtime = get_runtime(Context) + return {"message": f"api key: {runtime.context.api_key}"} + + graph = StateGraph(state_schema=State, context_schema=Context) + graph.add_node("context_runtime", context_runtime) + graph.add_edge(START, "context_runtime") + graph.add_edge("context_runtime", END) + compiled = graph.compile() + result = compiled.invoke( + {"message": "hello world"}, context=Context(api_key="sk_123456") + ) + assert result == {"message": "api key: sk_123456"} diff --git a/libs/langgraph/tests/test_type_checking.py b/libs/langgraph/tests/test_type_checking.py index 0b7ee0679..5f807ddaa 100644 --- a/libs/langgraph/tests/test_type_checking.py +++ b/libs/langgraph/tests/test_type_checking.py @@ -2,11 +2,13 @@ from dataclasses import dataclass from operator import add from typing import Annotated, Any +import pytest from langchain_core.runnables import RunnableConfig from pydantic import BaseModel from typing_extensions import TypedDict from langgraph.graph import StateGraph +from langgraph.types import Command def test_typed_dict_state() -> None: @@ -103,3 +105,57 @@ def test_input_state_specified() -> None: new_graph.invoke({"something": 1}) new_graph.invoke({"something": 2, "info": ["hello", "world"]}) # type: ignore[arg-type] + + +@pytest.mark.skip("Purely for type checking") +def test_invoke_with_all_valid_types() -> None: + class State(TypedDict): + a: int + + def a(state: State) -> Any: ... + + graph = StateGraph(State).add_node("a", a).set_entry_point("a").compile() + graph.invoke({"a": 1}) + graph.invoke(None) + graph.invoke(Command()) + + +def test_add_node_with_explicit_input_schema() -> None: + class A(TypedDict): + a1: int + a2: str + + class B(TypedDict): + b1: int + b2: str + + class ANarrow(TypedDict): + a1: int + + class BNarrow(TypedDict): + b1: int + + class State(A, B): ... + + def a(state: A) -> Any: ... + + def b(state: B) -> Any: ... + + workflow = StateGraph(State) + # input schema matches typed schemas + workflow.add_node("a", a, input_schema=A) + workflow.add_node("b", b, input_schema=B) + + # input schema does not match typed schemas + workflow.add_node("a_wrong", a, input_schema=B) # type: ignore[arg-type] + workflow.add_node("b_wrong", b, input_schema=A) # type: ignore[arg-type] + + # input schema is more broad than the typed schemas, which is allowed + # by the principles of contravariance + workflow.add_node("a_inclusive", a, input_schema=State) + workflow.add_node("b_inclusive", b, input_schema=State) + + # input schema is more narrow than the typed schemas, which is not allowed + # because it violates the principles of contravariance + workflow.add_node("a_narrow", a, input_schema=ANarrow) # type: ignore[arg-type] + workflow.add_node("b_narrow", b, input_schema=BNarrow) # type: ignore[arg-type] diff --git a/libs/langgraph/tests/test_utils.py b/libs/langgraph/tests/test_utils.py index 455032afa..afe486af2 100644 --- a/libs/langgraph/tests/test_utils.py +++ b/libs/langgraph/tests/test_utils.py @@ -17,18 +17,19 @@ import langsmith import pytest from typing_extensions import NotRequired, Required, TypedDict -from langgraph.graph import END, StateGraph -from langgraph.graph.state import CompiledStateGraph -from langgraph.utils.config import _is_not_empty -from langgraph.utils.fields import ( +from langgraph._internal._config import _is_not_empty +from langgraph._internal._fields import ( _is_optional_type, get_enhanced_type_hints, get_field_default, ) -from langgraph.utils.runnable import ( +from langgraph._internal._runnable import ( is_async_callable, is_async_generator, ) +from langgraph.constants import END +from langgraph.graph import StateGraph +from langgraph.graph.state import CompiledStateGraph pytestmark = pytest.mark.anyio diff --git a/libs/langgraph/langgraph/utils/__init__.py b/libs/langgraph/utils/__init__.py similarity index 100% rename from libs/langgraph/langgraph/utils/__init__.py rename to libs/langgraph/utils/__init__.py diff --git a/libs/langgraph/utils/runnable.py b/libs/langgraph/utils/runnable.py new file mode 100644 index 000000000..0c1a94ed4 --- /dev/null +++ b/libs/langgraph/utils/runnable.py @@ -0,0 +1,2 @@ +# import for backwards compatibility +from langgraph._internal._runnable import RunnableCallable, RunnableSeq # noqa: F401 diff --git a/libs/langgraph/uv.lock b/libs/langgraph/uv.lock index 23d8febfd..f979cf4d7 100644 --- a/libs/langgraph/uv.lock +++ b/libs/langgraph/uv.lock @@ -169,23 +169,23 @@ css = [ [[package]] name = "blockbuster" -version = "1.5.24" +version = "1.5.25" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "forbiddenfruit", marker = "python_full_version >= '3.11' and implementation_name == 'cpython'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/c8/1e456a043179f2aef10bcaafea79f6d06c0ac45cc994767a54f680509f3b/blockbuster-1.5.24.tar.gz", hash = "sha256:97645775761a5d425666ec0bc99629b65c7eccdc2f770d2439850682567af4ec", size = 51245, upload-time = "2025-03-18T10:12:06.398Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/bc/57c49465decaeeedd58ce2d970b4cdfd93a74ba9993abff2dc498a31c283/blockbuster-1.5.25.tar.gz", hash = "sha256:b72f1d2aefdeecd2a820ddf1e1c8593bf00b96e9fdc4cd2199ebafd06f7cb8f0", size = 36058, upload-time = "2025-07-14T16:00:20.766Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c8/57a4c80e5abec29fa9406307a5277527f21210bfc6c2c61c3d8ded36c09b/blockbuster-1.5.24-py3-none-any.whl", hash = "sha256:e703497b55bc72af09d60d1cd746c2f3ba7ce0c446fa256be6ccda5e7d403520", size = 13214, upload-time = "2025-03-18T10:12:04.802Z" }, + { url = "https://files.pythonhosted.org/packages/0b/01/dccc277c014f171f61a6047bb22c684e16c7f2db6bb5c8cce1feaf41ec55/blockbuster-1.5.25-py3-none-any.whl", hash = "sha256:cb06229762273e0f5f3accdaed3d2c5a3b61b055e38843de202311ede21bb0f5", size = 13196, upload-time = "2025-07-14T16:00:19.396Z" }, ] [[package]] name = "certifi" -version = "2025.7.9" +version = "2025.7.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" }, + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, ] [[package]] @@ -520,31 +520,31 @@ wheels = [ [[package]] name = "debugpy" -version = "1.8.14" +version = "1.8.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444, upload-time = "2025-04-10T19:46:10.981Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/3a9a28ddb750a76eaec445c7f4d3147ea2c579a97dbd9e25d39001b92b21/debugpy-1.8.15.tar.gz", hash = "sha256:58d7a20b7773ab5ee6bdfb2e6cf622fdf1e40c9d5aef2857d85391526719ac00", size = 1643279, upload-time = "2025-07-15T16:43:29.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/df/156df75a41aaebd97cee9d3870fe68f8001b6c1c4ca023e221cfce69bece/debugpy-1.8.14-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:93fee753097e85623cab1c0e6a68c76308cd9f13ffdf44127e6fab4fbf024339", size = 2076510, upload-time = "2025-04-10T19:46:13.315Z" }, - { url = "https://files.pythonhosted.org/packages/69/cd/4fc391607bca0996db5f3658762106e3d2427beaef9bfd363fd370a3c054/debugpy-1.8.14-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d937d93ae4fa51cdc94d3e865f535f185d5f9748efb41d0d49e33bf3365bd79", size = 3559614, upload-time = "2025-04-10T19:46:14.647Z" }, - { url = "https://files.pythonhosted.org/packages/1a/42/4e6d2b9d63e002db79edfd0cb5656f1c403958915e0e73ab3e9220012eec/debugpy-1.8.14-cp310-cp310-win32.whl", hash = "sha256:c442f20577b38cc7a9aafecffe1094f78f07fb8423c3dddb384e6b8f49fd2987", size = 5208588, upload-time = "2025-04-10T19:46:16.233Z" }, - { url = "https://files.pythonhosted.org/packages/97/b1/cc9e4e5faadc9d00df1a64a3c2d5c5f4b9df28196c39ada06361c5141f89/debugpy-1.8.14-cp310-cp310-win_amd64.whl", hash = "sha256:f117dedda6d969c5c9483e23f573b38f4e39412845c7bc487b6f2648df30fe84", size = 5241043, upload-time = "2025-04-10T19:46:17.768Z" }, - { url = "https://files.pythonhosted.org/packages/67/e8/57fe0c86915671fd6a3d2d8746e40485fd55e8d9e682388fbb3a3d42b86f/debugpy-1.8.14-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:1b2ac8c13b2645e0b1eaf30e816404990fbdb168e193322be8f545e8c01644a9", size = 2175064, upload-time = "2025-04-10T19:46:19.486Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/2b2fd1b1c9569c6764ccdb650a6f752e4ac31be465049563c9eb127a8487/debugpy-1.8.14-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf431c343a99384ac7eab2f763980724834f933a271e90496944195318c619e2", size = 3132359, upload-time = "2025-04-10T19:46:21.192Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ee/b825c87ed06256ee2a7ed8bab8fb3bb5851293bf9465409fdffc6261c426/debugpy-1.8.14-cp311-cp311-win32.whl", hash = "sha256:c99295c76161ad8d507b413cd33422d7c542889fbb73035889420ac1fad354f2", size = 5133269, upload-time = "2025-04-10T19:46:23.047Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a6/6c70cd15afa43d37839d60f324213843174c1d1e6bb616bd89f7c1341bac/debugpy-1.8.14-cp311-cp311-win_amd64.whl", hash = "sha256:7816acea4a46d7e4e50ad8d09d963a680ecc814ae31cdef3622eb05ccacf7b01", size = 5158156, upload-time = "2025-04-10T19:46:24.521Z" }, - { url = "https://files.pythonhosted.org/packages/d9/2a/ac2df0eda4898f29c46eb6713a5148e6f8b2b389c8ec9e425a4a1d67bf07/debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84", size = 2501268, upload-time = "2025-04-10T19:46:26.044Z" }, - { url = "https://files.pythonhosted.org/packages/10/53/0a0cb5d79dd9f7039169f8bf94a144ad3efa52cc519940b3b7dde23bcb89/debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826", size = 4221077, upload-time = "2025-04-10T19:46:27.464Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d5/84e01821f362327bf4828728aa31e907a2eca7c78cd7c6ec062780d249f8/debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f", size = 5255127, upload-time = "2025-04-10T19:46:29.467Z" }, - { url = "https://files.pythonhosted.org/packages/33/16/1ed929d812c758295cac7f9cf3dab5c73439c83d9091f2d91871e648093e/debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f", size = 5297249, upload-time = "2025-04-10T19:46:31.538Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676, upload-time = "2025-04-10T19:46:32.96Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514, upload-time = "2025-04-10T19:46:34.336Z" }, - { url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756, upload-time = "2025-04-10T19:46:36.199Z" }, - { url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119, upload-time = "2025-04-10T19:46:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/6f/96ba96545f55b6a675afa08c96b42810de9b18c7ad17446bbec82762127a/debugpy-1.8.14-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:413512d35ff52c2fb0fd2d65e69f373ffd24f0ecb1fac514c04a668599c5ce7f", size = 2077696, upload-time = "2025-04-10T19:46:46.817Z" }, - { url = "https://files.pythonhosted.org/packages/fa/84/f378a2dd837d94de3c85bca14f1db79f8fcad7e20b108b40d59da56a6d22/debugpy-1.8.14-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c9156f7524a0d70b7a7e22b2e311d8ba76a15496fb00730e46dcdeedb9e1eea", size = 3554846, upload-time = "2025-04-10T19:46:48.72Z" }, - { url = "https://files.pythonhosted.org/packages/db/52/88824fe5d6893f59933f664c6e12783749ab537a2101baf5c713164d8aa2/debugpy-1.8.14-cp39-cp39-win32.whl", hash = "sha256:b44985f97cc3dd9d52c42eb59ee9d7ee0c4e7ecd62bca704891f997de4cef23d", size = 5209350, upload-time = "2025-04-10T19:46:50.284Z" }, - { url = "https://files.pythonhosted.org/packages/41/35/72e9399be24a04cb72cfe1284572c9fcd1d742c7fa23786925c18fa54ad8/debugpy-1.8.14-cp39-cp39-win_amd64.whl", hash = "sha256:b1528cfee6c1b1c698eb10b6b096c598738a8238822d218173d21c3086de8123", size = 5241852, upload-time = "2025-04-10T19:46:52.022Z" }, - { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230, upload-time = "2025-04-10T19:46:54.077Z" }, + { url = "https://files.pythonhosted.org/packages/69/51/0b4315169f0d945271db037ae6b98c0548a2d48cc036335cd1b2f5516c1b/debugpy-1.8.15-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e9a8125c85172e3ec30985012e7a81ea5e70bbb836637f8a4104f454f9b06c97", size = 2084890, upload-time = "2025-07-15T16:43:31.239Z" }, + { url = "https://files.pythonhosted.org/packages/36/cc/a5391dedb079280d7b72418022e00ba8227ae0b5bc8b2e3d1ecffc5d6b01/debugpy-1.8.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd0b6b5eccaa745c214fd240ea82f46049d99ef74b185a3517dad3ea1ec55d9", size = 3561470, upload-time = "2025-07-15T16:43:32.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/92/acf64b92010c66b33c077dee3862c733798a2c90e7d14b25c01d771e2a0d/debugpy-1.8.15-cp310-cp310-win32.whl", hash = "sha256:8181cce4d344010f6bfe94a531c351a46a96b0f7987750932b2908e7a1e14a55", size = 5229194, upload-time = "2025-07-15T16:43:33.997Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f5/c58c015c9ff78de35901bea3ab4dbf7946d7a4aa867ee73875df06ba6468/debugpy-1.8.15-cp310-cp310-win_amd64.whl", hash = "sha256:af2dcae4e4cd6e8b35f982ccab29fe65f7e8766e10720a717bc80c464584ee21", size = 5260900, upload-time = "2025-07-15T16:43:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b3/1c44a2ed311199ab11c2299c9474a6c7cd80d19278defd333aeb7c287995/debugpy-1.8.15-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:babc4fb1962dd6a37e94d611280e3d0d11a1f5e6c72ac9b3d87a08212c4b6dd3", size = 2183442, upload-time = "2025-07-15T16:43:36.733Z" }, + { url = "https://files.pythonhosted.org/packages/f6/69/e2dcb721491e1c294d348681227c9b44fb95218f379aa88e12a19d85528d/debugpy-1.8.15-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f778e68f2986a58479d0ac4f643e0b8c82fdd97c2e200d4d61e7c2d13838eb53", size = 3134215, upload-time = "2025-07-15T16:43:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/76/4ce63b95d8294dcf2fd1820860b300a420d077df4e93afcaa25a984c2ca7/debugpy-1.8.15-cp311-cp311-win32.whl", hash = "sha256:f9d1b5abd75cd965e2deabb1a06b0e93a1546f31f9f621d2705e78104377c702", size = 5154037, upload-time = "2025-07-15T16:43:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/e5a7c784465eb9c976d84408873d597dc7ce74a0fc69ed009548a1a94813/debugpy-1.8.15-cp311-cp311-win_amd64.whl", hash = "sha256:62954fb904bec463e2b5a415777f6d1926c97febb08ef1694da0e5d1463c5c3b", size = 5178133, upload-time = "2025-07-15T16:43:40.969Z" }, + { url = "https://files.pythonhosted.org/packages/ab/4a/4508d256e52897f5cdfee6a6d7580974811e911c6d01321df3264508a5ac/debugpy-1.8.15-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:3dcc7225cb317469721ab5136cda9ff9c8b6e6fb43e87c9e15d5b108b99d01ba", size = 2511197, upload-time = "2025-07-15T16:43:42.343Z" }, + { url = "https://files.pythonhosted.org/packages/99/8d/7f6ef1097e7fecf26b4ef72338d08e41644a41b7ee958a19f494ffcffc29/debugpy-1.8.15-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:047a493ca93c85ccede1dbbaf4e66816794bdc214213dde41a9a61e42d27f8fc", size = 4229517, upload-time = "2025-07-15T16:43:44.14Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e8/e8c6a9aa33a9c9c6dacbf31747384f6ed2adde4de2e9693c766bdf323aa3/debugpy-1.8.15-cp312-cp312-win32.whl", hash = "sha256:b08e9b0bc260cf324c890626961dad4ffd973f7568fbf57feb3c3a65ab6b6327", size = 5276132, upload-time = "2025-07-15T16:43:45.529Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ad/231050c6177b3476b85fcea01e565dac83607b5233d003ff067e2ee44d8f/debugpy-1.8.15-cp312-cp312-win_amd64.whl", hash = "sha256:e2a4fe357c92334272eb2845fcfcdbec3ef9f22c16cf613c388ac0887aed15fa", size = 5317645, upload-time = "2025-07-15T16:43:46.968Z" }, + { url = "https://files.pythonhosted.org/packages/28/70/2928aad2310726d5920b18ed9f54b9f06df5aa4c10cf9b45fa18ff0ab7e8/debugpy-1.8.15-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:f5e01291ad7d6649aed5773256c5bba7a1a556196300232de1474c3c372592bf", size = 2495538, upload-time = "2025-07-15T16:43:48.927Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c6/9b8ffb4ca91fac8b2877eef63c9cc0e87dd2570b1120054c272815ec4cd0/debugpy-1.8.15-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94dc0f0d00e528d915e0ce1c78e771475b2335b376c49afcc7382ee0b146bab6", size = 4221874, upload-time = "2025-07-15T16:43:50.282Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/9b8d59674b4bf489318c7c46a1aab58e606e583651438084b7e029bf3c43/debugpy-1.8.15-cp313-cp313-win32.whl", hash = "sha256:fcf0748d4f6e25f89dc5e013d1129ca6f26ad4da405e0723a4f704583896a709", size = 5275949, upload-time = "2025-07-15T16:43:52.079Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/9e58e6fdfa8710a5e6ec06c2401241b9ad48b71c0a7eb99570a1f1edb1d3/debugpy-1.8.15-cp313-cp313-win_amd64.whl", hash = "sha256:73c943776cb83e36baf95e8f7f8da765896fd94b05991e7bc162456d25500683", size = 5317720, upload-time = "2025-07-15T16:43:53.703Z" }, + { url = "https://files.pythonhosted.org/packages/90/ca/5253cc91a5380722bdf20f500cc03c3ffc78ef8e1f711788dd08a02a8a04/debugpy-1.8.15-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:085b6d0adb3eb457c2823ac497a0690b10a99eff8b01c01a041e84579f114b56", size = 2086078, upload-time = "2025-07-15T16:44:00.761Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ed/333d555e8a26792cec1d7521d7f6d4eb23f4c9e67e11ed55342c2312f188/debugpy-1.8.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd546a405381d17527814852642df0a74b7da8acc20ae5f3cfad0b7c86419511", size = 3556714, upload-time = "2025-07-15T16:44:02.188Z" }, + { url = "https://files.pythonhosted.org/packages/01/e2/699a9471a4c2bbbe5e2326e6dcd9b79d37a752c576f06ae2c27bc4f14a90/debugpy-1.8.15-cp39-cp39-win32.whl", hash = "sha256:ae0d445fe11ff4351428e6c2389e904e1cdcb4a47785da5a5ec4af6c5b95fce5", size = 5229934, upload-time = "2025-07-15T16:44:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c4/aa720b33b601b96fe482aa025e5a9eac22ff9f223756313d6f117474e366/debugpy-1.8.15-cp39-cp39-win_amd64.whl", hash = "sha256:de7db80189ca97ab4b10a87e4039cfe4dd7ddfccc8f33b5ae40fcd33792fc67a", size = 5261732, upload-time = "2025-07-15T16:44:04.97Z" }, + { url = "https://files.pythonhosted.org/packages/07/d5/98748d9860e767a1248b5e31ffa7ce8cb7006e97bf8abbf3d891d0a8ba4e/debugpy-1.8.15-py2.py3-none-any.whl", hash = "sha256:bce2e6c5ff4f2e00b98d45e7e01a49c7b489ff6df5f12d881c67d2f1ac635f3d", size = 5282697, upload-time = "2025-07-15T16:44:07.996Z" }, ] [[package]] @@ -885,7 +885,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.24.0" +version = "4.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -893,9 +893,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/6e/35174c1d3f30560848c82d3c233c01420e047d70925c897a4d6e932b4898/jsonschema-4.24.1.tar.gz", hash = "sha256:fe45a130cc7f67cd0d67640b4e7e3e2e666919462ae355eda238296eafeb4b5d", size = 356635, upload-time = "2025-07-17T14:40:01.05Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" }, + { url = "https://files.pythonhosted.org/packages/85/7f/ea48ffb58f9791f9d97ccb35e42fea1ebc81c67ce36dc4b8b2eee60e8661/jsonschema-4.24.1-py3-none-any.whl", hash = "sha256:6b916866aa0b61437785f1277aa2cbd63512e8d4b47151072ef13292049b4627", size = 89060, upload-time = "2025-07-17T14:39:59.471Z" }, ] [package.optional-dependencies] @@ -1174,7 +1174,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "0.3.68" +version = "0.3.69" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -1185,14 +1185,14 @@ dependencies = [ { name = "tenacity" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/20/f5b18a17bfbe3416177e702ab2fd230b7d168abb17be31fb48f43f0bb772/langchain_core-0.3.68.tar.gz", hash = "sha256:312e1932ac9aa2eaf111b70fdc171776fa571d1a86c1f873dcac88a094b19c6f", size = 563041, upload-time = "2025-07-03T17:02:28.704Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/26/c4770d3933237cde2918d502e3b0a8b6ce100b296840b632658f3e59b341/langchain_core-0.3.69.tar.gz", hash = "sha256:c132961117cc7f0227a4c58dd3e209674a6dd5b7e74abc61a0df93b0d736e283", size = 563824, upload-time = "2025-07-15T21:19:56.626Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/da/c89be0a272993bfcb762b2a356b9f55de507784c2755ad63caec25d183bf/langchain_core-0.3.68-py3-none-any.whl", hash = "sha256:5e5c1fbef419590537c91b8c2d86af896fbcbaf0d5ed7fdcdd77f7d8f3467ba0", size = 441405, upload-time = "2025-07-03T17:02:27.115Z" }, + { url = "https://files.pythonhosted.org/packages/51/7b/bb7b088440ff9cc55e9e6eba94162cbdcd3b1693c194e1ad4764acba29b9/langchain_core-0.3.69-py3-none-any.whl", hash = "sha256:383e9cb4919f7ef4b24bf8552ef42e4323c064924fea88b28dd5d7ddb740d3b8", size = 441556, upload-time = "2025-07-15T21:19:55.342Z" }, ] [[package]] name = "langgraph" -version = "0.5.4" +version = "0.6.0a1" source = { editable = "." } dependencies = [ { name = "langchain-core" }, @@ -1248,7 +1248,7 @@ dev = [ { name = "langgraph-checkpoint", editable = "../checkpoint" }, { name = "langgraph-checkpoint-postgres", editable = "../checkpoint-postgres" }, { name = "langgraph-checkpoint-sqlite", editable = "../checkpoint-sqlite" }, - { name = "langgraph-cli", extras = ["inmem"] }, + { name = "langgraph-cli", extras = ["inmem"], editable = "../cli" }, { name = "langgraph-prebuilt", editable = "../prebuilt" }, { name = "langgraph-sdk", editable = "../sdk-py" }, { name = "mypy" }, @@ -1271,7 +1271,7 @@ dev = [ [[package]] name = "langgraph-api" -version = "0.2.86" +version = "0.2.95" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle", marker = "python_full_version >= '3.11'" }, @@ -1294,9 +1294,9 @@ dependencies = [ { name = "uvicorn", marker = "python_full_version >= '3.11'" }, { name = "watchfiles", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/06/f8d6c1310772a8507dfa2c586bab8d0ab8b8cbe1f896106ee315af08fb1d/langgraph_api-0.2.86.tar.gz", hash = "sha256:220532a5a2232d32efef7e3b98be74ee6328d18f785e83949fa815ef2ac77f2f", size = 237417, upload-time = "2025-07-11T17:02:39.535Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/3d/5afd71e18806b71634e2178ba9e5e78a6678e0b8121158c9f458e57a8b9b/langgraph_api-0.2.95.tar.gz", hash = "sha256:7604cf276e592af00ab17642c053ced6f87122c53186256645593c7da4fbbfa3", size = 238773, upload-time = "2025-07-17T16:56:11.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/48/e6b774e8cfe254694b768629c72004689d94d002a54e949c23e05b776eae/langgraph_api-0.2.86-py3-none-any.whl", hash = "sha256:b20ac26ef9c5323732012eed602290ca9ca268473341dcb3282b02ed6622ec8c", size = 192498, upload-time = "2025-07-11T17:02:38.199Z" }, + { url = "https://files.pythonhosted.org/packages/7d/17/63636946f3d5d1c59b5b0a2d936d8009860227368e8868d33c72b61dfbbb/langgraph_api-0.2.95-py3-none-any.whl", hash = "sha256:25946eef80794bf92c27daf21db4af864779677bb9f92c1bc795901d7113e9ae", size = 194381, upload-time = "2025-07-17T16:56:10.275Z" }, ] [[package]] @@ -1394,17 +1394,13 @@ dev = [ [[package]] name = "langgraph-cli" -version = "0.3.4" -source = { registry = "https://pypi.org/simple" } +version = "0.3.5" +source = { editable = "../cli" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "langgraph-sdk", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/ee/41f54032b2ab64c06e66e7f5e7a6c22d9159f2bff6bf08a38c9f11f84753/langgraph_cli-0.3.4.tar.gz", hash = "sha256:6300df4fc6f7106fd5fcdba2cbec9e8b1158daa6760d41333d1b3b5999280ad0", size = 728156, upload-time = "2025-07-08T19:52:24.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/9c/310dae8c638477e2f0e5744726d4b283878c1e11639fa693bf24b23cf7ac/langgraph_cli-0.3.4-py3-none-any.whl", hash = "sha256:b3ac9fbc67cec5d0295c23a9e7a9014f34502639fb52b2d02c89b3bb2ba36c33", size = 36525, upload-time = "2025-07-08T19:52:23.351Z" }, -] [package.optional-dependencies] inmem = [ @@ -1413,6 +1409,28 @@ inmem = [ { name = "python-dotenv" }, ] +[package.metadata] +requires-dist = [ + { name = "click", specifier = ">=8.1.7" }, + { name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.2.67,<0.3.0" }, + { name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.6.0" }, + { name = "langgraph-sdk", marker = "python_full_version >= '3.11'", specifier = ">=0.1.0" }, + { name = "python-dotenv", marker = "extra == 'inmem'", specifier = ">=0.8.0" }, +] +provides-extras = ["inmem"] + +[package.metadata.requires-dev] +dev = [ + { name = "codespell" }, + { name = "msgspec" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-mock" }, + { name = "pytest-watch" }, + { name = "ruff" }, +] + [[package]] name = "langgraph-prebuilt" version = "0.5.2" @@ -1446,7 +1464,7 @@ dev = [ [[package]] name = "langgraph-runtime-inmem" -version = "0.3.4" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blockbuster", marker = "python_full_version >= '3.11'" }, @@ -1456,14 +1474,14 @@ dependencies = [ { name = "starlette", marker = "python_full_version >= '3.11'" }, { name = "structlog", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/17/7ff669ff44a53ab342903c2996fff75a77494af9fe56abcfbca64fe2342b/langgraph_runtime_inmem-0.3.4.tar.gz", hash = "sha256:eda7828f3ea07126e5265024b74a3fa9bf611633ad83ba3296ab9f51d89b7c0c", size = 77424, upload-time = "2025-07-01T14:45:07.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/0c/d145c6d83d36efda17b10812760711b77ec05f5bbe962c961d75b32e3c17/langgraph_runtime_inmem-0.6.0.tar.gz", hash = "sha256:b09675789a331be4a2b387c9c46de8772c4c8418e74c057b4ca24e85c25acae3", size = 77618, upload-time = "2025-07-17T16:51:01.504Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/0e/39c13ca7229a9425a0e5744a1d3817f80d38dd5ca7703494fb9cf836ba45/langgraph_runtime_inmem-0.3.4-py3-none-any.whl", hash = "sha256:dcb9ac68ac90b3fb1ddaf666d14a367ab70e69d5bb5589b77a72c318e29104ae", size = 29139, upload-time = "2025-07-01T14:45:06.472Z" }, + { url = "https://files.pythonhosted.org/packages/12/6a/9dc5769b5d2f97d1feacbbf93b180c359dff7462454b37dfef8aed4ebcf7/langgraph_runtime_inmem-0.6.0-py3-none-any.whl", hash = "sha256:312dab25bec6557f1edf95cb8bd7c8bb52f7f4bfeecaf66e7001662f095c9079", size = 29317, upload-time = "2025-07-17T16:51:00.622Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.1.74" +version = "0.2.0a1" source = { editable = "../sdk-py" } dependencies = [ { name = "httpx" }, @@ -1489,7 +1507,7 @@ dev = [ [[package]] name = "langsmith" -version = "0.4.5" +version = "0.4.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -1500,9 +1518,9 @@ dependencies = [ { name = "requests-toolbelt" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/92/7885823f3d13222f57773921f0da19b37d628c64607491233dc853a0f6ea/langsmith-0.4.5.tar.gz", hash = "sha256:49444bd8ccd4e46402f1b9ff1d686fa8e3a31b175e7085e72175ab8ec6164a34", size = 352235, upload-time = "2025-07-10T22:08:04.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/9e/11536528c6e351820ad3fca0d2807f0e0f0619ff907529c78f68ba648497/langsmith-0.4.6.tar.gz", hash = "sha256:9189dbc9c60f2086ca3a1f0110cfe3aff6b0b7c2e0e3384f9572e70502e7933c", size = 352364, upload-time = "2025-07-15T19:43:18.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/10/ad3107b666c3203b7938d10ea6b8746b9735c399cf737a51386d58e41d34/langsmith-0.4.5-py3-none-any.whl", hash = "sha256:4167717a2cccc4dff5809dbddc439628e836f6fd13d4fdb31ea013bc8d5cfaf5", size = 367795, upload-time = "2025-07-10T22:08:02.548Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9b/f2be47db823e89448ea41bfd8fc5ce6a995556bd25be4c23e5b3bb5b6c9b/langsmith-0.4.6-py3-none-any.whl", hash = "sha256:900e83fe59ee672bcf2f75c8bb47cd012bf8154d92a99c0355fc38b6485cbd3e", size = 367901, upload-time = "2025-07-15T19:43:16.508Z" }, ] [[package]] @@ -1599,7 +1617,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.16.1" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, @@ -1607,39 +1625,39 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/12/2bf23a80fcef5edb75de9a1e295d778e0f46ea89eb8b115818b663eff42b/mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a", size = 10958644, upload-time = "2025-06-16T16:51:11.649Z" }, - { url = "https://files.pythonhosted.org/packages/08/50/bfe47b3b278eacf348291742fd5e6613bbc4b3434b72ce9361896417cfe5/mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72", size = 10087033, upload-time = "2025-06-16T16:35:30.089Z" }, - { url = "https://files.pythonhosted.org/packages/21/de/40307c12fe25675a0776aaa2cdd2879cf30d99eec91b898de00228dc3ab5/mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea", size = 11875645, upload-time = "2025-06-16T16:35:48.49Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d8/85bdb59e4a98b7a31495bd8f1a4445d8ffc86cde4ab1f8c11d247c11aedc/mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574", size = 12616986, upload-time = "2025-06-16T16:48:39.526Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d0/bb25731158fa8f8ee9e068d3e94fcceb4971fedf1424248496292512afe9/mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d", size = 12878632, upload-time = "2025-06-16T16:36:08.195Z" }, - { url = "https://files.pythonhosted.org/packages/2d/11/822a9beb7a2b825c0cb06132ca0a5183f8327a5e23ef89717c9474ba0bc6/mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6", size = 9484391, upload-time = "2025-06-16T16:37:56.151Z" }, - { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" }, - { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" }, - { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" }, - { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" }, - { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" }, - { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" }, - { url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" }, - { url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" }, - { url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" }, - { url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" }, - { url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" }, - { url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" }, - { url = "https://files.pythonhosted.org/packages/49/5e/ed1e6a7344005df11dfd58b0fdd59ce939a0ba9f7ed37754bf20670b74db/mypy-1.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fc688329af6a287567f45cc1cefb9db662defeb14625213a5b7da6e692e2069", size = 10959511, upload-time = "2025-06-16T16:47:21.945Z" }, - { url = "https://files.pythonhosted.org/packages/30/88/a7cbc2541e91fe04f43d9e4577264b260fecedb9bccb64ffb1a34b7e6c22/mypy-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e198ab3f55924c03ead626ff424cad1732d0d391478dfbf7bb97b34602395da", size = 10075555, upload-time = "2025-06-16T16:50:14.084Z" }, - { url = "https://files.pythonhosted.org/packages/93/f7/c62b1e31a32fbd1546cca5e0a2e5f181be5761265ad1f2e94f2a306fa906/mypy-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09aa4f91ada245f0a45dbc47e548fd94e0dd5a8433e0114917dc3b526912a30c", size = 11874169, upload-time = "2025-06-16T16:49:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/c8/15/db580a28034657fb6cb87af2f8996435a5b19d429ea4dcd6e1c73d418e60/mypy-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13c7cd5b1cb2909aa318a90fd1b7e31f17c50b242953e7dd58345b2a814f6383", size = 12610060, upload-time = "2025-06-16T16:34:15.215Z" }, - { url = "https://files.pythonhosted.org/packages/ec/78/c17f48f6843048fa92d1489d3095e99324f2a8c420f831a04ccc454e2e51/mypy-1.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:58e07fb958bc5d752a280da0e890c538f1515b79a65757bbdc54252ba82e0b40", size = 12875199, upload-time = "2025-06-16T16:35:14.448Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d6/ed42167d0a42680381653fd251d877382351e1bd2c6dd8a818764be3beb1/mypy-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f895078594d918f93337a505f8add9bd654d1a24962b4c6ed9390e12531eb31b", size = 9487033, upload-time = "2025-06-16T16:49:57.907Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, + { url = "https://files.pythonhosted.org/packages/6a/31/e762baa3b73905c856d45ab77b4af850e8159dffffd86a52879539a08c6b/mypy-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8e08de6138043108b3b18f09d3f817a4783912e48828ab397ecf183135d84d6", size = 10998313, upload-time = "2025-07-14T20:33:24.519Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c1/25b2f0d46fb7e0b5e2bee61ec3a47fe13eff9e3c2f2234f144858bbe6485/mypy-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce4a17920ec144647d448fc43725b5873548b1aae6c603225626747ededf582d", size = 10128922, upload-time = "2025-07-14T20:34:06.414Z" }, + { url = "https://files.pythonhosted.org/packages/02/78/6d646603a57aa8a2886df1b8881fe777ea60f28098790c1089230cd9c61d/mypy-1.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff25d151cc057fdddb1cb1881ef36e9c41fa2a5e78d8dd71bee6e4dcd2bc05b", size = 11913524, upload-time = "2025-07-14T20:33:19.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/19/dae6c55e87ee426fb76980f7e78484450cad1c01c55a1dc4e91c930bea01/mypy-1.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93468cf29aa9a132bceb103bd8475f78cacde2b1b9a94fd978d50d4bdf616c9a", size = 12650527, upload-time = "2025-07-14T20:32:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/86/e1/f916845a235235a6c1e4d4d065a3930113767001d491b8b2e1b61ca56647/mypy-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98189382b310f16343151f65dd7e6867386d3e35f7878c45cfa11383d175d91f", size = 12897284, upload-time = "2025-07-14T20:33:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/ae/dc/414760708a4ea1b096bd214d26a24e30ac5e917ef293bc33cdb6fe22d2da/mypy-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:c004135a300ab06a045c1c0d8e3f10215e71d7b4f5bb9a42ab80236364429937", size = 9506493, upload-time = "2025-07-14T20:34:01.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" }, + { url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/5f8ab461369b9e62157072156935cec9d272196556bdc7c2ff5f4c7c0f9b/mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06", size = 11070019, upload-time = "2025-07-14T20:32:07.99Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/c49c9e5a2ac0badcc54beb24e774d2499748302c9568f7f09e8730e953fa/mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a", size = 10114457, upload-time = "2025-07-14T20:33:47.285Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/fb3f9c939ad9beed3e328008b3fb90b20fda2cddc0f7e4c20dbefefc3b33/mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889", size = 11857838, upload-time = "2025-07-14T20:33:14.462Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/85607ab5137d65e4f54d9797b77d5a038ef34f714929cf8ad30b03f628df/mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba", size = 12731358, upload-time = "2025-07-14T20:32:25.579Z" }, + { url = "https://files.pythonhosted.org/packages/73/d0/341dbbfb35ce53d01f8f2969facbb66486cee9804048bf6c01b048127501/mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658", size = 12917480, upload-time = "2025-07-14T20:34:21.868Z" }, + { url = "https://files.pythonhosted.org/packages/64/63/70c8b7dbfc520089ac48d01367a97e8acd734f65bd07813081f508a8c94c/mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c", size = 9589666, upload-time = "2025-07-14T20:34:16.841Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a0/6263dd11941231f688f0a8f2faf90ceac1dc243d148d314a089d2fe25108/mypy-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63e751f1b5ab51d6f3d219fe3a2fe4523eaa387d854ad06906c63883fde5b1ab", size = 10988185, upload-time = "2025-07-14T20:33:04.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/13/b8f16d6b0dc80277129559c8e7dbc9011241a0da8f60d031edb0e6e9ac8f/mypy-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fb09d05e0f1c329a36dcd30e27564a3555717cde87301fae4fb542402ddfad", size = 10120169, upload-time = "2025-07-14T20:32:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/14/ef/978ba79df0d65af680e20d43121363cf643eb79b04bf3880d01fc8afeb6f/mypy-1.17.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72c34ce05ac3a1361ae2ebb50757fb6e3624032d91488d93544e9f82db0ed6c", size = 11918121, upload-time = "2025-07-14T20:33:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/55ef70b104151a0d8280474f05268ff0a2a79be8d788d5e647257d121309/mypy-1.17.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434ad499ad8dde8b2f6391ddfa982f41cb07ccda8e3c67781b1bfd4e5f9450a8", size = 12648821, upload-time = "2025-07-14T20:32:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/26/8c/7781fcd2e1eef48fbedd3a422c21fe300a8e03ed5be2eb4bd10246a77f4e/mypy-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f105f61a5eff52e137fd73bee32958b2add9d9f0a856f17314018646af838e97", size = 12896955, upload-time = "2025-07-14T20:32:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/03ac759dabe86e98ca7b6681f114f90ee03f3ff8365a57049d311bd4a4e3/mypy-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:ba06254a5a22729853209550d80f94e28690d5530c661f9416a68ac097b13fc4", size = 9512957, upload-time = "2025-07-14T20:33:28.619Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, ] [[package]] @@ -1746,81 +1764,81 @@ wheels = [ [[package]] name = "orjson" -version = "3.10.18" +version = "3.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810, upload-time = "2025-04-29T23:30:08.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/87/03ababa86d984952304ac8ce9fbd3a317afb4a225b9a81f9b606ac60c873/orjson-3.11.0.tar.gz", hash = "sha256:2e4c129da624f291bcc607016a99e7f04a353f6874f3bd8d9b47b88597d5f700", size = 5318246, upload-time = "2025-07-15T16:08:29.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/16/2ceb9fb7bc2b11b1e4a3ea27794256e93dee2309ebe297fd131a778cd150/orjson-3.10.18-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a45e5d68066b408e4bc383b6e4ef05e717c65219a9e1390abc6155a520cac402", size = 248927, upload-time = "2025-04-29T23:28:08.643Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e1/d3c0a2bba5b9906badd121da449295062b289236c39c3a7801f92c4682b0/orjson-3.10.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be3b9b143e8b9db05368b13b04c84d37544ec85bb97237b3a923f076265ec89c", size = 136995, upload-time = "2025-04-29T23:28:11.503Z" }, - { url = "https://files.pythonhosted.org/packages/d7/51/698dd65e94f153ee5ecb2586c89702c9e9d12f165a63e74eb9ea1299f4e1/orjson-3.10.18-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9b0aa09745e2c9b3bf779b096fa71d1cc2d801a604ef6dd79c8b1bfef52b2f92", size = 132893, upload-time = "2025-04-29T23:28:12.751Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e5/155ce5a2c43a85e790fcf8b985400138ce5369f24ee6770378ee6b691036/orjson-3.10.18-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53a245c104d2792e65c8d225158f2b8262749ffe64bc7755b00024757d957a13", size = 137017, upload-time = "2025-04-29T23:28:14.498Z" }, - { url = "https://files.pythonhosted.org/packages/46/bb/6141ec3beac3125c0b07375aee01b5124989907d61c72c7636136e4bd03e/orjson-3.10.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9495ab2611b7f8a0a8a505bcb0f0cbdb5469caafe17b0e404c3c746f9900469", size = 138290, upload-time = "2025-04-29T23:28:16.211Z" }, - { url = "https://files.pythonhosted.org/packages/77/36/6961eca0b66b7809d33c4ca58c6bd4c23a1b914fb23aba2fa2883f791434/orjson-3.10.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73be1cbcebadeabdbc468f82b087df435843c809cd079a565fb16f0f3b23238f", size = 142828, upload-time = "2025-04-29T23:28:18.065Z" }, - { url = "https://files.pythonhosted.org/packages/8b/2f/0c646d5fd689d3be94f4d83fa9435a6c4322c9b8533edbb3cd4bc8c5f69a/orjson-3.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe8936ee2679e38903df158037a2f1c108129dee218975122e37847fb1d4ac68", size = 132806, upload-time = "2025-04-29T23:28:19.782Z" }, - { url = "https://files.pythonhosted.org/packages/ea/af/65907b40c74ef4c3674ef2bcfa311c695eb934710459841b3c2da212215c/orjson-3.10.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7115fcbc8525c74e4c2b608129bef740198e9a120ae46184dac7683191042056", size = 135005, upload-time = "2025-04-29T23:28:21.367Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d1/68bd20ac6a32cd1f1b10d23e7cc58ee1e730e80624e3031d77067d7150fc/orjson-3.10.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:771474ad34c66bc4d1c01f645f150048030694ea5b2709b87d3bda273ffe505d", size = 413418, upload-time = "2025-04-29T23:28:23.097Z" }, - { url = "https://files.pythonhosted.org/packages/31/31/c701ec0bcc3e80e5cb6e319c628ef7b768aaa24b0f3b4c599df2eaacfa24/orjson-3.10.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c14047dbbea52886dd87169f21939af5d55143dad22d10db6a7514f058156a8", size = 153288, upload-time = "2025-04-29T23:28:25.02Z" }, - { url = "https://files.pythonhosted.org/packages/d9/31/5e1aa99a10893a43cfc58009f9da840990cc8a9ebb75aa452210ba18587e/orjson-3.10.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:641481b73baec8db14fdf58f8967e52dc8bda1f2aba3aa5f5c1b07ed6df50b7f", size = 137181, upload-time = "2025-04-29T23:28:26.318Z" }, - { url = "https://files.pythonhosted.org/packages/bf/8c/daba0ac1b8690011d9242a0f37235f7d17df6d0ad941021048523b76674e/orjson-3.10.18-cp310-cp310-win32.whl", hash = "sha256:607eb3ae0909d47280c1fc657c4284c34b785bae371d007595633f4b1a2bbe06", size = 142694, upload-time = "2025-04-29T23:28:28.092Z" }, - { url = "https://files.pythonhosted.org/packages/16/62/8b687724143286b63e1d0fab3ad4214d54566d80b0ba9d67c26aaf28a2f8/orjson-3.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:8770432524ce0eca50b7efc2a9a5f486ee0113a5fbb4231526d414e6254eba92", size = 134600, upload-time = "2025-04-29T23:28:29.422Z" }, - { url = "https://files.pythonhosted.org/packages/97/c7/c54a948ce9a4278794f669a353551ce7db4ffb656c69a6e1f2264d563e50/orjson-3.10.18-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e0a183ac3b8e40471e8d843105da6fbe7c070faab023be3b08188ee3f85719b8", size = 248929, upload-time = "2025-04-29T23:28:30.716Z" }, - { url = "https://files.pythonhosted.org/packages/9e/60/a9c674ef1dd8ab22b5b10f9300e7e70444d4e3cda4b8258d6c2488c32143/orjson-3.10.18-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5ef7c164d9174362f85238d0cd4afdeeb89d9e523e4651add6a5d458d6f7d42d", size = 133364, upload-time = "2025-04-29T23:28:32.392Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4e/f7d1bdd983082216e414e6d7ef897b0c2957f99c545826c06f371d52337e/orjson-3.10.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd14c5d99cdc7bf93f22b12ec3b294931518aa019e2a147e8aa2f31fd3240f7", size = 136995, upload-time = "2025-04-29T23:28:34.024Z" }, - { url = "https://files.pythonhosted.org/packages/17/89/46b9181ba0ea251c9243b0c8ce29ff7c9796fa943806a9c8b02592fce8ea/orjson-3.10.18-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b672502323b6cd133c4af6b79e3bea36bad2d16bca6c1f645903fce83909a7a", size = 132894, upload-time = "2025-04-29T23:28:35.318Z" }, - { url = "https://files.pythonhosted.org/packages/ca/dd/7bce6fcc5b8c21aef59ba3c67f2166f0a1a9b0317dcca4a9d5bd7934ecfd/orjson-3.10.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51f8c63be6e070ec894c629186b1c0fe798662b8687f3d9fdfa5e401c6bd7679", size = 137016, upload-time = "2025-04-29T23:28:36.674Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4a/b8aea1c83af805dcd31c1f03c95aabb3e19a016b2a4645dd822c5686e94d/orjson-3.10.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9478ade5313d724e0495d167083c6f3be0dd2f1c9c8a38db9a9e912cdaf947", size = 138290, upload-time = "2025-04-29T23:28:38.3Z" }, - { url = "https://files.pythonhosted.org/packages/36/d6/7eb05c85d987b688707f45dcf83c91abc2251e0dd9fb4f7be96514f838b1/orjson-3.10.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:187aefa562300a9d382b4b4eb9694806e5848b0cedf52037bb5c228c61bb66d4", size = 142829, upload-time = "2025-04-29T23:28:39.657Z" }, - { url = "https://files.pythonhosted.org/packages/d2/78/ddd3ee7873f2b5f90f016bc04062713d567435c53ecc8783aab3a4d34915/orjson-3.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da552683bc9da222379c7a01779bddd0ad39dd699dd6300abaf43eadee38334", size = 132805, upload-time = "2025-04-29T23:28:40.969Z" }, - { url = "https://files.pythonhosted.org/packages/8c/09/c8e047f73d2c5d21ead9c180203e111cddeffc0848d5f0f974e346e21c8e/orjson-3.10.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e450885f7b47a0231979d9c49b567ed1c4e9f69240804621be87c40bc9d3cf17", size = 135008, upload-time = "2025-04-29T23:28:42.284Z" }, - { url = "https://files.pythonhosted.org/packages/0c/4b/dccbf5055ef8fb6eda542ab271955fc1f9bf0b941a058490293f8811122b/orjson-3.10.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5e3c9cc2ba324187cd06287ca24f65528f16dfc80add48dc99fa6c836bb3137e", size = 413419, upload-time = "2025-04-29T23:28:43.673Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f3/1eac0c5e2d6d6790bd2025ebfbefcbd37f0d097103d76f9b3f9302af5a17/orjson-3.10.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:50ce016233ac4bfd843ac5471e232b865271d7d9d44cf9d33773bcd883ce442b", size = 153292, upload-time = "2025-04-29T23:28:45.573Z" }, - { url = "https://files.pythonhosted.org/packages/1f/b4/ef0abf64c8f1fabf98791819ab502c2c8c1dc48b786646533a93637d8999/orjson-3.10.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b3ceff74a8f7ffde0b2785ca749fc4e80e4315c0fd887561144059fb1c138aa7", size = 137182, upload-time = "2025-04-29T23:28:47.229Z" }, - { url = "https://files.pythonhosted.org/packages/a9/a3/6ea878e7b4a0dc5c888d0370d7752dcb23f402747d10e2257478d69b5e63/orjson-3.10.18-cp311-cp311-win32.whl", hash = "sha256:fdba703c722bd868c04702cac4cb8c6b8ff137af2623bc0ddb3b3e6a2c8996c1", size = 142695, upload-time = "2025-04-29T23:28:48.564Z" }, - { url = "https://files.pythonhosted.org/packages/79/2a/4048700a3233d562f0e90d5572a849baa18ae4e5ce4c3ba6247e4ece57b0/orjson-3.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:c28082933c71ff4bc6ccc82a454a2bffcef6e1d7379756ca567c772e4fb3278a", size = 134603, upload-time = "2025-04-29T23:28:50.442Z" }, - { url = "https://files.pythonhosted.org/packages/03/45/10d934535a4993d27e1c84f1810e79ccf8b1b7418cef12151a22fe9bb1e1/orjson-3.10.18-cp311-cp311-win_arm64.whl", hash = "sha256:a6c7c391beaedd3fa63206e5c2b7b554196f14debf1ec9deb54b5d279b1b46f5", size = 131400, upload-time = "2025-04-29T23:28:51.838Z" }, - { url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184, upload-time = "2025-04-29T23:28:53.612Z" }, - { url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279, upload-time = "2025-04-29T23:28:55.055Z" }, - { url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799, upload-time = "2025-04-29T23:28:56.828Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791, upload-time = "2025-04-29T23:28:58.751Z" }, - { url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059, upload-time = "2025-04-29T23:29:00.129Z" }, - { url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359, upload-time = "2025-04-29T23:29:01.704Z" }, - { url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853, upload-time = "2025-04-29T23:29:03.576Z" }, - { url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131, upload-time = "2025-04-29T23:29:05.753Z" }, - { url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834, upload-time = "2025-04-29T23:29:07.35Z" }, - { url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368, upload-time = "2025-04-29T23:29:09.301Z" }, - { url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359, upload-time = "2025-04-29T23:29:10.813Z" }, - { url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466, upload-time = "2025-04-29T23:29:12.26Z" }, - { url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683, upload-time = "2025-04-29T23:29:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754, upload-time = "2025-04-29T23:29:15.338Z" }, - { url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218, upload-time = "2025-04-29T23:29:17.324Z" }, - { url = "https://files.pythonhosted.org/packages/04/f0/8aedb6574b68096f3be8f74c0b56d36fd94bcf47e6c7ed47a7bd1474aaa8/orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147", size = 249087, upload-time = "2025-04-29T23:29:19.083Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f7/7118f965541aeac6844fcb18d6988e111ac0d349c9b80cda53583e758908/orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c", size = 133273, upload-time = "2025-04-29T23:29:20.602Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d9/839637cc06eaf528dd8127b36004247bf56e064501f68df9ee6fd56a88ee/orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103", size = 136779, upload-time = "2025-04-29T23:29:22.062Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/f226ecfef31a1f0e7d6bf9a31a0bbaf384c7cbe3fce49cc9c2acc51f902a/orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595", size = 132811, upload-time = "2025-04-29T23:29:23.602Z" }, - { url = "https://files.pythonhosted.org/packages/73/2d/371513d04143c85b681cf8f3bce743656eb5b640cb1f461dad750ac4b4d4/orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc", size = 137018, upload-time = "2025-04-29T23:29:25.094Z" }, - { url = "https://files.pythonhosted.org/packages/69/cb/a4d37a30507b7a59bdc484e4a3253c8141bf756d4e13fcc1da760a0b00cb/orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc", size = 138368, upload-time = "2025-04-29T23:29:26.609Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ae/cd10883c48d912d216d541eb3db8b2433415fde67f620afe6f311f5cd2ca/orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049", size = 142840, upload-time = "2025-04-29T23:29:28.153Z" }, - { url = "https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58", size = 133135, upload-time = "2025-04-29T23:29:29.726Z" }, - { url = "https://files.pythonhosted.org/packages/13/4a/35971fd809a8896731930a80dfff0b8ff48eeb5d8b57bb4d0d525160017f/orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034", size = 134810, upload-time = "2025-04-29T23:29:31.269Z" }, - { url = "https://files.pythonhosted.org/packages/99/70/0fa9e6310cda98365629182486ff37a1c6578e34c33992df271a476ea1cd/orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1", size = 413491, upload-time = "2025-04-29T23:29:33.315Z" }, - { url = "https://files.pythonhosted.org/packages/32/cb/990a0e88498babddb74fb97855ae4fbd22a82960e9b06eab5775cac435da/orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012", size = 153277, upload-time = "2025-04-29T23:29:34.946Z" }, - { url = "https://files.pythonhosted.org/packages/92/44/473248c3305bf782a384ed50dd8bc2d3cde1543d107138fd99b707480ca1/orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f", size = 137367, upload-time = "2025-04-29T23:29:36.52Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687, upload-time = "2025-04-29T23:29:38.292Z" }, - { url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794, upload-time = "2025-04-29T23:29:40.349Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186, upload-time = "2025-04-29T23:29:41.922Z" }, - { url = "https://files.pythonhosted.org/packages/df/db/69488acaa2316788b7e171f024912c6fe8193aa2e24e9cfc7bc41c3669ba/orjson-3.10.18-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c95fae14225edfd699454e84f61c3dd938df6629a00c6ce15e704f57b58433bb", size = 249301, upload-time = "2025-04-29T23:29:44.719Z" }, - { url = "https://files.pythonhosted.org/packages/23/21/d816c44ec5d1482c654e1d23517d935bb2716e1453ff9380e861dc6efdd3/orjson-3.10.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5232d85f177f98e0cefabb48b5e7f60cff6f3f0365f9c60631fecd73849b2a82", size = 136786, upload-time = "2025-04-29T23:29:46.517Z" }, - { url = "https://files.pythonhosted.org/packages/a5/9f/f68d8a9985b717e39ba7bf95b57ba173fcd86aeca843229ec60d38f1faa7/orjson-3.10.18-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2783e121cafedf0d85c148c248a20470018b4ffd34494a68e125e7d5857655d1", size = 132711, upload-time = "2025-04-29T23:29:48.605Z" }, - { url = "https://files.pythonhosted.org/packages/b5/63/447f5955439bf7b99bdd67c38a3f689d140d998ac58e3b7d57340520343c/orjson-3.10.18-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e54ee3722caf3db09c91f442441e78f916046aa58d16b93af8a91500b7bbf273", size = 136841, upload-time = "2025-04-29T23:29:50.31Z" }, - { url = "https://files.pythonhosted.org/packages/68/9e/4855972f2be74097242e4681ab6766d36638a079e09d66f3d6a5d1188ce7/orjson-3.10.18-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2daf7e5379b61380808c24f6fc182b7719301739e4271c3ec88f2984a2d61f89", size = 138082, upload-time = "2025-04-29T23:29:51.992Z" }, - { url = "https://files.pythonhosted.org/packages/08/0f/e68431e53a39698d2355faf1f018c60a3019b4b54b4ea6be9dc6b8208a3d/orjson-3.10.18-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f39b371af3add20b25338f4b29a8d6e79a8c7ed0e9dd49e008228a065d07781", size = 142618, upload-time = "2025-04-29T23:29:53.642Z" }, - { url = "https://files.pythonhosted.org/packages/32/da/bdcfff239ddba1b6ef465efe49d7e43cc8c30041522feba9fd4241d47c32/orjson-3.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b819ed34c01d88c6bec290e6842966f8e9ff84b7694632e88341363440d4cc0", size = 132627, upload-time = "2025-04-29T23:29:55.318Z" }, - { url = "https://files.pythonhosted.org/packages/0c/28/bc634da09bbe972328f615b0961f1e7d91acb3cc68bddbca9e8dd64e8e24/orjson-3.10.18-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2f6c57debaef0b1aa13092822cbd3698a1fb0209a9ea013a969f4efa36bdea57", size = 134832, upload-time = "2025-04-29T23:29:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d2/e8ac0c2d0ec782ed8925b4eb33f040cee1f1fbd1d8b268aeb84b94153e49/orjson-3.10.18-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:755b6d61ffdb1ffa1e768330190132e21343757c9aa2308c67257cc81a1a6f5a", size = 413161, upload-time = "2025-04-29T23:29:59.148Z" }, - { url = "https://files.pythonhosted.org/packages/28/f0/397e98c352a27594566e865999dc6b88d6f37d5bbb87b23c982af24114c4/orjson-3.10.18-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ce8d0a875a85b4c8579eab5ac535fb4b2a50937267482be402627ca7e7570ee3", size = 153012, upload-time = "2025-04-29T23:30:01.066Z" }, - { url = "https://files.pythonhosted.org/packages/93/bf/2c7334caeb48bdaa4cae0bde17ea417297ee136598653b1da7ae1f98c785/orjson-3.10.18-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57b5d0673cbd26781bebc2bf86f99dd19bd5a9cb55f71cc4f66419f6b50f3d77", size = 136999, upload-time = "2025-04-29T23:30:02.93Z" }, - { url = "https://files.pythonhosted.org/packages/35/72/4827b1c0c31621c2aa1e661a899cdd2cfac0565c6cd7131890daa4ef7535/orjson-3.10.18-cp39-cp39-win32.whl", hash = "sha256:951775d8b49d1d16ca8818b1f20c4965cae9157e7b562a2ae34d3967b8f21c8e", size = 142560, upload-time = "2025-04-29T23:30:04.805Z" }, - { url = "https://files.pythonhosted.org/packages/72/91/ef8e76868e7eed478887c82f60607a8abf58dadd24e95817229a4b2e2639/orjson-3.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:fdd9d68f83f0bc4406610b1ac68bdcded8c5ee58605cc69e643a06f4d075f429", size = 134455, upload-time = "2025-04-29T23:30:06.588Z" }, + { url = "https://files.pythonhosted.org/packages/07/aa/50818f480f0edcb33290c8f35eef6dd3a31e2ff7e1195f8b236ac7419811/orjson-3.11.0-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b8913baba9751f7400f8fa4ec18a8b618ff01177490842e39e47b66c1b04bc79", size = 240422, upload-time = "2025-07-15T16:06:23.029Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/5235aff455fa76337493d21e68618e7cf53aa9db011aaeb06cf378f1344c/orjson-3.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d4d86910554de5c9c87bc560b3bdd315cc3988adbdc2acf5dda3797079407ed", size = 132473, upload-time = "2025-07-15T16:06:25.598Z" }, + { url = "https://files.pythonhosted.org/packages/23/93/bf1c4e77e7affc46cca13fb852842a86dca2dabbee1d91515ed17b1c21c4/orjson-3.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84ae3d329360cf18fb61b67c505c00dedb61b0ee23abfd50f377a58e7d7bed06", size = 127195, upload-time = "2025-07-15T16:06:27.001Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2d/64b52c6827e43aa3d98def19e188e091a6c574ca13d9ecef5f3f3284fac6/orjson-3.11.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47a54e660414baacd71ebf41a69bb17ea25abb3c5b69ce9e13e43be7ac20e342", size = 128895, upload-time = "2025-07-15T16:06:28.641Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5f/9d290bc7a88392f9f7dc2e92ceb2e3efbbebaaf56bbba655b5fe2e3d2ca3/orjson-3.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2560b740604751854be146169c1de7e7ee1e6120b00c1788ec3f3a012c6a243f", size = 132016, upload-time = "2025-07-15T16:06:32.576Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8c/b2bdc34649bbb7b44827d487aef7ad4d6a96c53ebc490ddcc191d47bc3b9/orjson-3.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7f9cd995da9e46fbac0a371f0ff6e89a21d8ecb7a8a113c0acb147b0a32f73", size = 134251, upload-time = "2025-07-15T16:06:34.075Z" }, + { url = "https://files.pythonhosted.org/packages/33/be/b763b602976aa27407e6f75331ac581258c719f8abb70f66f2de962f649f/orjson-3.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cf728cb3a013bdf9f4132575404bf885aa773d8bb4205656575e1890fc91990", size = 128078, upload-time = "2025-07-15T16:06:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/ac/24/1b0fed70392bf179ac8b5abe800f1102ed94f89ac4f889d83916947a2b4e/orjson-3.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c27de273320294121200440cd5002b6aeb922d3cb9dab3357087c69f04ca6934", size = 130734, upload-time = "2025-07-15T16:06:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/05/d2/2d042bb4fe1da067692cb70d8c01a5ce2737e2f56444e6b2d716853ce8c3/orjson-3.11.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4430ec6ff1a1f4595dd7e0fad991bdb2fed65401ed294984c490ffa025926325", size = 404040, upload-time = "2025-07-15T16:06:38.259Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c5/54938ab416c0d19c93f0d6977a47bb2b3d121e150305380b783f7d6da185/orjson-3.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:325be41a8d7c227d460a9795a181511ba0e731cf3fee088c63eb47e706ea7559", size = 144808, upload-time = "2025-07-15T16:06:39.796Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/5ead422f396ee7c8941659ceee3da001e26998971f7d5fe0a38519c48aa5/orjson-3.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9760217b84d1aee393b4436fbe9c639e963ec7bc0f2c074581ce5fb3777e466", size = 132570, upload-time = "2025-07-15T16:06:41.209Z" }, + { url = "https://files.pythonhosted.org/packages/f6/01/db8352f7d0374d7eec25144e294991800aa85738b2dc7f19cc152ba1b254/orjson-3.11.0-cp310-cp310-win32.whl", hash = "sha256:fe36e5012f886ff91c68b87a499c227fa220e9668cea96335219874c8be5fab5", size = 134763, upload-time = "2025-07-15T16:06:42.524Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/1322b64d5836d92f0b0c119d959853b3c968b8aae23dd1e3c1bfa566823b/orjson-3.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebeecd5d5511b3ca9dc4e7db0ab95266afd41baf424cc2fad8c2d3a3cdae650a", size = 129506, upload-time = "2025-07-15T16:06:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f9/2c/0b71a763f0f5130aa2631ef79e2cd84d361294665acccbb12b7a9813194e/orjson-3.11.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1785df7ada75c18411ff7e20ac822af904a40161ea9dfe8c55b3f6b66939add6", size = 240007, upload-time = "2025-07-15T16:06:45.411Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5a/f79ccd63d378b9c7c771d7a54c203d261b4c618fe3034ae95cd30f934f34/orjson-3.11.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:a57899bebbcea146616a2426d20b51b3562b4bc9f8039a3bd14fae361c23053d", size = 129320, upload-time = "2025-07-15T16:06:47.249Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8a/63dafc147fa5ba945ad809c374b8f4ee692bb6b18aa6e161c3e6b69b594e/orjson-3.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fbc2fc825aff1456dd358c11a0ad7912a4cb4537d3db92e5334af7463a967", size = 132254, upload-time = "2025-07-15T16:06:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/4d1eb230483cc689a2f039c531bb2c980029c40ca5a9b5f64dce9786e955/orjson-3.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4305a638f4cf9bed3746ca3b7c242f14e05177d5baec2527026e0f9ee6c24fb7", size = 127003, upload-time = "2025-07-15T16:06:50.34Z" }, + { url = "https://files.pythonhosted.org/packages/4f/39/b6e96072946d908684e0f4b3de1639062fd5b32016b2929c035bd8e5c847/orjson-3.11.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1235fe7bbc37164f69302199d46f29cfb874018738714dccc5a5a44042c79c77", size = 128674, upload-time = "2025-07-15T16:06:51.659Z" }, + { url = "https://files.pythonhosted.org/packages/1e/dd/c77e3013f35b202ec2cc1f78a95fadf86b8c5a320d56eb1a0bbb965a87bb/orjson-3.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a640e3954e7b4fcb160097551e54cafbde9966be3991932155b71071077881aa", size = 131846, upload-time = "2025-07-15T16:06:53.359Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7d/d83f0f96c2b142f9cdcf12df19052ea3767970989dc757598dc108db208f/orjson-3.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d750b97d22d5566955e50b02c622f3a1d32744d7a578c878b29a873190ccb7a", size = 134016, upload-time = "2025-07-15T16:06:54.691Z" }, + { url = "https://files.pythonhosted.org/packages/67/4f/d22f79a3c56dde563c4fbc12eebf9224a1b87af5e4ec61beb11f9b3eb499/orjson-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bfcfe498484161e011f8190a400591c52b026de96b3b3cbd3f21e8999b9dc0e", size = 127930, upload-time = "2025-07-15T16:06:56.001Z" }, + { url = "https://files.pythonhosted.org/packages/07/1e/26aede257db2163d974139fd4571f1e80f565216ccbd2c44ee1d43a63dcc/orjson-3.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:feaed3ed43a1d2df75c039798eb5ec92c350c7d86be53369bafc4f3700ce7df2", size = 130569, upload-time = "2025-07-15T16:06:57.275Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bf/2cb57eac8d6054b555cba27203490489a7d3f5dca8c34382f22f2f0f17ba/orjson-3.11.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1120607ec8fc98acf8c54aac6fb0b7b003ba883401fa2d261833111e2fa071", size = 403844, upload-time = "2025-07-15T16:06:59.107Z" }, + { url = "https://files.pythonhosted.org/packages/76/34/36e859ccfc45464df7b35c438c0ecc7751c930b3ebbefb50db7e3a641eb7/orjson-3.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c4b48d9775b0cf1f0aca734f4c6b272cbfacfac38e6a455e6520662f9434afb7", size = 144613, upload-time = "2025-07-15T16:07:00.48Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/5aeb84cdd0b44dc3972668944a1312f7983c2a45fb6b0e5e32b2f9408540/orjson-3.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f018ed1986d79434ac712ff19f951cd00b4dfcb767444410fbb834ebec160abf", size = 132419, upload-time = "2025-07-15T16:07:01.927Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/95ee1e61a067ad24c4921609156b3beeca8b102f6f36dca62b08e1a7c7a8/orjson-3.11.0-cp311-cp311-win32.whl", hash = "sha256:08e191f8a55ac2c00be48e98a5d10dca004cbe8abe73392c55951bfda60fc123", size = 134620, upload-time = "2025-07-15T16:07:03.304Z" }, + { url = "https://files.pythonhosted.org/packages/94/3e/afd5e284db9387023803553061ea05c785c36fe7845e4fe25912424b343f/orjson-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:b5a4214ea59c8a3b56f8d484b28114af74e9fba0956f9be5c3ce388ae143bf1f", size = 129333, upload-time = "2025-07-15T16:07:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a4/d29e9995d73f23f2444b4db299a99477a4f7e6f5bf8923b775ef43a4e660/orjson-3.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:57e8e7198a679ab21241ab3f355a7990c7447559e35940595e628c107ef23736", size = 126656, upload-time = "2025-07-15T16:07:06.288Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/241e304fb1e58ea70b720f1a9e5349c6bb7735ffac401ef1b94f422edd6d/orjson-3.11.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b4089f940c638bb1947d54e46c1cd58f4259072fcc97bc833ea9c78903150ac9", size = 240269, upload-time = "2025-07-15T16:07:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/26/7c/289457cdf40be992b43f1d90ae213ebc03a31a8e2850271ecd79e79a3135/orjson-3.11.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:8335a0ba1c26359fb5c82d643b4c1abbee2bc62875e0f2b5bde6c8e9e25eb68c", size = 129276, upload-time = "2025-07-15T16:07:10.128Z" }, + { url = "https://files.pythonhosted.org/packages/66/de/5c0528d46ded965939b6b7f75b1fe93af42b9906b0039096fc92c9001c12/orjson-3.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c1c9772dafc811d16d6a7efa3369a739da15d1720d6e58ebe7562f54d6f4a2", size = 131966, upload-time = "2025-07-15T16:07:11.509Z" }, + { url = "https://files.pythonhosted.org/packages/ad/74/39822f267b5935fb6fc961ccc443f4968a74d34fc9270b83caa44e37d907/orjson-3.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9457ccbd8b241fb4ba516417a4c5b95ba0059df4ac801309bcb4ec3870f45ad9", size = 127028, upload-time = "2025-07-15T16:07:13.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e3/28f6ed7f03db69bddb3ef48621b2b05b394125188f5909ee0a43fcf4820e/orjson-3.11.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0846e13abe79daece94a00b92574f294acad1d362be766c04245b9b4dd0e47e1", size = 129105, upload-time = "2025-07-15T16:07:14.367Z" }, + { url = "https://files.pythonhosted.org/packages/cb/50/8867fd2fc92c0ab1c3e14673ec5d9d0191202e4ab8ba6256d7a1d6943ad3/orjson-3.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5587c85ae02f608a3f377b6af9eb04829606f518257cbffa8f5081c1aacf2e2f", size = 131902, upload-time = "2025-07-15T16:07:16.176Z" }, + { url = "https://files.pythonhosted.org/packages/13/65/c189deea10342afee08006331082ff67d11b98c2394989998b3ea060354a/orjson-3.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7a1964a71c1567b4570c932a0084ac24ad52c8cf6253d1881400936565ed438", size = 134042, upload-time = "2025-07-15T16:07:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e4/cf23c3f4231d2a9a043940ab045f799f84a6df1b4fb6c9b4412cdc3ebf8c/orjson-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5a8243e73690cc6e9151c9e1dd046a8f21778d775f7d478fa1eb4daa4897c61", size = 128260, upload-time = "2025-07-15T16:07:19.651Z" }, + { url = "https://files.pythonhosted.org/packages/de/b9/2cb94d3a67edb918d19bad4a831af99cd96c3657a23daa239611bcf335d7/orjson-3.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51646f6d995df37b6e1b628f092f41c0feccf1d47e3452c6e95e2474b547d842", size = 130282, upload-time = "2025-07-15T16:07:21.022Z" }, + { url = "https://files.pythonhosted.org/packages/0b/96/df963cc973e689d4c56398647917b4ee95f47e5b6d2779338c09c015b23b/orjson-3.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:2fb8ca8f0b4e31b8aaec674c7540649b64ef02809410506a44dc68d31bd5647b", size = 403765, upload-time = "2025-07-15T16:07:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/fb/92/71429ee1badb69f53281602dbb270fa84fc2e51c83193a814d0208bb63b0/orjson-3.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:64a6a3e94a44856c3f6557e6aa56a6686544fed9816ae0afa8df9077f5759791", size = 144779, upload-time = "2025-07-15T16:07:27.339Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ab/3678b2e5ff0c622a974cb8664ed7cdda5ed26ae2b9d71ba66ec36f32d6cf/orjson-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69f95d484938d8fab5963e09131bcf9fbbb81fa4ec132e316eb2fb9adb8ce78", size = 132797, upload-time = "2025-07-15T16:07:28.717Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/74509f715ff189d2aca90ebb0bd5af6658e0f9aa2512abbe6feca4c78208/orjson-3.11.0-cp312-cp312-win32.whl", hash = "sha256:8514f9f9c667ce7d7ef709ab1a73e7fcab78c297270e90b1963df7126d2b0e23", size = 134695, upload-time = "2025-07-15T16:07:30.034Z" }, + { url = "https://files.pythonhosted.org/packages/82/ba/ef25e3e223f452a01eac6a5b38d05c152d037508dcbf87ad2858cbb7d82e/orjson-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:41b38a894520b8cb5344a35ffafdf6ae8042f56d16771b2c5eb107798cee85ee", size = 129446, upload-time = "2025-07-15T16:07:31.412Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cd/6f4d93867c5d81bb4ab2d4ac870d3d6e9ba34fa580a03b8d04bf1ce1d8ad/orjson-3.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:5579acd235dd134467340b2f8a670c1c36023b5a69c6a3174c4792af7502bd92", size = 126400, upload-time = "2025-07-15T16:07:34.143Z" }, + { url = "https://files.pythonhosted.org/packages/31/63/82d9b6b48624009d230bc6038e54778af8f84dfd54402f9504f477c5cfd5/orjson-3.11.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4a8ba9698655e16746fdf5266939427da0f9553305152aeb1a1cc14974a19cfb", size = 240125, upload-time = "2025-07-15T16:07:35.976Z" }, + { url = "https://files.pythonhosted.org/packages/16/3a/d557ed87c63237d4c97a7bac7ac054c347ab8c4b6da09748d162ca287175/orjson-3.11.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:67133847f9a35a5ef5acfa3325d4a2f7fe05c11f1505c4117bb086fc06f2a58f", size = 129189, upload-time = "2025-07-15T16:07:37.486Z" }, + { url = "https://files.pythonhosted.org/packages/69/5e/b2c9e22e2cd10aa7d76a629cee65d661e06a61fbaf4dc226386f5636dd44/orjson-3.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f797d57814975b78f5f5423acb003db6f9be5186b72d48bd97a1000e89d331d", size = 131953, upload-time = "2025-07-15T16:07:39.254Z" }, + { url = "https://files.pythonhosted.org/packages/e2/60/760fcd9b50eb44d1206f2b30c8d310b79714553b9d94a02f9ea3252ebe63/orjson-3.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:28acd19822987c5163b9e03a6e60853a52acfee384af2b394d11cb413b889246", size = 126922, upload-time = "2025-07-15T16:07:41.282Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/8c46daa867ccc92da6de9567608be62052774b924a77c78382e30d50b579/orjson-3.11.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8d38d9e1e2cf9729658e35956cf01e13e89148beb4cb9e794c9c10c5cb252f8", size = 128787, upload-time = "2025-07-15T16:07:42.681Z" }, + { url = "https://files.pythonhosted.org/packages/f2/14/a2f1b123d85f11a19e8749f7d3f9ed6c9b331c61f7b47cfd3e9a1fedb9bc/orjson-3.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05f094edd2b782650b0761fd78858d9254de1c1286f5af43145b3d08cdacfd51", size = 131895, upload-time = "2025-07-15T16:07:44.519Z" }, + { url = "https://files.pythonhosted.org/packages/c8/10/362e8192df7528e8086ea712c5cb01355c8d4e52c59a804417ba01e2eb2d/orjson-3.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d09176a4a9e04a5394a4a0edd758f645d53d903b306d02f2691b97d5c736a9e", size = 133868, upload-time = "2025-07-15T16:07:46.227Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4e/ef43582ef3e3dfd2a39bc3106fa543364fde1ba58489841120219da6e22f/orjson-3.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a585042104e90a61eda2564d11317b6a304eb4e71cd33e839f5af6be56c34d3", size = 128234, upload-time = "2025-07-15T16:07:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fa/02dabb2f1d605bee8c4bb1160cfc7467976b1ed359a62cc92e0681b53c45/orjson-3.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d2218629dbfdeeb5c9e0573d59f809d42f9d49ae6464d2f479e667aee14c3ef4", size = 130232, upload-time = "2025-07-15T16:07:50.197Z" }, + { url = "https://files.pythonhosted.org/packages/16/76/951b5619605c8d2ede80cc989f32a66abc954530d86e84030db2250c63a1/orjson-3.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:613e54a2b10b51b656305c11235a9c4a5c5491ef5c283f86483d4e9e123ed5e4", size = 403648, upload-time = "2025-07-15T16:07:52.136Z" }, + { url = "https://files.pythonhosted.org/packages/96/e2/5fa53bb411455a63b3713db90b588e6ca5ed2db59ad49b3fb8a0e94e0dda/orjson-3.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9dac7fbf3b8b05965986c5cfae051eb9a30fced7f15f1d13a5adc608436eb486", size = 144572, upload-time = "2025-07-15T16:07:54.004Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d0/7d6f91e1e0f034258c3a3358f20b0c9490070e8a7ab8880085547274c7f9/orjson-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93b64b254414e2be55ac5257124b5602c5f0b4d06b80bd27d1165efe8f36e836", size = 132766, upload-time = "2025-07-15T16:07:55.936Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f8/4d46481f1b3fb40dc826d62179f96c808eb470cdcc74b6593fb114d74af3/orjson-3.11.0-cp313-cp313-win32.whl", hash = "sha256:359cbe11bc940c64cb3848cf22000d2aef36aff7bfd09ca2c0b9cb309c387132", size = 134638, upload-time = "2025-07-15T16:07:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/85/3f/544938dcfb7337d85ee1e43d7685cf8f3bfd452e0b15a32fe70cb4ca5094/orjson-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:0759b36428067dc777b202dd286fbdd33d7f261c6455c4238ea4e8474358b1e6", size = 129411, upload-time = "2025-07-15T16:07:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/43/0c/f75015669d7817d222df1bb207f402277b77d22c4833950c8c8c7cf2d325/orjson-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:51cdca2f36e923126d0734efaf72ddbb5d6da01dbd20eab898bdc50de80d7b5a", size = 126349, upload-time = "2025-07-15T16:08:00.322Z" }, + { url = "https://files.pythonhosted.org/packages/6c/41/eac31c44ce001b3da8a6b5ebbb8a4fc2c3eaf479e2d068e36b2ea6ab7095/orjson-3.11.0-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d79c180cfb3ae68f13245d0ff551dca03d96258aa560830bf8a223bd68d8272c", size = 241023, upload-time = "2025-07-15T16:08:02.233Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d6/1edc258f3eff573af7416b2b8536032e6f4ed3759fa5773c5db95a28d2f2/orjson-3.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:105bca887532dc71ce4b05a5de95dea447a310409d7a8cf0cb1c4a120469e9ad", size = 132245, upload-time = "2025-07-15T16:08:04.734Z" }, + { url = "https://files.pythonhosted.org/packages/24/89/49236838cdc8d88b93f1c80f44531103f589307e4e783c855a6a63f28b45/orjson-3.11.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acf5a63ae9cdb88274126af85913ceae554d8fd71122effa24a53227abbeee16", size = 126981, upload-time = "2025-07-15T16:08:06.114Z" }, + { url = "https://files.pythonhosted.org/packages/80/78/8744b86efae7693344edcf255addc2a9f9e4f5552ccf71d9581d03c3e1aa/orjson-3.11.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:894635df36c0be32f1c8c8607e853b8865edb58e7618e57892e85d06418723eb", size = 128686, upload-time = "2025-07-15T16:08:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/91/8c/4c45feee9fa52488e67be2e887eb966337d4ddb6675129471f0dab98587d/orjson-3.11.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02dd4f0a1a2be943a104ce5f3ec092631ee3e9f0b4bb9eeee3400430bd94ddef", size = 131830, upload-time = "2025-07-15T16:08:14.423Z" }, + { url = "https://files.pythonhosted.org/packages/47/15/9462308306650de38d042af226e186d2fe28ee8e44c5462e011e767e6e44/orjson-3.11.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:720b4bb5e1b971960a62c2fa254c2d2a14e7eb791e350d05df8583025aa59d15", size = 134004, upload-time = "2025-07-15T16:08:16.024Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/bfa55d7681cf704d73e9c6de8138535b2f41e06a49d88bf9bdf27c8d4d7b/orjson-3.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf058105a8aed144e0d1cfe7ac4174748c3fc7203f225abaeac7f4121abccb0", size = 127893, upload-time = "2025-07-15T16:08:17.558Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bb/e91aa9e63077d8754d1578787e8917078e5c6743579290bc454bbc609241/orjson-3.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a2788f741e5a0e885e5eaf1d91d0c9106e03cb9575b0c55ba36fd3d48b0b1e9b", size = 130546, upload-time = "2025-07-15T16:08:19.21Z" }, + { url = "https://files.pythonhosted.org/packages/9d/67/4c53a325ac9abf883e922da214707f63efcb8b4d54529984df0e6aff1d0b/orjson-3.11.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:c60c99fe1e15894367b0340b2ff16c7c69f9c3f3a54aa3961a58c102b292ad94", size = 403849, upload-time = "2025-07-15T16:08:21.025Z" }, + { url = "https://files.pythonhosted.org/packages/5a/64/a779341bd2231e28eb09cf6e6260d9f713a39ae5163b0f1228ab5175bfee/orjson-3.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:99d17aab984f4d029b8f3c307e6be3c63d9ee5ef55e30d761caf05e883009949", size = 144600, upload-time = "2025-07-15T16:08:22.701Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/fc36a6e3b40df3388ecf57b18a940f6584362652e6ee57464ccc5715b2e3/orjson-3.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e98f02e23611763c9e5dfcb83bd33219231091589f0d1691e721aea9c52bf329", size = 132416, upload-time = "2025-07-15T16:08:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/3b/29/eb5ed777d7ea5d0fdee5981751e3a4e9de73f47e32bb20f1ea748b04b1d2/orjson-3.11.0-cp39-cp39-win32.whl", hash = "sha256:923301f33ea866b18f8836cf41d9c6d33e3b5cab8577d20fed34ec29f0e13a0d", size = 134617, upload-time = "2025-07-15T16:08:26.052Z" }, + { url = "https://files.pythonhosted.org/packages/72/40/feba627d9349bb1a91500e0047ae526d83bb1918545ff4dfee3e1bd7195e/orjson-3.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:475491bb78af2a0170f49e90013f1a0f1286527f3617491f8940d7e5da862da7", size = 129320, upload-time = "2025-07-15T16:08:27.484Z" }, ] [[package]] @@ -1882,11 +1900,11 @@ wheels = [ [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] @@ -2445,23 +2463,27 @@ wheels = [ [[package]] name = "pywin32" -version = "310" +version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/da/a5f38fffbba2fb99aa4aa905480ac4b8e83ca486659ac8c95bce47fb5276/pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1", size = 8848240, upload-time = "2025-03-17T00:55:46.783Z" }, - { url = "https://files.pythonhosted.org/packages/aa/fe/d873a773324fa565619ba555a82c9dabd677301720f3660a731a5d07e49a/pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d", size = 9601854, upload-time = "2025-03-17T00:55:48.783Z" }, - { url = "https://files.pythonhosted.org/packages/3c/84/1a8e3d7a15490d28a5d816efa229ecb4999cdc51a7c30dd8914f669093b8/pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213", size = 8522963, upload-time = "2025-03-17T00:55:50.969Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload-time = "2025-03-17T00:55:53.124Z" }, - { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload-time = "2025-03-17T00:55:55.203Z" }, - { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload-time = "2025-03-17T00:55:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239, upload-time = "2025-03-17T00:55:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839, upload-time = "2025-03-17T00:56:00.8Z" }, - { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470, upload-time = "2025-03-17T00:56:02.601Z" }, - { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384, upload-time = "2025-03-17T00:56:04.383Z" }, - { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039, upload-time = "2025-03-17T00:56:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload-time = "2025-03-17T00:56:07.819Z" }, - { url = "https://files.pythonhosted.org/packages/a2/cd/d09d434630edb6a0c44ad5079611279a67530296cfe0451e003de7f449ff/pywin32-310-cp39-cp39-win32.whl", hash = "sha256:851c8d927af0d879221e616ae1f66145253537bbdd321a77e8ef701b443a9a1a", size = 8848099, upload-time = "2025-03-17T00:55:42.415Z" }, - { url = "https://files.pythonhosted.org/packages/93/ff/2a8c10315ffbdee7b3883ac0d1667e267ca8b3f6f640d81d43b87a82c0c7/pywin32-310-cp39-cp39-win_amd64.whl", hash = "sha256:96867217335559ac619f00ad70e513c0fcf84b8a3af9fc2bba3b59b97da70475", size = 9602031, upload-time = "2025-03-17T00:55:44.512Z" }, + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" }, ] [[package]] @@ -2821,27 +2843,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.3" +version = "0.12.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/2a/43955b530c49684d3c38fcda18c43caf91e99204c2a065552528e0552d4f/ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77", size = 4459341, upload-time = "2025-07-11T13:21:16.086Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/ce/8d7dbedede481245b489b769d27e2934730791a9a82765cb94566c6e6abd/ruff-0.12.4.tar.gz", hash = "sha256:13efa16df6c6eeb7d0f091abae50f58e9522f3843edb40d56ad52a5a4a4b6873", size = 5131435, upload-time = "2025-07-17T17:27:19.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fd/b44c5115539de0d598d75232a1cc7201430b6891808df111b8b0506aae43/ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2", size = 10430499, upload-time = "2025-07-11T13:20:26.321Z" }, - { url = "https://files.pythonhosted.org/packages/43/c5/9eba4f337970d7f639a37077be067e4ec80a2ad359e4cc6c5b56805cbc66/ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041", size = 11213413, upload-time = "2025-07-11T13:20:30.017Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2c/fac3016236cf1fe0bdc8e5de4f24c76ce53c6dd9b5f350d902549b7719b2/ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882", size = 10586941, upload-time = "2025-07-11T13:20:33.046Z" }, - { url = "https://files.pythonhosted.org/packages/c5/0f/41fec224e9dfa49a139f0b402ad6f5d53696ba1800e0f77b279d55210ca9/ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901", size = 10783001, upload-time = "2025-07-11T13:20:35.534Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ca/dd64a9ce56d9ed6cad109606ac014860b1c217c883e93bf61536400ba107/ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0", size = 10269641, upload-time = "2025-07-11T13:20:38.459Z" }, - { url = "https://files.pythonhosted.org/packages/63/5c/2be545034c6bd5ce5bb740ced3e7014d7916f4c445974be11d2a406d5088/ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6", size = 11875059, upload-time = "2025-07-11T13:20:41.517Z" }, - { url = "https://files.pythonhosted.org/packages/8e/d4/a74ef1e801ceb5855e9527dae105eaff136afcb9cc4d2056d44feb0e4792/ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc", size = 12658890, upload-time = "2025-07-11T13:20:44.442Z" }, - { url = "https://files.pythonhosted.org/packages/13/c8/1057916416de02e6d7c9bcd550868a49b72df94e3cca0aeb77457dcd9644/ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687", size = 12232008, upload-time = "2025-07-11T13:20:47.374Z" }, - { url = "https://files.pythonhosted.org/packages/f5/59/4f7c130cc25220392051fadfe15f63ed70001487eca21d1796db46cbcc04/ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e", size = 11499096, upload-time = "2025-07-11T13:20:50.348Z" }, - { url = "https://files.pythonhosted.org/packages/d4/01/a0ad24a5d2ed6be03a312e30d32d4e3904bfdbc1cdbe63c47be9d0e82c79/ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311", size = 11688307, upload-time = "2025-07-11T13:20:52.945Z" }, - { url = "https://files.pythonhosted.org/packages/93/72/08f9e826085b1f57c9a0226e48acb27643ff19b61516a34c6cab9d6ff3fa/ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07", size = 10661020, upload-time = "2025-07-11T13:20:55.799Z" }, - { url = "https://files.pythonhosted.org/packages/80/a0/68da1250d12893466c78e54b4a0ff381370a33d848804bb51279367fc688/ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12", size = 10246300, upload-time = "2025-07-11T13:20:58.222Z" }, - { url = "https://files.pythonhosted.org/packages/6a/22/5f0093d556403e04b6fd0984fc0fb32fbb6f6ce116828fd54306a946f444/ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b", size = 11263119, upload-time = "2025-07-11T13:21:01.503Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/f4c0b69bdaffb9968ba40dd5fa7df354ae0c73d01f988601d8fac0c639b1/ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f", size = 11746990, upload-time = "2025-07-11T13:21:04.524Z" }, - { url = "https://files.pythonhosted.org/packages/fe/84/7cc7bd73924ee6be4724be0db5414a4a2ed82d06b30827342315a1be9e9c/ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d", size = 10589263, upload-time = "2025-07-11T13:21:07.148Z" }, - { url = "https://files.pythonhosted.org/packages/07/87/c070f5f027bd81f3efee7d14cb4d84067ecf67a3a8efb43aadfc72aa79a6/ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7", size = 11695072, upload-time = "2025-07-11T13:21:11.004Z" }, - { url = "https://files.pythonhosted.org/packages/e0/30/f3eaf6563c637b6e66238ed6535f6775480db973c836336e4122161986fc/ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1", size = 10805855, upload-time = "2025-07-11T13:21:13.547Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9f/517bc5f61bad205b7f36684ffa5415c013862dee02f55f38a217bdbe7aa4/ruff-0.12.4-py3-none-linux_armv6l.whl", hash = "sha256:cb0d261dac457ab939aeb247e804125a5d521b21adf27e721895b0d3f83a0d0a", size = 10188824, upload-time = "2025-07-17T17:26:31.412Z" }, + { url = "https://files.pythonhosted.org/packages/28/83/691baae5a11fbbde91df01c565c650fd17b0eabed259e8b7563de17c6529/ruff-0.12.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:55c0f4ca9769408d9b9bac530c30d3e66490bd2beb2d3dae3e4128a1f05c7442", size = 10884521, upload-time = "2025-07-17T17:26:35.084Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8d/756d780ff4076e6dd035d058fa220345f8c458391f7edfb1c10731eedc75/ruff-0.12.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a8224cc3722c9ad9044da7f89c4c1ec452aef2cfe3904365025dd2f51daeae0e", size = 10277653, upload-time = "2025-07-17T17:26:37.897Z" }, + { url = "https://files.pythonhosted.org/packages/8d/97/8eeee0f48ece153206dce730fc9e0e0ca54fd7f261bb3d99c0a4343a1892/ruff-0.12.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9949d01d64fa3672449a51ddb5d7548b33e130240ad418884ee6efa7a229586", size = 10485993, upload-time = "2025-07-17T17:26:40.68Z" }, + { url = "https://files.pythonhosted.org/packages/49/b8/22a43d23a1f68df9b88f952616c8508ea6ce4ed4f15353b8168c48b2d7e7/ruff-0.12.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:be0593c69df9ad1465e8a2d10e3defd111fdb62dcd5be23ae2c06da77e8fcffb", size = 10022824, upload-time = "2025-07-17T17:26:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/cd/70/37c234c220366993e8cffcbd6cadbf332bfc848cbd6f45b02bade17e0149/ruff-0.12.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7dea966bcb55d4ecc4cc3270bccb6f87a337326c9dcd3c07d5b97000dbff41c", size = 11524414, upload-time = "2025-07-17T17:26:46.219Z" }, + { url = "https://files.pythonhosted.org/packages/14/77/c30f9964f481b5e0e29dd6a1fae1f769ac3fd468eb76fdd5661936edd262/ruff-0.12.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afcfa3ab5ab5dd0e1c39bf286d829e042a15e966b3726eea79528e2e24d8371a", size = 12419216, upload-time = "2025-07-17T17:26:48.883Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/af7fe0a4202dce4ef62c5e33fecbed07f0178f5b4dd9c0d2fcff5ab4a47c/ruff-0.12.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c057ce464b1413c926cdb203a0f858cd52f3e73dcb3270a3318d1630f6395bb3", size = 11976756, upload-time = "2025-07-17T17:26:51.754Z" }, + { url = "https://files.pythonhosted.org/packages/09/d1/33fb1fc00e20a939c305dbe2f80df7c28ba9193f7a85470b982815a2dc6a/ruff-0.12.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64b90d1122dc2713330350626b10d60818930819623abbb56535c6466cce045", size = 11020019, upload-time = "2025-07-17T17:26:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/64/f4/e3cd7f7bda646526f09693e2e02bd83d85fff8a8222c52cf9681c0d30843/ruff-0.12.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abc48f3d9667fdc74022380b5c745873499ff827393a636f7a59da1515e7c57", size = 11277890, upload-time = "2025-07-17T17:26:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d0/69a85fb8b94501ff1a4f95b7591505e8983f38823da6941eb5b6badb1e3a/ruff-0.12.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2b2449dc0c138d877d629bea151bee8c0ae3b8e9c43f5fcaafcd0c0d0726b184", size = 10348539, upload-time = "2025-07-17T17:26:59.381Z" }, + { url = "https://files.pythonhosted.org/packages/16/a0/91372d1cb1678f7d42d4893b88c252b01ff1dffcad09ae0c51aa2542275f/ruff-0.12.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:56e45bb11f625db55f9b70477062e6a1a04d53628eda7784dce6e0f55fd549eb", size = 10009579, upload-time = "2025-07-17T17:27:02.462Z" }, + { url = "https://files.pythonhosted.org/packages/23/1b/c4a833e3114d2cc0f677e58f1df6c3b20f62328dbfa710b87a1636a5e8eb/ruff-0.12.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:478fccdb82ca148a98a9ff43658944f7ab5ec41c3c49d77cd99d44da019371a1", size = 10942982, upload-time = "2025-07-17T17:27:05.343Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ce/ce85e445cf0a5dd8842f2f0c6f0018eedb164a92bdf3eda51984ffd4d989/ruff-0.12.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0fc426bec2e4e5f4c4f182b9d2ce6a75c85ba9bcdbe5c6f2a74fcb8df437df4b", size = 11343331, upload-time = "2025-07-17T17:27:08.652Z" }, + { url = "https://files.pythonhosted.org/packages/35/cf/441b7fc58368455233cfb5b77206c849b6dfb48b23de532adcc2e50ccc06/ruff-0.12.4-py3-none-win32.whl", hash = "sha256:4de27977827893cdfb1211d42d84bc180fceb7b72471104671c59be37041cf93", size = 10267904, upload-time = "2025-07-17T17:27:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7e/20af4a0df5e1299e7368d5ea4350412226afb03d95507faae94c80f00afd/ruff-0.12.4-py3-none-win_amd64.whl", hash = "sha256:fe0b9e9eb23736b453143d72d2ceca5db323963330d5b7859d60d101147d461a", size = 11209038, upload-time = "2025-07-17T17:27:14.417Z" }, + { url = "https://files.pythonhosted.org/packages/11/02/8857d0dfb8f44ef299a5dfd898f673edefb71e3b533b3b9d2db4c832dd13/ruff-0.12.4-py3-none-win_arm64.whl", hash = "sha256:0618ec4442a83ab545e5b71202a5c0ed7791e8471435b94e655b570a5031a98e", size = 10469336, upload-time = "2025-07-17T17:27:16.913Z" }, ] [[package]] diff --git a/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py b/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py index 0b474f259..7b63c528c 100644 --- a/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py +++ b/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py @@ -11,6 +11,7 @@ from typing import ( cast, get_type_hints, ) +from warnings import warn from langchain_core.language_models import ( BaseChatModel, @@ -34,6 +35,8 @@ from langchain_core.tools import BaseTool from pydantic import BaseModel from typing_extensions import Annotated, TypedDict +from langgraph._internal._runnable import RunnableCallable, RunnableLike +from langgraph._internal._typing import MISSING from langgraph.errors import ErrorCode, create_error_message from langgraph.graph import END, StateGraph from langgraph.graph.message import add_messages @@ -43,7 +46,7 @@ from langgraph.prebuilt._internal import ToolCallWithContext from langgraph.prebuilt.tool_node import ToolNode from langgraph.store.base import BaseStore from langgraph.types import Checkpointer, Send -from langgraph.utils.runnable import RunnableCallable, RunnableLike +from langgraph.warnings import LangGraphDeprecatedSinceV10 StructuredResponse = Union[dict, BaseModel] StructuredResponseSchema = Union[dict, type[BaseModel]] @@ -253,7 +256,7 @@ def create_react_agent( pre_model_hook: Optional[RunnableLike] = None, post_model_hook: Optional[RunnableLike] = None, state_schema: Optional[StateSchemaType] = None, - config_schema: Optional[Type[Any]] = None, + context_schema: Optional[Type[Any]] = None, checkpointer: Optional[Checkpointer] = None, store: Optional[BaseStore] = None, interrupt_before: Optional[list[str]] = None, @@ -261,6 +264,7 @@ def create_react_agent( debug: bool = False, version: Literal["v1", "v2"] = "v2", name: Optional[str] = None, + **deprecated_kwargs: Any, ) -> CompiledStateGraph: """Creates an agent graph that calls tools in a loop until a stopping condition is met. @@ -335,8 +339,7 @@ def create_react_agent( state_schema: An optional state schema that defines graph state. Must have `messages` and `remaining_steps` keys. Defaults to `AgentState` that defines those two keys. - config_schema: An optional schema for configuration. - Use this to expose configurable parameters via agent.config_specs. + context_schema: An optional schema for runtime context. checkpointer: An optional checkpoint saver object. This is used for persisting the state of the graph (e.g., as chat memory) for a single thread (e.g., a single conversation). store: An optional store object. This is used for persisting data @@ -403,6 +406,17 @@ def create_react_agent( print(chunk) ``` """ + if ( + config_schema := deprecated_kwargs.pop("config_schema", MISSING) + ) is not MISSING: + warn( + "`config_schema` is no longer supported. Use `context_schema` instead.", + category=LangGraphDeprecatedSinceV10, + ) + + if context_schema is not None: + context_schema = config_schema + if version not in ("v1", "v2"): raise ValueError( f"Invalid version {version}. Supported versions are 'v1' and 'v2'." @@ -591,7 +605,7 @@ def create_react_agent( if not tool_calling_enabled: # Define a new graph - workflow = StateGraph(state_schema, config_schema=config_schema) + workflow = StateGraph(state_schema=state_schema, context_schema=context_schema) workflow.add_node( "agent", RunnableCallable(call_model, acall_model), @@ -664,7 +678,9 @@ def create_react_agent( ] # Define a new graph - workflow = StateGraph(state_schema or AgentState, config_schema=config_schema) + workflow = StateGraph( + state_schema=state_schema or AgentState, context_schema=context_schema + ) # Define the two nodes we will cycle between workflow.add_node( diff --git a/libs/prebuilt/langgraph/prebuilt/tool_node.py b/libs/prebuilt/langgraph/prebuilt/tool_node.py index da84cc520..12f751d6f 100644 --- a/libs/prebuilt/langgraph/prebuilt/tool_node.py +++ b/libs/prebuilt/langgraph/prebuilt/tool_node.py @@ -70,11 +70,11 @@ from langchain_core.tools.base import ( from pydantic import BaseModel from typing_extensions import Annotated, get_args, get_origin +from langgraph._internal._runnable import RunnableCallable from langgraph.errors import GraphBubbleUp from langgraph.prebuilt._internal import ToolCallWithContext from langgraph.store.base import BaseStore from langgraph.types import Command, Send -from langgraph.utils.runnable import RunnableCallable INVALID_TOOL_NAME_ERROR_TEMPLATE = ( "Error: {requested_tool} is not a valid tool, try one of [{available_tools}]." @@ -447,11 +447,11 @@ class ToolNode(RunnableCallable): response = self.tools_by_name[call["name"]].invoke(call_args, config) # GraphInterrupt is a special exception that will always be raised. - # It can be triggered in the following scenarios: - # (1) a NodeInterrupt is raised inside a tool - # (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool - # (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph - # called as a tool + # It can be triggered in the following scenarios, + # Where GraphInterrupt(GraphBubbleUp) is raised from an `interrupt` invocation most commonly: + # (1) a GraphInterrupt is raised inside a tool + # (2) a GraphInterrupt is raised inside a graph node for a graph called as a tool + # (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool # (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture) except GraphBubbleUp as e: raise e diff --git a/libs/prebuilt/langgraph/prebuilt/tool_validator.py b/libs/prebuilt/langgraph/prebuilt/tool_validator.py index 0e58c7d6c..d63e0c535 100644 --- a/libs/prebuilt/langgraph/prebuilt/tool_validator.py +++ b/libs/prebuilt/langgraph/prebuilt/tool_validator.py @@ -34,7 +34,7 @@ from pydantic import BaseModel, ValidationError from pydantic.v1 import BaseModel as BaseModelV1 from pydantic.v1 import ValidationError as ValidationErrorV1 -from langgraph.utils.runnable import RunnableCallable +from langgraph._internal._runnable import RunnableCallable def _default_format_error( diff --git a/libs/prebuilt/tests/memory_assert.py b/libs/prebuilt/tests/memory_assert.py index c12cbb3b8..0b22a605a 100644 --- a/libs/prebuilt/tests/memory_assert.py +++ b/libs/prebuilt/tests/memory_assert.py @@ -11,7 +11,7 @@ from langgraph.checkpoint.base import ( SerializerProtocol, ) from langgraph.checkpoint.memory import InMemorySaver, PersistentDict -from langgraph.pregel.checkpoint import copy_checkpoint +from langgraph.pregel._checkpoint import copy_checkpoint class MemorySaverAssertImmutable(InMemorySaver): diff --git a/libs/prebuilt/tests/test_react_agent.py b/libs/prebuilt/tests/test_react_agent.py index d31c67256..b337cb325 100644 --- a/libs/prebuilt/tests/test_react_agent.py +++ b/libs/prebuilt/tests/test_react_agent.py @@ -30,6 +30,7 @@ from pydantic.v1 import BaseModel as BaseModelV1 from typing_extensions import TypedDict from langgraph.checkpoint.base import BaseCheckpointSaver +from langgraph.config import get_stream_writer from langgraph.graph import START, MessagesState, StateGraph, add_messages from langgraph.graph.message import REMOVE_ALL_MESSAGES from langgraph.prebuilt import ( @@ -54,7 +55,6 @@ from langgraph.prebuilt.tool_node import ( from langgraph.store.base import BaseStore from langgraph.store.memory import InMemoryStore from langgraph.types import Command, Interrupt, interrupt -from langgraph.utils.config import get_stream_writer from tests.any_str import AnyStr from tests.messages import _AnyIdHumanMessage, _AnyIdToolMessage from tests.model import FakeToolCallingModel @@ -1299,9 +1299,7 @@ def test_tool_node_node_interrupt( assert task.interrupts == ( Interrupt( value="provide value for foo", - when="during", - resumable=True, - ns=[AnyStr("tools:")], + id=AnyStr(), ), ) diff --git a/libs/prebuilt/tests/test_tool_node.py b/libs/prebuilt/tests/test_tool_node.py index b94094a23..ec7c91fdd 100644 --- a/libs/prebuilt/tests/test_tool_node.py +++ b/libs/prebuilt/tests/test_tool_node.py @@ -14,7 +14,7 @@ from langchain_core.tools import tool as dec_tool from pydantic import BaseModel, ValidationError from pydantic.v1 import ValidationError as ValidationErrorV1 -from langgraph.errors import NodeInterrupt +from langgraph.errors import GraphBubbleUp, GraphInterrupt from langgraph.prebuilt import ToolNode from langgraph.prebuilt.tool_node import TOOL_CALL_ERROR_TEMPLATE from langgraph.types import Command, Send @@ -462,16 +462,16 @@ def test_tool_node_incorrect_tool_name(): def test_tool_node_node_interrupt(): - def tool_interrupt(some_val: int) -> str: + def tool_interrupt(some_val: int) -> None: """Tool docstring.""" - raise NodeInterrupt("foo") + raise GraphBubbleUp("foo") - def handle(e: NodeInterrupt): + def handle(e: GraphInterrupt): return "handled" - for handle_tool_errors in (True, (NodeInterrupt,), "handled", handle, False): + for handle_tool_errors in (True, (GraphBubbleUp,), "handled", handle, False): node = ToolNode([tool_interrupt], handle_tool_errors=handle_tool_errors) - with pytest.raises(NodeInterrupt) as exc_info: + with pytest.raises(GraphBubbleUp) as exc_info: node.invoke( { "messages": [ diff --git a/libs/prebuilt/uv.lock b/libs/prebuilt/uv.lock index 1f3f105d2..a300dc1a3 100644 --- a/libs/prebuilt/uv.lock +++ b/libs/prebuilt/uv.lock @@ -316,7 +316,7 @@ wheels = [ [[package]] name = "langgraph" -version = "0.5.4" +version = "0.6.0a1" source = { editable = "../langgraph" } dependencies = [ { name = "langchain-core" }, @@ -344,7 +344,7 @@ dev = [ { name = "langgraph-checkpoint", editable = "../checkpoint" }, { name = "langgraph-checkpoint-postgres", editable = "../checkpoint-postgres" }, { name = "langgraph-checkpoint-sqlite", editable = "../checkpoint-sqlite" }, - { name = "langgraph-cli", extras = ["inmem"] }, + { name = "langgraph-cli", extras = ["inmem"], editable = "../cli" }, { name = "langgraph-prebuilt", editable = "." }, { name = "langgraph-sdk", editable = "../sdk-py" }, { name = "mypy" }, @@ -507,7 +507,7 @@ dev = [ [[package]] name = "langgraph-sdk" -version = "0.1.74" +version = "0.2.0a1" source = { editable = "../sdk-py" } dependencies = [ { name = "httpx" }, diff --git a/libs/sdk-py/langgraph_sdk/auth/types.py b/libs/sdk-py/langgraph_sdk/auth/types.py index c9a5bd523..c0260ee72 100644 --- a/libs/sdk-py/langgraph_sdk/auth/types.py +++ b/libs/sdk-py/langgraph_sdk/auth/types.py @@ -556,7 +556,8 @@ class AssistantsCreate(typing.TypedDict, total=False): create_params = { "assistant_id": UUID("123e4567-e89b-12d3-a456-426614174000"), "graph_id": "graph123", - "config": {"key": "value"}, + "config": {"tags": ["tag1", "tag2"]}, + "context": {"key": "value"}, "metadata": {"owner": "user123"}, "if_exists": "do_nothing", "name": "Assistant 1" @@ -570,9 +571,11 @@ class AssistantsCreate(typing.TypedDict, total=False): graph_id: str """Graph ID to use for this assistant.""" - config: dict[str, typing.Any] | typing.Any | None + config: dict[str, typing.Any] """typing.Optional configuration for the assistant.""" + context: dict[str, typing.Any] + metadata: MetadataInput """typing.Optional metadata to attach to the assistant.""" @@ -610,7 +613,8 @@ class AssistantsUpdate(typing.TypedDict, total=False): update_params = { "assistant_id": UUID("123e4567-e89b-12d3-a456-426614174000"), "graph_id": "graph123", - "config": {"key": "value"}, + "config": {"tags": ["tag1", "tag2"]}, + "context": {"key": "value"}, "metadata": {"owner": "user123"}, "name": "Assistant 1", "version": 1 @@ -624,9 +628,12 @@ class AssistantsUpdate(typing.TypedDict, total=False): graph_id: str | None """typing.Optional graph ID to update.""" - config: dict[str, typing.Any] | typing.Any | None + config: dict[str, typing.Any] """typing.Optional configuration to update.""" + context: dict[str, typing.Any] + """The static context of the assistant.""" + metadata: MetadataInput """typing.Optional metadata to update.""" diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index 00ace666c..32447e863 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -39,6 +39,7 @@ from langgraph_sdk.schema import ( Checkpoint, Command, Config, + Context, Cron, CronSortBy, DisconnectMode, @@ -646,9 +647,9 @@ class AssistantsClient: } } }, - 'config_schema': + 'context_schema': { - 'title': 'Configurable', + 'title': 'Context', 'type': 'object', 'properties': { @@ -706,6 +707,7 @@ class AssistantsClient: graph_id: str | None, config: Config | None = None, *, + context: Context | None = None, metadata: Json = None, assistant_id: str | None = None, if_exists: OnConflictBehavior | None = None, @@ -721,6 +723,8 @@ class AssistantsClient: graph_id: The ID of the graph the assistant should use. The graph ID is normally set in your langgraph.json configuration. config: Configuration to use for the graph. metadata: Metadata to add to assistant. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" assistant_id: Assistant ID to use, will default to a random UUID if not provided. if_exists: How to handle duplicate creation. Defaults to 'raise' under the hood. Must be either 'raise' (raise error if duplicate), or 'do_nothing' (return existing assistant). @@ -738,7 +742,7 @@ class AssistantsClient: client = get_client(url="http://localhost:2024") assistant = await client.assistants.create( graph_id="agent", - config={"configurable": {"model_name": "openai"}}, + context={"model_name": "openai"}, metadata={"number":1}, assistant_id="my-assistant-id", if_exists="do_nothing", @@ -751,6 +755,8 @@ class AssistantsClient: } if config: payload["config"] = config + if context: + payload["context"] = context if metadata: payload["metadata"] = metadata if assistant_id: @@ -769,6 +775,7 @@ class AssistantsClient: *, graph_id: str | None = None, config: Config | None = None, + context: Context | None = None, metadata: Json = None, name: str | None = None, headers: dict[str, str] | None = None, @@ -783,6 +790,8 @@ class AssistantsClient: graph_id: The ID of the graph the assistant should use. The graph ID is normally set in your langgraph.json configuration. If None, assistant will keep pointing to same graph. config: Configuration to use for the graph. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" metadata: Metadata to merge with existing assistant metadata. name: The new name for the assistant. headers: Optional custom headers to include with the request. @@ -799,7 +808,7 @@ class AssistantsClient: assistant = await client.assistants.update( assistant_id='e280dad7-8618-443f-87f1-8e41841c180f', graph_id="other-graph", - config={"configurable": {"model_name": "anthropic"}}, + context={"model_name": "anthropic"}, metadata={"number":2} ) ``` @@ -810,6 +819,8 @@ class AssistantsClient: payload["graph_id"] = graph_id if config: payload["config"] = config + if context: + payload["context"] = context if metadata: payload["metadata"] = metadata if name: @@ -1482,7 +1493,7 @@ class ThreadsClient: class RunsClient: """Client for managing runs in LangGraph. - A run is a single assistant invocation with optional input, config, and metadata. + A run is a single assistant invocation with optional input, config, context, and metadata. This client manages runs, which can be stateful (on threads) or stateless. ???+ example "Example" @@ -1509,6 +1520,7 @@ class RunsClient: stream_resumable: bool = False, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, checkpoint_during: bool | None = None, @@ -1562,6 +1574,7 @@ class RunsClient: stream_resumable: bool = False, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, checkpoint_during: bool | None = None, @@ -1592,6 +1605,8 @@ class RunsClient: If true, the stream can be resumed and replayed in its entirety even after disconnection. metadata: Metadata to assign to the run. config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" checkpoint: The checkpoint to resume from. checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). interrupt_before: Nodes to interrupt immediately before they get executed. @@ -1623,7 +1638,7 @@ class RunsClient: input={"messages": [{"role": "user", "content": "how are you?"}]}, stream_mode=["values","debug"], metadata={"name":"my_run"}, - config={"configurable": {"model_name": "anthropic"}}, + context={"model_name": "anthropic"}, interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], feedback_keys=["my_feedback_key_1","my_feedback_key_2"], @@ -1650,6 +1665,7 @@ class RunsClient: {k: v for k, v in command.items() if v is not None} if command else None ), "config": config, + "context": context, "metadata": metadata, "stream_mode": stream_mode, "stream_subgraphs": stream_subgraphs, @@ -1701,6 +1717,7 @@ class RunsClient: metadata: dict | None = None, checkpoint_during: bool | None = None, config: Config | None = None, + context: Context | None = None, interrupt_before: All | Sequence[str] | None = None, interrupt_after: All | Sequence[str] | None = None, webhook: str | None = None, @@ -1724,6 +1741,7 @@ class RunsClient: stream_resumable: bool = False, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, checkpoint_during: bool | None = None, @@ -1749,6 +1767,7 @@ class RunsClient: stream_resumable: bool = False, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, checkpoint_during: bool | None = None, @@ -1777,6 +1796,8 @@ class RunsClient: If true, the stream can be resumed and replayed in its entirety even after disconnection. metadata: Metadata to assign to the run. config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" checkpoint: The checkpoint to resume from. checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). interrupt_before: Nodes to interrupt immediately before they get executed. @@ -1805,7 +1826,7 @@ class RunsClient: assistant_id="my_assistant_id", input={"messages": [{"role": "user", "content": "hello!"}]}, metadata={"name":"my_run"}, - config={"configurable": {"model_name": "openai"}}, + context={"model_name": "openai"}, interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], webhook="https://my.fake.webhook.com", @@ -1849,10 +1870,13 @@ class RunsClient: 'graph_id': 'agent', 'thread_id': 'my_thread_id', 'checkpoint_id': None, - 'model_name': "openai", 'assistant_id': 'my_assistant_id' - } + }, }, + 'context': + { + 'model_name': 'openai' + } 'webhook': "https://my.fake.webhook.com", 'temporary': False, 'stream_mode': ['values'], @@ -1873,6 +1897,7 @@ class RunsClient: "stream_subgraphs": stream_subgraphs, "stream_resumable": stream_resumable, "config": config, + "context": context, "metadata": metadata, "assistant_id": assistant_id, "interrupt_before": interrupt_before, @@ -1919,6 +1944,7 @@ class RunsClient: command: Command | None = None, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, checkpoint_during: bool | None = None, @@ -1944,6 +1970,7 @@ class RunsClient: command: Command | None = None, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint_during: bool | None = None, interrupt_before: All | Sequence[str] | None = None, interrupt_after: All | Sequence[str] | None = None, @@ -1966,6 +1993,7 @@ class RunsClient: command: Command | None = None, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, checkpoint_during: bool | None = None, @@ -1992,6 +2020,8 @@ class RunsClient: command: A command to execute. Cannot be combined with input. metadata: Metadata to assign to the run. config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" checkpoint: The checkpoint to resume from. checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). interrupt_before: Nodes to interrupt immediately before they get executed. @@ -2022,7 +2052,7 @@ class RunsClient: assistant_id="agent", input={"messages": [{"role": "user", "content": "how are you?"}]}, metadata={"name":"my_run"}, - config={"configurable": {"model_name": "anthropic"}}, + context={"model_name": "anthropic"}, interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], webhook="https://my.fake.webhook.com", @@ -2068,6 +2098,7 @@ class RunsClient: {k: v for k, v in command.items() if v is not None} if command else None ), "config": config, + "context": context, "metadata": metadata, "assistant_id": assistant_id, "interrupt_before": interrupt_before, @@ -2332,7 +2363,7 @@ class RunsClient: class CronClient: """Client for managing recurrent runs (cron jobs) in LangGraph. - A run is a single invocation of an assistant with optional input and config. + A run is a single invocation of an assistant with optional input, config, and context. This client allows scheduling recurring runs to occur automatically. ???+ example "Example Usage" @@ -2365,6 +2396,7 @@ class CronClient: input: dict | None = None, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint_during: bool | None = None, interrupt_before: All | list[str] | None = None, interrupt_after: All | list[str] | None = None, @@ -2382,6 +2414,8 @@ class CronClient: input: The input to the graph. metadata: Metadata to assign to the cron job runs. config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). interrupt_before: Nodes to interrupt immediately before they get executed. @@ -2405,7 +2439,7 @@ class CronClient: schedule="27 15 * * *", input={"messages": [{"role": "user", "content": "hello!"}]}, metadata={"name":"my_run"}, - config={"configurable": {"model_name": "openai"}}, + context={"model_name": "openai"}, interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], webhook="https://my.fake.webhook.com", @@ -2418,6 +2452,7 @@ class CronClient: "input": input, "config": config, "metadata": metadata, + "context": context, "assistant_id": assistant_id, "checkpoint_during": checkpoint_during, "interrupt_before": interrupt_before, @@ -2439,6 +2474,7 @@ class CronClient: input: dict | None = None, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint_during: bool | None = None, interrupt_before: All | list[str] | None = None, interrupt_after: All | list[str] | None = None, @@ -2455,6 +2491,8 @@ class CronClient: input: The input to the graph. metadata: Metadata to assign to the cron job runs. config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). interrupt_before: Nodes to interrupt immediately before they get executed. interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. @@ -2475,7 +2513,7 @@ class CronClient: schedule="27 15 * * *", input={"messages": [{"role": "user", "content": "hello!"}]}, metadata={"name":"my_run"}, - config={"configurable": {"model_name": "openai"}}, + context={"model_name": "openai"}, interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], webhook="https://my.fake.webhook.com", @@ -2489,6 +2527,7 @@ class CronClient: "input": input, "config": config, "metadata": metadata, + "context": context, "assistant_id": assistant_id, "checkpoint_during": checkpoint_during, "interrupt_before": interrupt_before, @@ -3216,6 +3255,7 @@ class SyncAssistantsClient: 'created_at': '2024-06-25T17:10:33.109781+00:00', 'updated_at': '2024-06-25T17:10:33.109781+00:00', 'config': {}, + 'context': {}, 'metadata': {'created_by': 'system'} } ``` @@ -3379,6 +3419,20 @@ class SyncAssistantsClient: 'type': 'string' } } + }, + 'context_schema': + { + 'title': 'Context', + 'type': 'object', + 'properties': + { + 'model_name': + { + 'title': 'Model Name', + 'enum': ['anthropic', 'openai'], + 'type': 'string' + } + } } } ``` @@ -3422,6 +3476,7 @@ class SyncAssistantsClient: graph_id: str | None, config: Config | None = None, *, + context: Context | None = None, metadata: Json = None, assistant_id: str | None = None, if_exists: OnConflictBehavior | None = None, @@ -3436,6 +3491,8 @@ class SyncAssistantsClient: Args: graph_id: The ID of the graph the assistant should use. The graph ID is normally set in your langgraph.json configuration. config: Configuration to use for the graph. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" metadata: Metadata to add to assistant. assistant_id: Assistant ID to use, will default to a random UUID if not provided. if_exists: How to handle duplicate creation. Defaults to 'raise' under the hood. @@ -3454,7 +3511,7 @@ class SyncAssistantsClient: client = get_sync_client(url="http://localhost:2024") assistant = client.assistants.create( graph_id="agent", - config={"configurable": {"model_name": "openai"}}, + context={"model_name": "openai"}, metadata={"number":1}, assistant_id="my-assistant-id", if_exists="do_nothing", @@ -3467,6 +3524,8 @@ class SyncAssistantsClient: } if config: payload["config"] = config + if context: + payload["context"] = context if metadata: payload["metadata"] = metadata if assistant_id: @@ -3485,6 +3544,7 @@ class SyncAssistantsClient: *, graph_id: str | None = None, config: Config | None = None, + context: Context | None = None, metadata: Json = None, name: str | None = None, headers: dict[str, str] | None = None, @@ -3499,6 +3559,8 @@ class SyncAssistantsClient: graph_id: The ID of the graph the assistant should use. The graph ID is normally set in your langgraph.json configuration. If None, assistant will keep pointing to same graph. config: Configuration to use for the graph. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" metadata: Metadata to merge with existing assistant metadata. name: The new name for the assistant. headers: Optional custom headers to include with the request. @@ -3515,7 +3577,7 @@ class SyncAssistantsClient: assistant = client.assistants.update( assistant_id='e280dad7-8618-443f-87f1-8e41841c180f', graph_id="other-graph", - config={"configurable": {"model_name": "anthropic"}}, + context={"model_name": "anthropic"}, metadata={"number":2} ) ``` @@ -3525,6 +3587,8 @@ class SyncAssistantsClient: payload["graph_id"] = graph_id if config: payload["config"] = config + if context: + payload["context"] = context if metadata: payload["metadata"] = metadata if name: @@ -4220,6 +4284,7 @@ class SyncRunsClient: stream_subgraphs: bool = False, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, checkpoint_during: bool | None = None, @@ -4248,6 +4313,7 @@ class SyncRunsClient: stream_resumable: bool = False, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint_during: bool | None = None, interrupt_before: All | Sequence[str] | None = None, interrupt_after: All | Sequence[str] | None = None, @@ -4273,6 +4339,7 @@ class SyncRunsClient: stream_resumable: bool = False, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, checkpoint_during: bool | None = None, @@ -4303,6 +4370,8 @@ class SyncRunsClient: If true, the stream can be resumed and replayed in its entirety even after disconnection. metadata: Metadata to assign to the run. config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" checkpoint: The checkpoint to resume from. checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). interrupt_before: Nodes to interrupt immediately before they get executed. @@ -4335,7 +4404,7 @@ class SyncRunsClient: input={"messages": [{"role": "user", "content": "how are you?"}]}, stream_mode=["values","debug"], metadata={"name":"my_run"}, - config={"configurable": {"model_name": "anthropic"}}, + context={"model_name": "anthropic"}, interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], feedback_keys=["my_feedback_key_1","my_feedback_key_2"], @@ -4359,6 +4428,7 @@ class SyncRunsClient: {k: v for k, v in command.items() if v is not None} if command else None ), "config": config, + "context": context, "metadata": metadata, "stream_mode": stream_mode, "stream_subgraphs": stream_subgraphs, @@ -4409,6 +4479,7 @@ class SyncRunsClient: stream_resumable: bool = False, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint_during: bool | None = None, interrupt_before: All | Sequence[str] | None = None, interrupt_after: All | Sequence[str] | None = None, @@ -4433,6 +4504,7 @@ class SyncRunsClient: stream_resumable: bool = False, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, checkpoint_during: bool | None = None, @@ -4458,6 +4530,7 @@ class SyncRunsClient: stream_resumable: bool = False, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, checkpoint_during: bool | None = None, @@ -4486,6 +4559,8 @@ class SyncRunsClient: If true, the stream can be resumed and replayed in its entirety even after disconnection. metadata: Metadata to assign to the run. config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" checkpoint: The checkpoint to resume from. checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). interrupt_before: Nodes to interrupt immediately before they get executed. @@ -4514,7 +4589,7 @@ class SyncRunsClient: assistant_id="my_assistant_id", input={"messages": [{"role": "user", "content": "hello!"}]}, metadata={"name":"my_run"}, - config={"configurable": {"model_name": "openai"}}, + context={"model_name": "openai"}, interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], webhook="https://my.fake.webhook.com", @@ -4558,10 +4633,13 @@ class SyncRunsClient: 'graph_id': 'agent', 'thread_id': 'my_thread_id', 'checkpoint_id': None, - 'model_name': "openai", 'assistant_id': 'my_assistant_id' } }, + 'context': + { + 'model_name': 'openai' + }, 'webhook': "https://my.fake.webhook.com", 'temporary': False, 'stream_mode': ['values'], @@ -4582,6 +4660,7 @@ class SyncRunsClient: "stream_subgraphs": stream_subgraphs, "stream_resumable": stream_resumable, "config": config, + "context": context, "metadata": metadata, "assistant_id": assistant_id, "interrupt_before": interrupt_before, @@ -4630,6 +4709,7 @@ class SyncRunsClient: command: Command | None = None, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, checkpoint_during: bool | None = None, @@ -4654,6 +4734,7 @@ class SyncRunsClient: command: Command | None = None, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint_during: bool | None = None, interrupt_before: All | Sequence[str] | None = None, interrupt_after: All | Sequence[str] | None = None, @@ -4675,6 +4756,7 @@ class SyncRunsClient: command: Command | None = None, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint_during: bool | None = None, checkpoint: Checkpoint | None = None, checkpoint_id: str | None = None, @@ -4700,6 +4782,8 @@ class SyncRunsClient: command: The command to execute. metadata: Metadata to assign to the run. config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" checkpoint: The checkpoint to resume from. checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). interrupt_before: Nodes to interrupt immediately before they get executed. @@ -4730,7 +4814,7 @@ class SyncRunsClient: assistant_id="agent", input={"messages": [{"role": "user", "content": "how are you?"}]}, metadata={"name":"my_run"}, - config={"configurable": {"model_name": "anthropic"}}, + context={"model_name": "anthropic"}, interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], webhook="https://my.fake.webhook.com", @@ -4777,6 +4861,7 @@ class SyncRunsClient: {k: v for k, v in command.items() if v is not None} if command else None ), "config": config, + "context": context, "metadata": metadata, "assistant_id": assistant_id, "interrupt_before": interrupt_before, @@ -5059,6 +5144,7 @@ class SyncCronClient: metadata: dict | None = None, checkpoint_during: bool | None = None, config: Config | None = None, + context: Context | None = None, interrupt_before: All | list[str] | None = None, interrupt_after: All | list[str] | None = None, webhook: str | None = None, @@ -5075,6 +5161,8 @@ class SyncCronClient: input: The input to the graph. metadata: Metadata to assign to the cron job runs. config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). interrupt_before: Nodes to interrupt immediately before they get executed. interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. @@ -5096,7 +5184,7 @@ class SyncCronClient: schedule="27 15 * * *", input={"messages": [{"role": "user", "content": "hello!"}]}, metadata={"name":"my_run"}, - config={"configurable": {"model_name": "openai"}}, + context={"model_name": "openai"}, interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], webhook="https://my.fake.webhook.com", @@ -5109,6 +5197,7 @@ class SyncCronClient: "input": input, "config": config, "metadata": metadata, + "context": context, "assistant_id": assistant_id, "interrupt_before": interrupt_before, "interrupt_after": interrupt_after, @@ -5129,6 +5218,7 @@ class SyncCronClient: input: dict | None = None, metadata: dict | None = None, config: Config | None = None, + context: Context | None = None, checkpoint_during: bool | None = None, interrupt_before: All | list[str] | None = None, interrupt_after: All | list[str] | None = None, @@ -5145,6 +5235,8 @@ class SyncCronClient: input: The input to the graph. metadata: Metadata to assign to the cron job runs. config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Supported with langgraph>=0.6.0" checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). interrupt_before: Nodes to interrupt immediately before they get executed. interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. @@ -5165,7 +5257,7 @@ class SyncCronClient: schedule="27 15 * * *", input={"messages": [{"role": "user", "content": "hello!"}]}, metadata={"name":"my_run"}, - config={"configurable": {"model_name": "openai"}}, + context={"model_name": "openai"}, checkpoint_during=True, interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], @@ -5180,6 +5272,7 @@ class SyncCronClient: "input": input, "config": config, "metadata": metadata, + "context": context, "assistant_id": assistant_id, "interrupt_before": interrupt_before, "interrupt_after": interrupt_after, diff --git a/libs/sdk-py/langgraph_sdk/schema.py b/libs/sdk-py/langgraph_sdk/schema.py index 1f125e0df..18759c30b 100644 --- a/libs/sdk-py/langgraph_sdk/schema.py +++ b/libs/sdk-py/langgraph_sdk/schema.py @@ -12,6 +12,8 @@ from typing import ( TypedDict, ) +from typing_extensions import TypeAlias + Json = Optional[dict[str, Any]] """Represents a JSON-like structure, which can be None or a dictionary with string keys and any values.""" @@ -129,6 +131,8 @@ SortOrder = Literal["asc", "desc"] The order to sort by. """ +Context: TypeAlias = dict[str, Any] + class Config(TypedDict, total=False): """Configuration options for a call.""" @@ -183,6 +187,9 @@ class GraphSchema(TypedDict): config_schema: dict | None """The schema for the graph config. Missing if unable to generate JSON schema from graph.""" + context_schema: dict | None + """The schema for the graph context. + Missing if unable to generate JSON schema from graph.""" Subgraphs = dict[str, GraphSchema] @@ -197,6 +204,8 @@ class AssistantBase(TypedDict): """The ID of the graph.""" config: Config """The assistant config.""" + context: Context + """The static context of the assistant.""" created_at: datetime """The time the assistant was created.""" metadata: Json @@ -222,17 +231,13 @@ class Assistant(AssistantBase): """The last time the assistant was updated.""" -class Interrupt(TypedDict, total=False): +class Interrupt(TypedDict): """Represents an interruption in the execution flow.""" value: Any """The value associated with the interrupt.""" - when: Literal["during"] - """When the interrupt occurred.""" - resumable: bool - """Whether the interrupt can be resumed.""" - ns: list[str] | None - """Optional namespace for the interrupt.""" + id: str + """The ID of the interrupt. Can be used to resume the interrupt.""" class Thread(TypedDict): @@ -356,6 +361,8 @@ class RunCreate(TypedDict): """Additional metadata to associate with the run.""" config: Config | None """Configuration options for the run.""" + context: Context | None + """The static context of the run.""" checkpoint_id: str | None """The identifier of a checkpoint to resume from.""" interrupt_before: list[str] | None diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml index 68ef541a1..df8c94fe7 100644 --- a/libs/sdk-py/pyproject.toml +++ b/libs/sdk-py/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "langgraph-sdk" -version = "0.1.74" +version = "0.2.0a1" description = "SDK for interacting with LangGraph API" authors = [] requires-python = ">=3.9" diff --git a/libs/sdk-py/uv.lock b/libs/sdk-py/uv.lock index 246dc37ba..8e9b727e0 100644 --- a/libs/sdk-py/uv.lock +++ b/libs/sdk-py/uv.lock @@ -119,7 +119,7 @@ wheels = [ [[package]] name = "langgraph-sdk" -version = "0.1.74" +version = "0.2.0a1" source = { editable = "." } dependencies = [ { name = "httpx" },