release: prep for langgraph v0.6 (#5325)

This commit is contained in:
Sydney Runkle
2025-07-22 13:14:31 -04:00
committed by GitHub
132 changed files with 5954 additions and 5257 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ name: CI
on:
push:
branches: [main]
branches: [main, v1]
pull_request:
permissions:
+32 -22
View File
@@ -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.
+3 -3
View File
@@ -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")
@@ -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")
@@ -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': '...',
# > }
# > ]
@@ -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
+4 -4
View File
@@ -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.
+29 -26
View File
@@ -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:
+38 -26
View File
@@ -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.
+30 -30
View File
@@ -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()
```
@@ -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'}}
```
+2 -1
View File
@@ -2,5 +2,6 @@
options:
members:
- TAG_HIDDEN
- TAG_NOSTREAM
- START
- END
- END
+35 -33
View File
@@ -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",
Generated
+20 -20
View File
@@ -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" },
@@ -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 = {
@@ -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()
@@ -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,
)
+1 -2
View File
@@ -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": "",
}
}
+1 -2
View File
@@ -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": "",
}
}
@@ -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": "",
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ class TestSqliteSaver:
"configurable": {
"thread_id": "thread-1",
# for backwards compatibility testing
"thread_ts": "1",
"checkpoint_id": "1",
"checkpoint_ns": "",
}
}
+1 -1
View File
@@ -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`).
@@ -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",
+1 -2
View File
@@ -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 = {
+2 -2
View File
@@ -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)
+4 -4
View File
@@ -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:
+2 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
@@ -0,0 +1,4 @@
"""Internal modules for LangGraph.
This module is not part of the public API, and thus stability is not guaranteed.
"""
@@ -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"))
@@ -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,
}
@@ -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:
@@ -128,6 +128,3 @@ class SyncQueue:
return len(self._queue)
__class_getitem__ = classmethod(types.GenericAlias)
__all__ = ["AsyncQueue", "SyncQueue"]
@@ -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
@@ -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(
@@ -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."""
+18 -6
View File
@@ -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",
)
@@ -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
+10 -13
View File
@@ -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",
]
+3 -1
View File
@@ -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]
@@ -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
@@ -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
@@ -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."""
+5 -3
View File
@@ -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
@@ -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:
+4 -5
View File
@@ -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
+48 -117
View File
@@ -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}'")
+37 -4
View File
@@ -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):
+35 -22
View File
@@ -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]
)
+3 -3
View File
@@ -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",
)
@@ -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,
+96
View File
@@ -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
+8 -3
View File
@@ -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]))
+167 -166
View File
@@ -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<State>.
@@ -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=<class 'float'>, 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:{}"
+15 -2
View File
@@ -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()
+1 -1
View File
@@ -1,3 +1,3 @@
from langgraph.managed.is_last_step import IsLastStep, RemainingSteps
__all__ = ["IsLastStep", "RemainingSteps"]
__all__ = ("IsLastStep", "RemainingSteps")
+3 -1
View File
@@ -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
@@ -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]):
File diff suppressed because it is too large Load Diff
@@ -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,
@@ -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
@@ -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
@@ -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
@@ -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")
@@ -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
@@ -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:
@@ -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
]
@@ -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
@@ -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)
@@ -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)
@@ -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]
@@ -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(
@@ -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
@@ -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
+10 -7
View File
@@ -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):
File diff suppressed because it is too large Load Diff
+29 -6
View File
@@ -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
+25 -17
View File
@@ -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
+11
View File
@@ -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,
)
+85
View File
@@ -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
+84 -99
View File
@@ -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],
),
)
)
+26 -6
View File
@@ -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)
+2
View File
@@ -2,6 +2,8 @@
from importlib import metadata
__all__ = ("__version__",)
try:
__version__ = metadata.version(__package__)
except metadata.PackageNotFoundError:
+13
View File
@@ -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))
+3 -2
View File
@@ -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" ]
@@ -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 -. &nbsp;exit&nbsp; .-> __end__;
agent -. &nbsp;continue&nbsp; .-> tools;
tools --> agent;
'''
# ---
# name: test_conditional_graph[memory].2
'''
---
config:
flowchart:
curve: linear
---
graph TD;
agent(agent)
tools(tools<hr/><small><em>parents = {}
version = 2
variant = b</em></small>)
__start__([<p>__start__</p>]):::first
__end__([<p>__end__</p>]):::last
__start__ --> agent;
agent -. &nbsp;exit&nbsp; .-> __end__;
agent -. &nbsp;continue&nbsp; .-> 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"
@@ -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__ -. &nbsp;go-left&nbsp; .-> left;
__start__ -. &nbsp;go-right&nbsp; .-> 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',
+3 -2
View File
@@ -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
+3 -3
View File
@@ -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:
+1 -1
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More