Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f53e5532e | ||
|
|
da62cfe64f | ||
|
|
b647dcb0f2 | ||
|
|
4df5680732 | ||
|
|
74a17a6d4c | ||
|
|
e2a3698250 | ||
|
|
4dfdb9a83e | ||
|
|
583d8c9499 | ||
|
|
15bbede7bc | ||
|
|
3ffdf4bb3f | ||
|
|
6578698414 | ||
|
|
048ae6c17b | ||
|
|
0e2c2eb13a | ||
|
|
515c4ffebe | ||
|
|
eefe057a47 | ||
|
|
18f34c30d8 | ||
|
|
2670bcf330 | ||
|
|
f6fb2ef5ca | ||
|
|
2fb7e92879 | ||
|
|
46b2d08a8a | ||
|
|
649b742e0a | ||
|
|
fd4629e778 | ||
|
|
d14f98f01b | ||
|
|
c0b56bf60d | ||
|
|
e8b875906f | ||
|
|
d48faecd42 | ||
|
|
5e175e098b | ||
|
|
bcf335651e | ||
|
|
965849823a | ||
|
|
45e7101457 | ||
|
|
ecd75a8c4d | ||
|
|
edec5c055e | ||
|
|
ff310cc8d6 | ||
|
|
233bd78ee4 | ||
|
|
b818bf2fba | ||
|
|
c5ec568cfb | ||
|
|
29548b2e27 |
@@ -1867,6 +1867,7 @@
|
||||
|
||||
"/store/items": {
|
||||
"put": {
|
||||
"tags": ["store/manage"],
|
||||
"summary": "Store or update an item.",
|
||||
"operationId": "put_item",
|
||||
"requestBody": {
|
||||
@@ -1892,6 +1893,7 @@
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["store/manage"],
|
||||
"summary": "Delete an item.",
|
||||
"operationId": "delete_item",
|
||||
"requestBody": {
|
||||
@@ -1917,6 +1919,7 @@
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": ["store/manage"],
|
||||
"summary": "Retrieve a single item.",
|
||||
"operationId": "get_item",
|
||||
"parameters": [
|
||||
@@ -1962,6 +1965,7 @@
|
||||
},
|
||||
"/store/items/search": {
|
||||
"post": {
|
||||
"tags": ["store/manage"],
|
||||
"summary": "Search for items within a namespace prefix.",
|
||||
"operationId": "search_items",
|
||||
"requestBody": {
|
||||
@@ -1994,6 +1998,7 @@
|
||||
},
|
||||
"/store/namespaces": {
|
||||
"post": {
|
||||
"tags": ["store/manage"],
|
||||
"summary": "List namespaces with optional match conditions.",
|
||||
"operationId": "list_namespaces",
|
||||
"requestBody": {
|
||||
|
||||
@@ -103,15 +103,15 @@ Parallel processing is vital for efficient multi-agent systems and complex tasks
|
||||
|
||||
For practical implementation, see our [map-reduce tutorial](../how-tos/map-reduce.ipynb).
|
||||
|
||||
### Sub-graphs
|
||||
### Subgraphs
|
||||
|
||||
Sub-graphs are essential for managing complex agent architectures, particularly in multi-agent systems. They allow:
|
||||
[Subgraphs](./low_level.md#subgraphs) are essential for managing complex agent architectures, particularly in [multi-agent systems](./multi_agent.md). They allow:
|
||||
|
||||
- Isolated state management for individual agents
|
||||
- Hierarchical organization of agent teams
|
||||
- Controlled communication between agents and the main system
|
||||
|
||||
Sub-graphs communicate with the parent graph through overlapping keys in the state schema. This enables flexible, modular agent design. For implementation details, refer to our [sub-graph tutorial](../how-tos/subgraph.ipynb).
|
||||
Subgraphs communicate with the parent graph through overlapping keys in the state schema. This enables flexible, modular agent design. For implementation details, refer to our [subgraph how-to guide](../how-tos/subgraph.ipynb).
|
||||
|
||||
### Reflection
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 35 KiB |
@@ -52,12 +52,12 @@ By default, the graph will have the same input and output schemas. If you want t
|
||||
|
||||
Typically, all graph nodes communicate with a single schema. This means that they will read and write to the same state channels. But, there are cases where we want more control over this:
|
||||
|
||||
* Internal nodes can pass information that is not required in the graph's input / output.
|
||||
* We may also want to use different input / output schemas for the graph. The output might, for example, only contain a single relevant output key.
|
||||
- Internal nodes can pass information that is not required in the graph's input / output.
|
||||
- We may also want to use different input / output schemas for the graph. The output might, for example, only contain a single relevant output key.
|
||||
|
||||
It is possible to have nodes write to private state channels inside the graph for internal node communication. We can simply define a private schema, `PrivateState`. See [this notebook](../how-tos/pass_private_state.ipynb) for more detail.
|
||||
It is possible to have nodes write to private state channels inside the graph for internal node communication. We can simply define a private schema, `PrivateState`. See [this notebook](../how-tos/pass_private_state.ipynb) for more detail.
|
||||
|
||||
It is also possible to define explicit input and output schemas for a graph. In these cases, we define an "internal" schema that contains *all* keys relevant to graph operations. But, we also define `input` and `output` schemas that are sub-sets of the "internal" schema to constrain the input and output of the graph. See [this notebook](../how-tos/input_output_schema.ipynb) for more detail.
|
||||
It is also possible to define explicit input and output schemas for a graph. In these cases, we define an "internal" schema that contains _all_ keys relevant to graph operations. But, we also define `input` and `output` schemas that are sub-sets of the "internal" schema to constrain the input and output of the graph. See [this notebook](../how-tos/input_output_schema.ipynb) for more detail.
|
||||
|
||||
Let's look at an example:
|
||||
|
||||
@@ -101,11 +101,12 @@ graph = builder.compile()
|
||||
graph.invoke({"user_input":"My"})
|
||||
{'graph_output': 'My name is Lance'}
|
||||
```
|
||||
|
||||
There are two subtle and important points to note here:
|
||||
|
||||
1. We pass `state: InputState` as the input schema to `node_1`. But, we write out to `foo`, a channel in `OverallState`. How can we write out to a state channel that is not included in the input schema? This is because a node *can write to any state channel in the graph state.* The graph state is the union of of the state channels defined at initialization, which includes `OverallState` and the filters `InputState` and `OutputState`.
|
||||
1. We pass `state: InputState` as the input schema to `node_1`. But, we write out to `foo`, a channel in `OverallState`. How can we write out to a state channel that is not included in the input schema? This is because a node _can write to any state channel in the graph state._ The graph state is the union of of the state channels defined at initialization, which includes `OverallState` and the filters `InputState` and `OutputState`.
|
||||
|
||||
2. We initialize the graph with `StateGraph(OverallState,input=InputState,output=OutputState)`. So, how can we write to `PrivateState` in `node_2`? How does the graph gain access to this schema if it was not passed in the `StateGraph` initialization? We can do this because *nodes can also declare additional state channels* as long as the state schema definition exists. In this case, the `PrivateState` schema is defined, so we can add `bar` as a new state channel in the graph and write to it.
|
||||
2. We initialize the graph with `StateGraph(OverallState,input=InputState,output=OutputState)`. So, how can we write to `PrivateState` in `node_2`? How does the graph gain access to this schema if it was not passed in the `StateGraph` initialization? We can do this because _nodes can also declare additional state channels_ as long as the state schema definition exists. In this case, the `PrivateState` schema is defined, so we can add `bar` as a new state channel in the graph and write to it.
|
||||
|
||||
### Reducers
|
||||
|
||||
@@ -323,7 +324,7 @@ graph.add_conditional_edges("node_a", continue_to_jokes)
|
||||
|
||||
## Persistence
|
||||
|
||||
LangGraph provides built-in persistence for your agent's state using [checkpointers][langgraph.checkpoint.base.BaseCheckpointSaver]. Checkpointers save snapshots of the graph state at every superstep, allowing resumption at any time. This enables features like human-in-the-loop interactions, memory management, and fault-tolerance. You can even directly manipulate a graph's state after its execution using the
|
||||
LangGraph provides built-in persistence for your agent's state using [checkpointers][langgraph.checkpoint.base.BaseCheckpointSaver]. Checkpointers save snapshots of the graph state at every superstep, allowing resumption at any time. This enables features like human-in-the-loop interactions, memory management, and fault-tolerance. You can even directly manipulate a graph's state after its execution using the
|
||||
appropriate `get` and `update` methods. For more details, see the [persistence conceptual guide](./persistence.md).
|
||||
|
||||
## Threads
|
||||
@@ -416,10 +417,112 @@ def my_node(state: State) -> State:
|
||||
return state
|
||||
```
|
||||
|
||||
## Subgraphs
|
||||
|
||||
A subgraph is a [graph](#graphs) that is used as a [node](#nodes) in another graph. This is nothing more than the age-old concept of encapsulation, applied to LangGraph. Some reasons for using subgraphs are:
|
||||
|
||||
- building [multi-agent systems](./multi_agent.md)
|
||||
|
||||
- when you want to reuse a set of nodes in multiple graphs, which maybe share some state, you can define them once in a subgraph and then use them in multiple parent graphs
|
||||
|
||||
- when you want different teams to work on different parts of the graph independently, you can define each part as a subgraph, and as long as the subgraph interface (the input and output schemas) is respected, the parent graph can be built without knowing any details of the subgraph
|
||||
|
||||
There are two ways to add subgraphs to a parent graph:
|
||||
|
||||
- add a node with the compiled subgraph: this is useful when the parent graph and the subgraph share state keys and you don't need to transform state on the way in or out
|
||||
|
||||
```python
|
||||
builder.add_node("subgraph", subgraph_builder.compile())
|
||||
```
|
||||
|
||||
- add a node with a function that invokes the subgraph: this is useful when the parent graph and the subgraph have different state schemas and you need to transform state before or after calling the subgraph
|
||||
|
||||
```python
|
||||
subgraph = subgraph_builder.compile()
|
||||
|
||||
def call_subgraph(state: State):
|
||||
return subgraph.invoke({"subgraph_key": state["parent_key"]})
|
||||
|
||||
builder.add_node("subgraph", call_subgraph)
|
||||
```
|
||||
|
||||
Let's take a look at examples for each.
|
||||
|
||||
### As a compiled graph
|
||||
|
||||
The simplest way to create subgraph nodes is by using a [compiled subgraph](#compiling-your-graph) directly. When doing so, it is **important** that the parent graph and the subgraph [state schemas](#state) share at least one key which they can use to communicate. If your graph and subgraph do not share any keys, you should use write a function [invoking the subgraph](#as-a-function) instead.
|
||||
|
||||
!!! Note
|
||||
If you pass extra keys to the subgraph node (i.e., in addition to the shared keys), they will be ignored by the subgraph node. Similarly, if you return extra keys from the subgraph, they will be ignored by the parent graph.
|
||||
|
||||
```python
|
||||
from langgraph.graph import START, StateGraph
|
||||
from typing import TypedDict
|
||||
|
||||
class State(TypedDict):
|
||||
foo: str
|
||||
|
||||
class SubgraphState(TypedDict):
|
||||
foo: str # note that this key is shared with the parent graph state
|
||||
bar: str
|
||||
|
||||
# Define subgraph
|
||||
def subgraph_node(state: SubgraphState):
|
||||
# note that this subgraph node can communicate with the parent graph via the shared "foo" key
|
||||
return {"foo": state["foo"] + "bar"}
|
||||
|
||||
subgraph_builder = StateGraph(SubgraphState)
|
||||
subgraph_builder.add_node(subgraph_node)
|
||||
...
|
||||
subgraph = subgraph_builder.compile()
|
||||
|
||||
# Define parent graph
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("subgraph", subgraph)
|
||||
...
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
### As a function
|
||||
|
||||
You might want to define a subgraph with a completely different schema. In this case, you can create a node function that invokes the subgraph. This function will need to [transform](../how-tos/subgraph-transform-state.ipynb) the input (parent) state to the subgraph state before invoking the subgraph, and transform the results back to the parent state before returning the state update from the node.
|
||||
|
||||
```python
|
||||
class State(TypedDict):
|
||||
foo: str
|
||||
|
||||
class SubgraphState(TypedDict):
|
||||
# note that none of these keys are shared with the parent graph state
|
||||
bar: str
|
||||
baz: str
|
||||
|
||||
# Define subgraph
|
||||
def subgraph_node(state: SubgraphState):
|
||||
return {"bar": state["bar"] + "baz"}
|
||||
|
||||
subgraph_builder = StateGraph(SubgraphState)
|
||||
subgraph_builder.add_node(subgraph_node)
|
||||
...
|
||||
subgraph = subgraph_builder.compile()
|
||||
|
||||
# Define parent graph
|
||||
def node(state: State):
|
||||
# transform the state to the subgraph state
|
||||
response = subgraph.invoke({"bar": state["foo"]})
|
||||
# transform response back to the parent state
|
||||
return {"foo": response["bar"]}
|
||||
|
||||
builder = StateGraph(State)
|
||||
# note that we are using `node` function instead of a compiled subgraph
|
||||
builder.add_node(node)
|
||||
...
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
## Visualization
|
||||
|
||||
It's often nice to be able to visualize graphs, especially as they get more complex. LangGraph comes with several built-in ways to visualize graphs. See [this how-to guide](../how-tos/visualization.ipynb) for more info.
|
||||
|
||||
## Streaming
|
||||
|
||||
LangGraph is built with first class support for streaming, including streaming updates from graph nodes during the execution, streaming tokens from LLM calls and more. See this [conceptual guide](./streaming.md) for more information.
|
||||
LangGraph is built with first class support for streaming, including streaming updates from graph nodes during the execution, streaming tokens from LLM calls and more. See this [conceptual guide](./streaming.md) for more information.
|
||||
|
||||
@@ -1,138 +1,174 @@
|
||||
# Multi-agent Systems
|
||||
|
||||
A multi-agent system is a system with multiple independent actors powered by LLMs that are connected in a specific way. These actors can be as simple as a prompt and an LLM call, or as complex as a [ReAct](./agentic_concepts.md#react-implementation) agent.
|
||||
An [agent](./agentic_concepts.md#agent-architectures) is _a system that uses an LLM to decide the control flow of an application_. As you develop these systems, they might grow more complex over time, making them harder to manage and scale. For example, you might run into the following problems:
|
||||
|
||||
The primary benefits of this architecture are:
|
||||
- agent has too many tools at its disposal and makes poor decisions about which tool to call next
|
||||
- context grows too complex for a single agent to keep track of
|
||||
- there is a need for multiple specialization areas in the system (e.g. planner, researcher, math expert, etc.)
|
||||
|
||||
* **Modularity**: Separate agents facilitate easier development, testing, and maintenance of agentic systems.
|
||||
* **Specialization**: You can create expert agents focused on specific domains, and compose them into more complex applications
|
||||
* **Control**: You can explicitly control how agents communicate (as opposed to relying on function calling)
|
||||
To tackle these, you might consider breaking your application into multiple smaller, independent agents and composing them into a **multi-agent system**. These independent agents can be as simple as a prompt and an LLM call, or as complex as a [ReAct](./agentic_concepts.md#react-implementation) agent (and more!).
|
||||
|
||||
## Multi-agent systems in LangGraph
|
||||
The primary benefits of using multi-agent systems are:
|
||||
|
||||
### Agents as nodes
|
||||
- **Modularity**: Separate agents make it easier to develop, test, and maintain agentic systems.
|
||||
- **Specialization**: You can create expert agents focused on specific domains, which helps with the overall system performance.
|
||||
- **Control**: You can explicitly control how agents communicate (as opposed to relying on function calling).
|
||||
|
||||
Agents can be defined as nodes in LangGraph. As any other node in the LangGraph, these agent nodes receive the graph state as an input and return an update to the state as their output.
|
||||
## Multi-agent architectures
|
||||
|
||||
* Simple **LLM nodes**: single LLMs with custom prompts
|
||||
* **Subgraph nodes**: complex graphs called inside the orchestrator graph node
|
||||

|
||||
|
||||

|
||||
There are several ways to connect agents in a multi-agent system:
|
||||
|
||||
### Agents as tools
|
||||
- **Network**: each agent can communicate with [every other agent](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/multi-agent-collaboration/). Any agent can decide which other agent to call next.
|
||||
- **Supervisor**: each agent communicates with a single [supervisor](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/) agent. Supervisor agent makes decisions on which agent should be called next.
|
||||
- **Supervisor (tool-calling)**: this is a special case of supervisor architecture. Individual agents can be represented as tools. In this case, a supervisor agent uses a tool-calling LLM to decide which of the agent tools to call, as well as the arguments to pass to those agents.
|
||||
- **Hierarchical**: you can define a multi-agent system with a supervisor of supervisors. This is a generalization of the supervisor architecture and allows for more complex control flows.
|
||||
- **Custom multi-agent workflow**: each agent communicates with only a subset of agents. Parts of the flow are deterministic, and only some agents can decide which other agents to call next.
|
||||
|
||||
Agents can also be defined as tools. In this case, the orchestrator agent (e.g. ReAct agent) would use a tool-calling LLM to decide which of the agent tools to call, as well as the arguments to pass to those agents.
|
||||
### Network
|
||||
|
||||
You could also take a "mega-graph" approach – incorporating subordinate agents' nodes directly into the parent, orchestrator graph. However, this is not recommended for complex subordinate agents, as it would make the overall system harder to scale, maintain and debug – you should use subgraphs or tools in those cases.
|
||||
In this architecture, agents are defined as graph nodes. Each agent can communicate with every other agent (many-to-many connections) and can decide which agent to call next. While very flexible, this architecture doesn't scale well as the number of agents grows:
|
||||
|
||||
## Communication in multi-agent systems
|
||||
- hard to enforce which agent should be called next
|
||||
- hard to determine how much [information](#shared-message-list) should be passed between the agents
|
||||
|
||||
A big question in multi-agent systems is how the agents communicate amongst themselves and with the orchestrator agent. This involves both the schema of how they communicate, as well as the sequence in which they communicate. LangGraph is perfect for orchestrating these types of systems and allows you to define both.
|
||||
We recommend avoiding this architecture in production and using one of the below architectures instead.
|
||||
|
||||
### Schema
|
||||
### Supervisor
|
||||
|
||||
LangGraph provides a lot of flexibility for how to communicate within multi-agent architectures.
|
||||
|
||||
* A node in LangGraph can have a [private input state schema](https://langchain-ai.github.io/langgraph/how-tos/pass_private_state/) that is distinct from the graph state schema. This allows passing additional information during the graph execution that is only needed for executing a particular node.
|
||||
* Subgraph node agents can have independent [input / output state schemas](https://langchain-ai.github.io/langgraph/how-tos/input_output_schema/). In this case it’s important to [add input / output transformations](https://langchain-ai.github.io/langgraph/how-tos/subgraph-transform-state/) so that the parent graph knows how to communicate with the subgraphs.
|
||||
* For tool-based subordinate agents, the orchestrator determines the inputs based on the tool schema. Additionally, LangGraph allows passing state to individual tools at runtime, so subordinate agents can access parent state, if needed.
|
||||
|
||||
### Sequence
|
||||
|
||||
LangGraph provides multiple methods to control agent communication sequence:
|
||||
|
||||
* **Explicit control flow (graph edges)**: LangGraph allows you to define the control flow of your application (i.e. the sequence of how agents communicate) explicitly, via [graph edges](./low_level.md#edges).
|
||||
In this architecture, we define agents as nodes and add a supervisor node (LLM) that decides which agent nodes should be called next. We use [conditional edges](./low_level.md#conditional-edges) to route execution to the appropriate agent node based on supervisor's decision. This architecture also lends itself well to running multiple agents in parallel or using [map-reduce](../how-tos/map-reduce.ipynb) pattern.
|
||||
|
||||
```python
|
||||
from typing import Literal
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_core.messages import SystemMessage
|
||||
from langgraph.graph import StateGraph, MessagesState, START, END
|
||||
from langgraph.graph import StateGraph, MessagesState, START
|
||||
|
||||
model = ChatOpenAI(model="gpt-4o-mini")
|
||||
model = ChatOpenAI()
|
||||
|
||||
def research_agent(state: MessagesState):
|
||||
"""Call research agent"""
|
||||
messages = [SystemMessage(content="You are a research assistant. Given a topic, provide key facts and information.")] + state["messages"]
|
||||
response = model.invoke(messages)
|
||||
class AgentState(MessagesState):
|
||||
next: Literal["agent_1", "agent_2"]
|
||||
|
||||
def supervisor(state: AgentState):
|
||||
response = model.invoke(...)
|
||||
return {"next": response["next_agent"]}
|
||||
|
||||
def agent_1(state: AgentState):
|
||||
response = model.invoke(...)
|
||||
return {"messages": [response]}
|
||||
|
||||
def summarize_agent(state: MessagesState):
|
||||
"""Call summarization agent"""
|
||||
messages = [SystemMessage(content="You are a summarization expert. Condense the given information into a brief summary.")] + state["messages"]
|
||||
response = model.invoke(messages)
|
||||
def agent_2(state: AgentState):
|
||||
response = model.invoke(...)
|
||||
return {"messages": [response]}
|
||||
|
||||
graph = StateGraph(MessagesState)
|
||||
graph.add_node("research", research_agent)
|
||||
graph.add_node("summarize", summarize_agent)
|
||||
builder = StateGraph(AgentState)
|
||||
builder.add_node(supervisor)
|
||||
builder.add_node(agent_1)
|
||||
builder.add_node(agent_2)
|
||||
|
||||
# define the flow explicitly
|
||||
graph.add_edge(START, "research")
|
||||
graph.add_edge("research", "summarize")
|
||||
graph.add_edge("summarize", END)
|
||||
builder.add_edge(START, "supervisor")
|
||||
# route to one of the agents or exit based on the supervisor's decisiion
|
||||
builder.add_conditional_edges("supervisor", lambda state: state["next"])
|
||||
builder.add_edge("agent_1", "supervisor")
|
||||
builder.add_edge("agent_2", "supervisor")
|
||||
|
||||
supervisor = builder.compile()
|
||||
```
|
||||
|
||||
* **Dynamic control flow (conditional edges)**: LangGraph also allows you to define [conditional edges](./low_level.md#conditional-edges), where the control flow is dependent on satisfying a given condition. In such cases, you can use an LLM to decide which subordinate agent to call next.
|
||||
Check out this [tutorial](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/) for an example of supervisor multi-agent architecture.
|
||||
|
||||
### Supervisor (tool-calling)
|
||||
|
||||
* **Implicit control flow (tool calling)**: if the orchestrator agent treats subordinate agents as tools, the tool-calling LLM powering the orchestrator will make decisions about the order in which the tools (agents) are being called.
|
||||
In this variant of the [supervisor](#supervisor) architecture, we define individual agents as **tools** and use a tool-calling LLM in the supervisor node. This can be implemented as a [ReAct](./agentic_concepts.md#react-implementation)-style agent with two nodes — an LLM node (supervisor) and a tool-calling node that executes tools (agents in this case).
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from langchain_core.messages import SystemMessage, ToolMessage
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.prebuilt import ToolNode, InjectedState, create_react_agent
|
||||
from langgraph.prebuilt import InjectedState, create_react_agent
|
||||
|
||||
model = ChatOpenAI(model="gpt-4o-mini")
|
||||
model = ChatOpenAI()
|
||||
|
||||
def research_agent(state: Annotated[dict, InjectedState]):
|
||||
"""Call research agent"""
|
||||
messages = [SystemMessage(content="You are a research assistant. Given a topic, provide key facts and information.")] + state["messages"][:-1]
|
||||
response = model.invoke(messages)
|
||||
tool_call = state["messages"][-1].tool_calls[0]
|
||||
return {"messages": [ToolMessage(response.content, tool_call_id=tool_call["id"])]}
|
||||
def agent_1(state: Annotated[dict, InjectedState]):
|
||||
tool_message = ...
|
||||
return {"messages": [tool_message]}
|
||||
|
||||
def summarize_agent(state: Annotated[dict, InjectedState]):
|
||||
"""Call summarization agent"""
|
||||
messages = [SystemMessage(content="You are a summarization expert. Condense the given information into a brief summary.")] + state["messages"][:-1]
|
||||
response = model.invoke(messages)
|
||||
tool_call = state["messages"][-1].tool_calls[0]
|
||||
return {"messages": [ToolMessage(response.content, tool_call_id=tool_call["id"])]}
|
||||
def agent_2(state: Annotated[dict, InjectedState]):
|
||||
tool_message = ...
|
||||
return {"messages": [tool_message]}
|
||||
|
||||
tool_node = ToolNode([research_agent, summarize_agent])
|
||||
graph = create_react_agent(model, [research_agent, summarize_agent], state_modifier="First research and then summarize information on a given topic.")
|
||||
tools = [agent_1, agent_2]
|
||||
supervisor = create_react_agent(model, tools)
|
||||
```
|
||||
|
||||
## Example architectures
|
||||
### Custom multi-agent workflow
|
||||
|
||||
Below are several examples of complex multi-agent architectures that can be implemented in LangGraph.
|
||||
In this architecture we add individual agents as graph nodes and define the order in which agents are called ahead of time, in a custom workflow. In LangGraph the workflow can be defined in two ways:
|
||||
|
||||
### Multi-Agent Collaboration
|
||||
- **Explicit control flow (normal edges)**: LangGraph allows you to explicitly define the control flow of your application (i.e. the sequence of how agents communicate) explicitly, via [normal graph edges](./low_level.md#normal-edges). This is the most deterministic variant of this architecture above — we always know which agent will be called next ahead of time.
|
||||
|
||||
In this example, different agents collaborate on a **shared** scratchpad of messages (i.e. shared graph state). This means that all the work any of them do is visible to the other ones. The benefit is that the other agents can see all the individual steps done. The downside is that sometimes is it overly verbose and unnecessary to pass ALL this information along, and sometimes only the final answer from an agent is needed. We call this **collaboration** because of the shared nature the scratchpad.
|
||||
- **Dynamic control flow (conditional edges)**: in LangGraph you can allow LLMs to decide parts of your application control flow. This can be achieved by using [conditional edges](./low_level.md#conditional-edges). A special case of this is a [supervisor tool-calling](#supervisor-tool-calling) architecture. In that case, the tool-calling LLM powering the supervisor agent will make decisions about the order in which the tools (agents) are being called.
|
||||
|
||||
In this case, the independent agents are actually just a single LLM call with a custom system message.
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langgraph.graph import StateGraph, MessagesState, START
|
||||
|
||||
Here is a visualization of how these agents are connected:
|
||||
model = ChatOpenAI()
|
||||
|
||||

|
||||
def agent_1(state: MessagesState):
|
||||
response = model.invoke(...)
|
||||
return {"messages": [response]}
|
||||
|
||||
See full code example in this [tutorial](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/multi-agent-collaboration/).
|
||||
def agent_2(state: MessagesState):
|
||||
response = model.invoke(...)
|
||||
return {"messages": [response]}
|
||||
|
||||
### Agent Supervisor
|
||||
builder = StateGraph(MessagesState)
|
||||
builder.add_node(agent_1)
|
||||
builder.add_node(agent_2)
|
||||
# define the flow explicitly
|
||||
builder.add_edge(START, "agent_1")
|
||||
builder.add_edge("agent_1", "agent_2")
|
||||
```
|
||||
|
||||
In this example, multiple agents are connected, but compared to above they do NOT share a shared scratchpad. Rather, they have their own independent scratchpads (i.e. their own state), and then their final responses are appended to a global scratchpad.
|
||||
## Communication between agents
|
||||
|
||||
In this case, the independent agents are a LangGraph ReAct agent (graph). This means they have their own individual prompt, LLM, and tools. When called, it's not just a single LLM call, but rather an invocation of the graph powering the ReAct agent.
|
||||
The most important thing when building multi-agent systems is figuring out how the agents communicate. There are few different considerations:
|
||||
|
||||

|
||||
- Do agents communicate via [**via graph state or via tool calls**](#graph-state-vs-tool-calls)?
|
||||
- What if two agents have [**different state schemas**](#different-state-schemas)?
|
||||
- How to communicate over a [**shared message list**](#shared-message-list)?
|
||||
|
||||
See full code example in this [tutorial](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/agent_supervisor/).
|
||||
### Graph state vs tool calls
|
||||
|
||||
### Hierarchical Agent Teams
|
||||
What is the "payload" that is being passed around between agents? In most of the architectures discussed above the agents communicate via the [graph state](./low_level.md#state). In the case of the [supervisor with tool-calling](#supervisor-tool-calling), the payloads are tool call arguments.
|
||||
|
||||
What if the job for a single worker in agent supervisor example becomes too complex? What if the number of workers becomes too large? For some applications, the system may be more effective if work is distributed hierarchically. You can do this by creating additional level of subgraphs and creating a top-level supervisor, along with mid-level supervisors:
|
||||

|
||||
|
||||

|
||||
#### Graph state
|
||||
|
||||
See full code example in this [tutorial](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/).
|
||||
To communicate via graph state, individual agents need to be defined as [graph nodes](./low_level.md#nodes). These can be added as functions or as entire [subgraphs](./low_level.md#subgraphs). At each step of the graph execution, agent node receives the current state of the graph, executes the agent code and then passes the updated state to the next nodes.
|
||||
|
||||
Typically agent nodes share a single [state schema](./low_level.md#schema). However, you might want to design agent nodes with [different state schemas](#different-state-schemas).
|
||||
|
||||
### Different state schemas
|
||||
|
||||
An agent might need to have a different state schema from the rest of the agents. For example, a search agent might only need to keep track of queries and retrieved documents. There are two ways to achieve this in LangGraph:
|
||||
|
||||
- Define [subgraph](./low_level.md#subgraphs) agents with a separate state schema. If there are no shared state keys (channels) between the subgraph and the parent graph, it’s important to [add input / output transformations](https://langchain-ai.github.io/langgraph/how-tos/subgraph-transform-state/) so that the parent graph knows how to communicate with the subgraphs.
|
||||
- Define agent node functions with a [private input state schema](https://langchain-ai.github.io/langgraph/how-tos/pass_private_state/) that is distinct from the overall graph state schema. This allows passing information that is only needed for executing that particular agent.
|
||||
|
||||
### Shared message list
|
||||
|
||||
The most common way for the agents to communicate is via a shared state channel, typically a list of messages. This assumes that there is always at least a single channel (key) in the state that is shared by the agents. When communicating via a shared message list there is an additional consideration: should the agents [share the full history](#share-full-history) of their thought process or only [the final result](#share-final-result)?
|
||||
|
||||

|
||||
|
||||
#### Share full history
|
||||
|
||||
Agents can **share the full history** of their thought process (i.e. "scratchpad") with all other agents. This "scratchpad" would typically look like a [list of messages](./low_level.md#why-use-messages). The benefit of sharing full thought process is that it might help other agents make better decisions and improve reasoning ability for the system as a whole. The downside is that as the number of agents and their complexity grows, the "scratchpad" will grow quickly and might require additional strategies for [memory management](./memory.md/#managing-long-conversation-history).
|
||||
|
||||
#### Share final result
|
||||
|
||||
Agents can have their own private "scratchpad" and only **share the final result** with the rest of the agents. This approach might work better for systems with many agents or agents that are more complex. In this case, you would need to define agents with [different state schemas](#different-state-schemas)
|
||||
|
||||
For agents called as tools, the supervisor determines the inputs based on the tool schema. Additionally, LangGraph allows [passing state](https://langchain-ai.github.io/langgraph/how-tos/pass-run-time-values-to-tools/#pass-graph-state-to-tools) to individual tools at runtime, so subordinate agents can access parent state, if needed.
|
||||
|
||||
@@ -21,6 +21,7 @@ These how-to guides show how to achieve that controllability.
|
||||
LangGraph makes it easy to persist state across graph runs (thread-level persistence) and across threads (cross-thread persistence). These how-to guides show how to add persistence to your graph.
|
||||
|
||||
- [How to add thread-level persistence to your graph](persistence.ipynb)
|
||||
- [How to add thread-level persistence to subgraphs](subgraph-persistence.ipynb)
|
||||
- [How to add cross-thread persistence to your graph](cross-thread-persistence.ipynb)
|
||||
- [How to use Postgres checkpointer for persistence](persistence_postgres.ipynb)
|
||||
- [How to create a custom checkpointer using MongoDB](persistence_mongodb.ipynb)
|
||||
@@ -73,8 +74,8 @@ These guides show how to use different streaming modes.
|
||||
|
||||
## Subgraphs
|
||||
|
||||
- [How to create subgraphs](subgraph.ipynb)
|
||||
- [How to manage state in subgraphs](subgraphs-manage-state.ipynb)
|
||||
- [How to add and use subgraphs](subgraph.ipynb)
|
||||
- [How to view and update state in subgraphs](subgraphs-manage-state.ipynb)
|
||||
- [How to transform inputs and outputs of a subgraph](subgraph-transform-state.ipynb)
|
||||
|
||||
## State Management
|
||||
@@ -103,4 +104,10 @@ Please note that here will we use a **prebuilt agent**. One of the big benefits
|
||||
- [How to add memory to a ReAct agent](create-react-agent-memory.ipynb)
|
||||
- [How to add a custom system prompt to a ReAct agent](create-react-agent-system-prompt.ipynb)
|
||||
- [How to add human-in-the-loop processes to a ReAct agent](create-react-agent-hitl.ipynb)
|
||||
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
|
||||
- [How to create prebuilt ReAct agent from scratch](react-agent-from-scratch.ipynb)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Errors
|
||||
|
||||
- [Error reference](../troubleshooting/errors/index.md)
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -70,12 +70,12 @@
|
||||
"source": [
|
||||
"## Without returning state\n",
|
||||
"\n",
|
||||
"We are going to define a dummy graph in this example that will always hit the recursion limit. First, we will implement it without returning the state and show that it hits the recursion limit. This graph is based on the ReACT architecture, but instead of actually making decisions and taking actions it just loops forever."
|
||||
"We are going to define a dummy graph in this example that will always hit the recursion limit. First, we will implement it without returning the state and show that it hits the recursion limit. This graph is based on the ReAct architecture, but instead of actually making decisions and taking actions it just loops forever."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -116,7 +116,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -145,7 +145,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -171,18 +171,18 @@
|
||||
"source": [
|
||||
"## With returning state\n",
|
||||
"\n",
|
||||
"If we wanted to actually return the state, what we are going to do is introduce a new key to our state called `is_last_step` which keeps track of if we are on the last step of our recursion limit. If so, we will bypass all other graph decisions and simply terminate the graph, returning the state to the user without causing an error.\n",
|
||||
"To avoid hitting the recursion limit, we can introduce a new key to our state called `remaining_steps`. It will keep track of number of steps until reaching the recursion limit. We can then check the value of `remaining_steps` to determine whether we should terminate the graph execution and return the state to the user without causing the `RecursionError`.\n",
|
||||
"\n",
|
||||
"We are going to use a `ManagedValue` channel to do this. A `ManagedValue` channel is a state channel that will exist for the duration of our graph run and no longer. Since our `action` node is going to always induce at least 2 extra steps to our graph (since the `action` node ALWAYS calls the `decision` node afterwards), we will use this channel to check if we are within 2 steps of the limit. See the implementation of `IsLastOrSecondToLastStepManager` below.\n",
|
||||
"To do so, we will use a special `RemainingSteps` annotation. Under the hood, it creates a special `ManagedValue` channel -- a state channel that will exist for the duration of our graph run and no longer.\n",
|
||||
"\n",
|
||||
"This implementation very closely mirrors the implementation of `isLastStep` (which you can use by calling `from langgraph.managed import IsLastStep` and then decorating state keys with the `isLastStep` type), but in this case we check if we are on the last OR second-to-last step, instead of just the last step.\n",
|
||||
"Since our `action` node is going to always induce at least 2 extra steps to our graph (since the `action` node ALWAYS calls the `decision` node afterwards), we will use this channel to check if we are within 2 steps of the limit.\n",
|
||||
"\n",
|
||||
"Now, when we run our graph we should receive no errors and instead get the last value of the state before the recursion limit was hit."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@@ -190,24 +190,18 @@
|
||||
"from langgraph.graph import StateGraph\n",
|
||||
"from typing import Annotated\n",
|
||||
"\n",
|
||||
"from langgraph.managed.base import ManagedValue\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class IsLastOrSecondToLastStepManager(ManagedValue[bool]):\n",
|
||||
" def __call__(self, step: int) -> bool:\n",
|
||||
" limit = self.config.get(\"recursion_limit\", 0)\n",
|
||||
" return step >= limit - 2\n",
|
||||
"from langgraph.managed.is_last_step import RemainingSteps\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" value: str\n",
|
||||
" action_result: str\n",
|
||||
" is_last_step: Annotated[bool, IsLastOrSecondToLastStepManager]\n",
|
||||
" remaining_steps: RemainingSteps\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def router(state: State):\n",
|
||||
" # Force the agent to end if it is on the last step\n",
|
||||
" if state[\"is_last_step\"]:\n",
|
||||
" # Force the agent to end\n",
|
||||
" if state[\"remaining_steps\"] <= 2:\n",
|
||||
" return END\n",
|
||||
" if state[\"value\"] == \"end\":\n",
|
||||
" return END\n",
|
||||
@@ -235,7 +229,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
@@ -244,7 +238,7 @@
|
||||
"{'value': 'keep going!', 'action_result': 'what a great result!'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -277,7 +271,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
"version": "3.11.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "176e8dbb-1a0a-49ce-a10e-2417e8ea17a0",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to add thread-level persistence to subgraphs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8c67581a-49fb-4597-a7fc-6774581c2160",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Prerequisites</p>\n",
|
||||
" <p>\n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
" <ul>\n",
|
||||
" <li> \n",
|
||||
" <a href=\"https://langchain-ai.github.io/langgraph/concepts/low_level/#subgraphs\">\n",
|
||||
" Subgraphs\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
" <li>\n",
|
||||
" <a href=\"https://langchain-ai.github.io/langgraph/concepts/persistence/\">\n",
|
||||
" Persistence\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
" </ul>\n",
|
||||
" </p>\n",
|
||||
"</div>\n",
|
||||
"\n",
|
||||
"This guide shows how you can add [thread-level](https://langchain-ai.github.io/langgraph/how-tos/persistence/) persistence to graphs that use [subgraphs](https://langchain-ai.github.io/langgraph/how-tos/subgraph/)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "8f83b855-ab23-4de7-9559-702cad9a29c6",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's install the required packages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "77d1eafa-3252-45f6-9af0-d94e1f9c5c9e",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"%%capture --no-stderr\n",
|
||||
"%pip install -U langgraph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "2e60c6cd-bf4e-46af-9761-b872d0fbe3b6",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Set up <a href=\"https://smith.langchain.com\">LangSmith</a> for LangGraph development</p>\n",
|
||||
" <p style=\"padding-top: 5px;\">\n",
|
||||
" Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started <a href=\"https://docs.smith.langchain.com\">here</a>. \n",
|
||||
" </p>\n",
|
||||
"</div>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "871b9056-fec7-4683-8c22-f56c91f5b13b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define the graph with persistence"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"id": "9f1303ef-df37-48e0-8a59-8ff169c52c5b",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"To add persistence to a graph with subgraphs, all you need to do is pass a [checkpointer](https://langchain-ai.github.io/langgraph/reference/checkpoints/#langgraph.checkpoint.base.BaseCheckpointSaver) when **compiling the parent graph**. LangGraph will automatically propagate the checkpointer to the child subgraphs."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c74cde2e-c127-4326-8d36-b6acef987f0a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"!!! note\n",
|
||||
" You **shouldn't provide** a checkpointer when compiling a subgraph. Instead, you must define a **single** checkpointer that you pass to `parent_graph.compile()`, and LangGraph will automatically propagate the checkpointer to the child subgraphs. If you pass the checkpointer to the `subgraph.compile()`, it will simply be ignored. This also applies when you [add a node function that invokes the subgraph](../subgraph#add-a-node-function-that-invokes-the-subgraph)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "c3a1fe22-1ca9-45eb-a35b-71b9c905e8c5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's define a simple graph with a single subgraph node to show how to do this."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "0d76f0c0-bd77-4eca-9527-27bcdf85dd42",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"<langgraph.graph.state.StateGraph at 0x106d2fa10>"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from langgraph.graph import START, StateGraph\n",
|
||||
"from langgraph.checkpoint.memory import MemorySaver\n",
|
||||
"from typing import TypedDict\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# subgraph\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class SubgraphState(TypedDict):\n",
|
||||
" foo: str # note that this key is shared with the parent graph state\n",
|
||||
" bar: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def subgraph_node_1(state: SubgraphState):\n",
|
||||
" return {\"bar\": \"bar\"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def subgraph_node_2(state: SubgraphState):\n",
|
||||
" # note that this node is using a state key ('bar') that is only available in the subgraph\n",
|
||||
" # and is sending update on the shared state key ('foo')\n",
|
||||
" return {\"foo\": state[\"foo\"] + state[\"bar\"]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"subgraph_builder = StateGraph(SubgraphState)\n",
|
||||
"subgraph_builder.add_node(subgraph_node_1)\n",
|
||||
"subgraph_builder.add_node(subgraph_node_2)\n",
|
||||
"subgraph_builder.add_edge(START, \"subgraph_node_1\")\n",
|
||||
"subgraph_builder.add_edge(\"subgraph_node_1\", \"subgraph_node_2\")\n",
|
||||
"subgraph = subgraph_builder.compile()\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# parent graph\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"class State(TypedDict):\n",
|
||||
" foo: str\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def node_1(state: State):\n",
|
||||
" return {\"foo\": \"hi! \" + state[\"foo\"]}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"builder = StateGraph(State)\n",
|
||||
"builder.add_node(\"node_1\", node_1)\n",
|
||||
"# note that we're adding the compiled subgraph as a node to the parent graph\n",
|
||||
"builder.add_node(\"node_2\", subgraph)\n",
|
||||
"builder.add_edge(START, \"node_1\")\n",
|
||||
"builder.add_edge(\"node_1\", \"node_2\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "47084b1f-9fd5-40a9-9d75-89eb5f853d02",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can now compile the graph with an in-memory checkpointer (`MemorySaver`)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "7657d285-c896-40c9-a569-b4a3b9c230c7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"checkpointer = MemorySaver()\n",
|
||||
"# You must only pass checkpointer when compiling the parent graph.\n",
|
||||
"# LangGraph will automatically propagate the checkpointer to the child subgraphs.\n",
|
||||
"graph = builder.compile(checkpointer=checkpointer)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0d193e3c-4ec3-4034-beed-8e5550c6542c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Verify persistence works"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "eb69a5f0-b92e-4d4e-9aa9-c4c4ec7de91a",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Let's now run the graph and inspect the persisted state for both the parent graph and the subgraph to verify that persistence works. We should expect to see the final execution results for both the parent and subgraph in `state.values`."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "13da686e-6ed6-4b83-93e8-1631fcc8c2a9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"config = {\"configurable\": {\"thread_id\": \"1\"}}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "8721f045-2e82-4bf0-9d85-5ba6ecf899d6",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'node_1': {'foo': 'hi! foo'}}\n",
|
||||
"{'subgraph_node_1': {'bar': 'bar'}}\n",
|
||||
"{'subgraph_node_2': {'foo': 'hi! foobar'}}\n",
|
||||
"{'node_2': {'foo': 'hi! foobar'}}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for _, chunk in graph.stream({\"foo\": \"foo\"}, config, subgraphs=True):\n",
|
||||
" print(chunk)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "ec6b5ce4-becc-4910-8a6d-d6b60d9d6f60",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"We can now view the parent graph state by calling `graph.get_state()` with the same config that we used to invoke the graph."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "3e817283-142d-4fda-8cb1-8de34717f833",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'foo': 'hi! foobar'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"graph.get_state(config).values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "fbc4f30b-941e-4140-8bfa-3b8cc670489c",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"To view the subgraph state, we need to do two things:\n",
|
||||
"\n",
|
||||
"1. Find the most recent config value for the subgraph\n",
|
||||
"2. Use `graph.get_state()` to retrieve that value for the most recent subgraph config.\n",
|
||||
"\n",
|
||||
"To find the correct config, we can examine the state history from the parent graph and find the state snapshot before we return results from `node_2` (the node with subgraph):"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "e896628f-36b2-45eb-b7c5-c64c1098f328",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"state_with_subgraph = [\n",
|
||||
" s for s in graph.get_state_history(config) if s.next == (\"node_2\",)\n",
|
||||
"][0]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "7af49977-42b1-40a1-88f1-f07437f8b7f9",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The state snapshot will include the list of `tasks` to be executed next. When using subgraphs, the `tasks` will contain the config that we can use to retrieve the subgraph state:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "21e96df3-946d-40f8-8d6d-055ae4177452",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'configurable': {'thread_id': '1',\n",
|
||||
" 'checkpoint_ns': 'node_2:6ef111a6-f290-7376-0dfc-a4152307bc5b'}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"subgraph_config = state_with_subgraph.tasks[0].state\n",
|
||||
"subgraph_config"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "1d2401b3-d52b-4895-a5d1-dccf015ba216",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'foo': 'hi! foobar', 'bar': 'bar'}"
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"graph.get_state(subgraph_config).values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "40aded92-99dd-427b-932d-aa78f474c271",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"If you want to learn more about how to modify the subgraph state for human-in-the-loop workflows, check out this [how-to guide](https://langchain-ai.github.io/langgraph/how-tos/subgraphs-manage-state/)."
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -5,15 +5,48 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# How to manage state in subgraphs\n",
|
||||
"# How to view and update state in subgraphs\n",
|
||||
"\n",
|
||||
"For more complex systems, sub-graphs are a useful design principle. Sub-graphs allow you to create and manage different states in different parts of your graph. This allows you build things like [multi-agent teams](https://langchain-ai.github.io/langgraph/tutorials/multi_agent/hierarchical_agent_teams/), where each team can track its own separate state.\n",
|
||||
"<div class=\"admonition tip\">\n",
|
||||
" <p class=\"admonition-title\">Prerequisites</p>\n",
|
||||
" <p>\n",
|
||||
" This guide assumes familiarity with the following:\n",
|
||||
" <ul>\n",
|
||||
" <li> \n",
|
||||
" <a href=\"https://langchain-ai.github.io/langgraph/concepts/low_level/#subgraphs\">\n",
|
||||
" Subgraphs\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
" <li> \n",
|
||||
" <a href=\"https://langchain-ai.github.io/langgraph/concepts/human_in_the_loop/\">\n",
|
||||
" Human-in-the-loop\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
" <li> \n",
|
||||
" <a href=\"https://langchain-ai.github.io/langgraph/concepts/low_level/#state\">\n",
|
||||
" State\n",
|
||||
" </a>\n",
|
||||
" </li>\n",
|
||||
" </ul>\n",
|
||||
" </p>\n",
|
||||
"</div> \n",
|
||||
"\n",
|
||||
"In this how-to guide we will cover how to manage the persisted state in subgraphs. This will enable a lot of the human-in-the-loop interaction patterns.\n",
|
||||
"Once you add [persistence](../subgraph-persistence), you can easily view and update the state of the subgraph at any point in time. This enables a lot of the human-in-the-loop interaction patterns:\n",
|
||||
"\n",
|
||||
"* You can surface a state during an interrupt to a user to let them accept an action.\n",
|
||||
"* You can rewind the subgraph to reproduce or avoid issues.\n",
|
||||
"* You can modify the state to let the user better control its actions.\n",
|
||||
"\n",
|
||||
"This guide shows how you can do this."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First we need to install the packages required"
|
||||
"First, let's install the required packages"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -68,7 +101,7 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define SubGraph\n",
|
||||
"## Define subgraph\n",
|
||||
"\n",
|
||||
"First, let's set up our subgraph. For this, we will create a simple graph that can get the weather for a specific city. We will compile this graph with a [breakpoint](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/) before the `weather_node`:"
|
||||
]
|
||||
@@ -121,7 +154,7 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Define Parent Graph\n",
|
||||
"## Define parent graph\n",
|
||||
"\n",
|
||||
"We can now setup the overall graph. This graph will first route to the subgraph if it needs to get the weather, otherwise it will route to a normal LLM."
|
||||
]
|
||||
@@ -444,7 +477,7 @@
|
||||
" if h.next == (\"model_node\",)\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# This pattern can be extended no matter how many levels deep - image model node was another subgraph in this case\n",
|
||||
"# This pattern can be extended no matter how many levels deep\n",
|
||||
"# subsubgraph_stat_history = next(h for h in graph.get_state_history(subgraph_state_before_model_node.tasks[0].state) if h.next == ('my_subsubgraph_node',))"
|
||||
]
|
||||
},
|
||||
@@ -660,7 +693,9 @@
|
||||
" print(update)\n",
|
||||
"# Graph execution should stop before the weather node\n",
|
||||
"print(\"interrupted!\")\n",
|
||||
"\n",
|
||||
"state = graph.get_state(config, subgraphs=True)\n",
|
||||
"\n",
|
||||
"# We update the state by passing in the message we want returned from the weather node, and make sure to use as_node\n",
|
||||
"graph.update_state(\n",
|
||||
" state.tasks[0].state.config,\n",
|
||||
@@ -669,6 +704,7 @@
|
||||
")\n",
|
||||
"for update in graph.stream(None, config=config, stream_mode=\"updates\", subgraphs=True):\n",
|
||||
" print(update)\n",
|
||||
"\n",
|
||||
"print(graph.get_state(config).values[\"messages\"])"
|
||||
]
|
||||
},
|
||||
@@ -708,6 +744,7 @@
|
||||
" print(update)\n",
|
||||
"# Graph execution should stop before the weather node\n",
|
||||
"print(\"interrupted!\")\n",
|
||||
"\n",
|
||||
"# We update the state by passing in the message we want returned from the weather graph, making sure to use as_node\n",
|
||||
"# Note that we don't need to pass in the subgraph config, since we aren't updating the state inside the subgraph\n",
|
||||
"graph.update_state(\n",
|
||||
@@ -717,6 +754,7 @@
|
||||
")\n",
|
||||
"for update in graph.stream(None, config=config, stream_mode=\"updates\"):\n",
|
||||
" print(update)\n",
|
||||
"\n",
|
||||
"print(graph.get_state(config).values[\"messages\"])"
|
||||
]
|
||||
},
|
||||
@@ -947,6 +985,7 @@
|
||||
" None, config=config, stream_mode=\"updates\", subgraphs=True\n",
|
||||
"):\n",
|
||||
" print(update)\n",
|
||||
"\n",
|
||||
"print(grandparent_graph.get_state(config).values[\"messages\"])"
|
||||
]
|
||||
},
|
||||
@@ -1002,7 +1041,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# GRAPH_RECURSION_LIMIT
|
||||
|
||||
Your LangGraph [`StateGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.state.StateGraph) reached the maximum number of steps before hitting a stop condition.
|
||||
This is often due to an infinite loop caused by code like the example below:
|
||||
|
||||
```python
|
||||
class State(TypedDict):
|
||||
some_key: str
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node("a", ...)
|
||||
builder.add_node("b", ...)
|
||||
builder.add_edge("a", "b")
|
||||
builder.add_edge("b", "a")
|
||||
...
|
||||
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
However, complex graphs may hit the default limit naturally.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If you are not expecting your graph to go through many iterations, you likely have a cycle. Check your logic for infinite loops.
|
||||
- If you have a complex graph, you can pass in a higher `recursion_limit` value into your `config` object when invoking your graph like this:
|
||||
|
||||
```python
|
||||
graph.invoke({...}, {"recursion_limit": 100})
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
# INVALID_CONCURRENT_GRAPH_UPDATE
|
||||
|
||||
A LangGraph [`StateGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.state.StateGraph) received concurrent updates to its state from multiple nodes to a state property that doesn't
|
||||
support it.
|
||||
|
||||
One way this can occur is if you are using a [fanout](https://langchain-ai.github.io/langgraph/how-tos/map-reduce/)
|
||||
or other parallel execution in your graph and you have defined a graph like this:
|
||||
|
||||
```python
|
||||
class State(TypedDict):
|
||||
some_key: str
|
||||
|
||||
def node(state: State):
|
||||
return {"some_key": "some_string_value"}
|
||||
|
||||
def other_node(state: State):
|
||||
return {"some_key": "some_string_value"}
|
||||
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node(node)
|
||||
builder.add_node(other_node)
|
||||
builder.add_edge(START, "node")
|
||||
builder.add_edge(START, "other_node")
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
If a node in the above graph returns `{ "some_key": "some_string_value" }`, this will overwrite the state value for `"some_key"` with `"some_string_value"`.
|
||||
However, if multiple nodes in e.g. a fanout within a single step return values for `"some_key"`, the graph will throw this error because
|
||||
there is uncertainty around how to update the internal state.
|
||||
|
||||
To get around this, you can define a reducer that combines multiple values:
|
||||
|
||||
```python
|
||||
import operator
|
||||
from typing import Annotated
|
||||
|
||||
class State(TypedDict):
|
||||
# The operator.add reducer fn makes this append-only
|
||||
some_key: Annotated[list, operator.add]
|
||||
```
|
||||
|
||||
This will allow you to define logic that handles the same key returned from multiple nodes executed in parallel.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
The following may help resolve this error:
|
||||
|
||||
- If your graph executes nodes in parallel, make sure you have defined relevant state keys with a reducer.
|
||||
@@ -0,0 +1,38 @@
|
||||
# INVALID_GRAPH_NODE_RETURN_VALUE
|
||||
|
||||
A LangGraph [`StateGraph`](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.state.StateGraph)
|
||||
received a non-dict return type from a node. Here's an example:
|
||||
|
||||
```python
|
||||
class State(TypedDict):
|
||||
some_key: str
|
||||
|
||||
def bad_node(state: State):
|
||||
# Should return an dict with a value for "some_key", not a list
|
||||
return ["whoops"]
|
||||
|
||||
builder = StateGraph(State)
|
||||
builder.add_node(bad_node)
|
||||
...
|
||||
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
Invoking the above graph will result in an error like this:
|
||||
|
||||
```python
|
||||
graph.invoke({ "some_key": "someval" });
|
||||
```
|
||||
|
||||
```
|
||||
InvalidUpdateError: Expected dict, got ['whoops']
|
||||
For troubleshooting, visit: https://python.langchain.com/docs/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE
|
||||
```
|
||||
|
||||
Nodes in your graph must return an dict containing one or more keys defined in your state.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
The following may help resolve this error:
|
||||
|
||||
- If you have complex logic in your node, make sure all code paths return an appropriate dict for your defined state.
|
||||
@@ -0,0 +1,12 @@
|
||||
# MULTIPLE_SUBGRAPHS
|
||||
|
||||
You are calling the same subgraph multiple times within a single LangGraph node with checkpointing enabled for each subgraph.
|
||||
|
||||
This is currently not allowed due to internal restrictions on how checkpoint namespacing for subgraphs works.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
The following may help resolve this error:
|
||||
|
||||
- If you don't need to interrupt/resume from a subgraph, pass `checkpointer=False` when compiling it like this: `.compile(checkpointer=False)`
|
||||
- Don't imperatively call graphs multiple times in the same node, and instead use the [`Send`](https://langchain-ai.github.io/langgraph/concepts/low_level/#send) API.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Error reference
|
||||
|
||||
This page contains guides around resolving common errors you may find while building with LangChain.
|
||||
Errors referenced below will have an `lc_error_code` property corresponding to one of the below codes when they are thrown in code.
|
||||
|
||||
- [GRAPH_RECURSION_LIMIT](./GRAPH_RECURSION_LIMIT.md)
|
||||
- [INVALID_CONCURRENT_GRAPH_UPDATE](./INVALID_CONCURRENT_GRAPH_UPDATE.md)
|
||||
- [INVALID_GRAPH_NODE_RETURN_VALUE](./INVALID_GRAPH_NODE_RETURN_VALUE.md)
|
||||
- [MULTIPLE_SUBGRAPHS](./MULTIPLE_SUBGRAPHS.md)
|
||||
@@ -25,8 +25,8 @@ Learn from example implementations of graphs designed for specific scenarios and
|
||||
|
||||
#### Multi-Agent Systems
|
||||
|
||||
- [Collaboration](multi_agent/multi-agent-collaboration.ipynb): Enable two agents to collaborate on a task
|
||||
- [Supervision](multi_agent/agent_supervisor.ipynb): Use an LLM to orchestrate and delegate to individual agents
|
||||
- [Network](multi_agent/multi-agent-collaboration.ipynb): Enable two or more agents to collaborate on a task
|
||||
- [Supervisor](multi_agent/agent_supervisor.ipynb): Use an LLM to orchestrate and delegate to individual agents
|
||||
- [Hierarchical Teams](multi_agent/hierarchical_agent_teams.ipynb): Orchestrate nested teams of agents to solve problems
|
||||
|
||||
#### RAG
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
"id": "a3e3ebc4-57af-4fe4-bdd3-36aff67bf276",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Agent Supervisor\n",
|
||||
"# Multi-agent supervisor\n",
|
||||
"\n",
|
||||
"The [previous example](../multi-agent-collaboration) routed messages automatically based on the output of the initial researcher agent.\n",
|
||||
"\n",
|
||||
"We can also choose to use an LLM to orchestrate the different agents.\n",
|
||||
"We can also choose to use an [LLM to orchestrate](https://langchain-ai.github.io/langgraph/concepts/multi_agent/#supervisor) the different agents.\n",
|
||||
"\n",
|
||||
"Below, we will create an agent group, with an agent supervisor to help delegate tasks.\n",
|
||||
"\n",
|
||||
@@ -376,7 +376,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"source": [
|
||||
"# Hierarchical Agent Teams\n",
|
||||
"\n",
|
||||
"In our previous example ([Agent Supervisor](../agent_supervisor)), we introduced the concept of a single supervisor node to route work between different worker nodes.\n",
|
||||
"In our previous example ([Agent Supervisor](../agent_supervisor)), we introduced the concept of a single [supervisor node](https://langchain-ai.github.io/langgraph/concepts/multi_agent/#supervisor) to route work between different worker nodes.\n",
|
||||
"\n",
|
||||
"But what if the job for a single worker becomes too complex? What if the number of workers becomes too large?\n",
|
||||
"\n",
|
||||
@@ -1117,7 +1117,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
"id": "39fd1948-b5c3-48c4-b10e-2ae7e8c83334",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Basic Multi-agent Collaboration\n",
|
||||
"# Multi-agent network\n",
|
||||
"\n",
|
||||
"A single agent can usually operate effectively using a handful of tools within a single domain, but even using powerful models like `gpt-4`, it can be less effective at using many tools. \n",
|
||||
"\n",
|
||||
"One way to approach complicated tasks is through a \"divide-and-conquer\" approach: create an specialized agent for each task or domain and route tasks to the correct \"expert\".\n",
|
||||
"One way to approach complicated tasks is through a \"divide-and-conquer\" approach: create an specialized agent for each task or domain and route tasks to the correct \"expert\". This is an example of a [multi-agent network](https://langchain-ai.github.io/langgraph/concepts/multi_agent/#network) architecture.\n",
|
||||
"\n",
|
||||
"This notebook (inspired by the paper [AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation](https://arxiv.org/abs/2308.08155), by Wu, et. al.) shows one way to do this using LangGraph.\n",
|
||||
"\n",
|
||||
@@ -535,7 +535,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.9"
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -97,8 +97,8 @@ nav:
|
||||
- SQL Agent: tutorials/sql-agent.ipynb
|
||||
- Agent Architectures:
|
||||
- Multi-Agent Systems:
|
||||
- Collaboration: tutorials/multi_agent/multi-agent-collaboration.ipynb
|
||||
- Supervision: tutorials/multi_agent/agent_supervisor.ipynb
|
||||
- Network: tutorials/multi_agent/multi-agent-collaboration.ipynb
|
||||
- Supervisor: tutorials/multi_agent/agent_supervisor.ipynb
|
||||
- Hierarchical Teams: tutorials/multi_agent/hierarchical_agent_teams.ipynb
|
||||
- Planning Agents:
|
||||
- Plan-and-Execute: tutorials/plan-and-execute/plan-and-execute.ipynb
|
||||
@@ -128,6 +128,7 @@ nav:
|
||||
- Control graph recursion limit: how-tos/recursion-limit.ipynb
|
||||
- Persistence:
|
||||
- Add thread-level persistence: how-tos/persistence.ipynb
|
||||
- Add thread-level persistence to subgraphs: how-tos/subgraph-persistence.ipynb
|
||||
- Add cross-thread persistence: how-tos/cross-thread-persistence.ipynb
|
||||
- Use Postgres checkpointer for persistence: how-tos/persistence_postgres.ipynb
|
||||
- Create custom checkpointer using MongoDB: how-tos/persistence_mongodb.ipynb
|
||||
@@ -162,8 +163,8 @@ nav:
|
||||
- Pass config to tools: how-tos/pass-config-to-tools.ipynb
|
||||
- Handle many tools: how-tos/many-tools.ipynb
|
||||
- Subgraphs:
|
||||
- Create subgraphs: how-tos/subgraph.ipynb
|
||||
- Manage state in subgraphs: how-tos/subgraphs-manage-state.ipynb
|
||||
- Add and use subgraphs: how-tos/subgraph.ipynb
|
||||
- View and update state in subgraphs: how-tos/subgraphs-manage-state.ipynb
|
||||
- Transform inputs and outputs of a subgraph: how-tos/subgraph-transform-state.ipynb
|
||||
- State Management:
|
||||
- Use Pydantic model as state: how-tos/state-model.ipynb
|
||||
@@ -177,6 +178,12 @@ nav:
|
||||
- Return structured output from a ReAct agent: how-tos/react-agent-structured-output.ipynb
|
||||
- Pass custom LangSmith run ID for graph runs: how-tos/run-id-langsmith.ipynb
|
||||
- Return state before hitting recursion limit: how-tos/return-when-recursion-limit-hits.ipynb
|
||||
- Error reference:
|
||||
- "troubleshooting/errors/index.md"
|
||||
- GRAPH_RECURSION_LIMIT: "troubleshooting/errors/GRAPH_RECURSION_LIMIT.md"
|
||||
- INVALID_CONCURRENT_GRAPH_UPDATE: "troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md"
|
||||
- INVALID_GRAPH_NODE_RETURN_VALUE: "troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md"
|
||||
- MULTIPLE_SUBGRAPHS: "troubleshooting/errors/MULTIPLE_SUBGRAPHS.md"
|
||||
- Prebuilt ReAct Agent:
|
||||
- Create a ReAct agent: how-tos/create-react-agent.ipynb
|
||||
- Add memory to a ReAct agent: how-tos/create-react-agent-memory.ipynb
|
||||
@@ -206,7 +213,7 @@ nav:
|
||||
- "cloud/index.md"
|
||||
- Tutorials:
|
||||
- Quick Start: "cloud/quick_start.md"
|
||||
- How-to Guides:
|
||||
- How-to Guides:
|
||||
- "cloud/how-tos/index.md"
|
||||
- Setup:
|
||||
- Setup App: "cloud/deployment/setup.md"
|
||||
@@ -229,7 +236,7 @@ nav:
|
||||
- Rollback: "cloud/how-tos/rollback_concurrent.md"
|
||||
- Reject: "cloud/how-tos/reject_concurrent.md"
|
||||
- Enqueue: "cloud/how-tos/enqueue_concurrent.md"
|
||||
- Human-in-the-Loop:
|
||||
- Human-in-the-Loop:
|
||||
- Add Breakpoint: "cloud/how-tos/human_in_the_loop_breakpoint.md"
|
||||
- Wait for User Input: "cloud/how-tos/human_in_the_loop_user_input.md"
|
||||
- Edit Graph State: "cloud/how-tos/human_in_the_loop_edit_state.md"
|
||||
@@ -249,8 +256,8 @@ nav:
|
||||
- Configure Agents: "cloud/how-tos/configuration_cloud.md"
|
||||
- Versioning Assistants: "cloud/how-tos/assistant_versioning.md"
|
||||
- Convert LangGraph calls to LangGraph Cloud calls: "cloud/how-tos/langgraph_to_langgraph_cloud.ipynb"
|
||||
- Integrate Webhooks: 'cloud/how-tos/webhooks.md'
|
||||
- Copy Threads: 'cloud/how-tos/copy_threads.md'
|
||||
- Integrate Webhooks: "cloud/how-tos/webhooks.md"
|
||||
- Copy Threads: "cloud/how-tos/copy_threads.md"
|
||||
- Check Status of Threads: "cloud/how-tos/check_thread_status.md"
|
||||
- Conceptual Guides:
|
||||
- API Concepts: "cloud/concepts/api.md"
|
||||
@@ -351,4 +358,4 @@ validation:
|
||||
# it's only an issue for tutorials/storm/storm.ipynb
|
||||
# because it creates anchors in the generated report
|
||||
# and those anchors are not available in the actual doc
|
||||
anchors: info
|
||||
anchors: info
|
||||
|
||||
@@ -3,7 +3,12 @@ from typing import Generic, Optional, Sequence, Type
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
from langgraph.errors import (
|
||||
EmptyChannelError,
|
||||
ErrorCode,
|
||||
InvalidUpdateError,
|
||||
create_error_message,
|
||||
)
|
||||
|
||||
|
||||
class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
@@ -35,9 +40,11 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
if len(values) == 0:
|
||||
return False
|
||||
if len(values) != 1:
|
||||
raise InvalidUpdateError(
|
||||
f"At key '{self.key}': Can receive only one value per step. Use an Annotated key to handle multiple values."
|
||||
msg = create_error_message(
|
||||
message=f"At key '{self.key}': Can receive only one value per step. Use an Annotated key to handle multiple values.",
|
||||
error_code=ErrorCode.INVALID_CONCURRENT_GRAPH_UPDATE,
|
||||
)
|
||||
raise InvalidUpdateError(msg)
|
||||
|
||||
self.value = values[-1]
|
||||
return True
|
||||
|
||||
@@ -12,6 +12,8 @@ EMPTY_MAP: Mapping[str, Any] = MappingProxyType({})
|
||||
EMPTY_SEQ: tuple[str, ...] = tuple()
|
||||
|
||||
# --- Public constants ---
|
||||
TAG_NOSTREAM = sys.intern("langsmith:nostream")
|
||||
"""Tag to disable streaming for a chat model."""
|
||||
TAG_HIDDEN = sys.intern("langsmith:hidden")
|
||||
"""Tag to hide a node/edge from certain tracing/streaming environments."""
|
||||
START = sys.intern("__start__")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Sequence
|
||||
|
||||
from langgraph.checkpoint.base import EmptyChannelError # noqa: F401
|
||||
@@ -6,12 +7,31 @@ from langgraph.types import Interrupt
|
||||
# EmptyChannelError re-exported for backwards compatibility
|
||||
|
||||
|
||||
class ErrorCode(Enum):
|
||||
GRAPH_RECURSION_LIMIT = "GRAPH_RECURSION_LIMIT"
|
||||
INVALID_CONCURRENT_GRAPH_UPDATE = "INVALID_CONCURRENT_GRAPH_UPDATE"
|
||||
INVALID_GRAPH_NODE_RETURN_VALUE = "INVALID_GRAPH_NODE_RETURN_VALUE"
|
||||
MULTIPLE_SUBGRAPHS = "MULTIPLE_SUBGRAPHS"
|
||||
|
||||
|
||||
def create_error_message(*, message: str, error_code: ErrorCode) -> str:
|
||||
return (
|
||||
f"{message}\n"
|
||||
"For troubleshooting, visit: https://python.langchain.com/docs/"
|
||||
f"troubleshooting/errors/{error_code.value}"
|
||||
)
|
||||
|
||||
|
||||
class GraphRecursionError(RecursionError):
|
||||
"""Raised when the graph has exhausted the maximum number of steps.
|
||||
|
||||
This prevents infinite loops. To increase the maximum number of steps,
|
||||
run your graph with a config specifying a higher `recursion_limit`.
|
||||
|
||||
Troubleshooting Guides:
|
||||
|
||||
- [GRAPH_RECURSION_LIMIT](https://python.langchain.com/docs/troubleshooting/errors/GRAPH_RECURSION_LIMIT)
|
||||
|
||||
Examples:
|
||||
|
||||
graph = builder.compile()
|
||||
@@ -26,7 +46,13 @@ class GraphRecursionError(RecursionError):
|
||||
|
||||
|
||||
class InvalidUpdateError(Exception):
|
||||
"""Raised when attempting to update a channel with an invalid set of updates."""
|
||||
"""Raised when attempting to update a channel with an invalid set of updates.
|
||||
|
||||
Troubleshooting Guides:
|
||||
|
||||
- [INVALID_CONCURRENT_GRAPH_UPDATE](https://python.langchain.com/docs/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE)
|
||||
- [INVALID_GRAPH_NODE_RETURN_VALUE](https://python.langchain.com/docs/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE)
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@@ -72,7 +98,12 @@ class CheckpointNotLatest(Exception):
|
||||
|
||||
|
||||
class MultipleSubgraphsError(Exception):
|
||||
"""Raised when multiple subgraphs are called inside the same node."""
|
||||
"""Raised when multiple subgraphs are called inside the same node.
|
||||
|
||||
Troubleshooting guides:
|
||||
|
||||
- [MULTIPLE_SUBGRAPHS](https://python.langchain.com/docs/troubleshooting/errors/MULTIPLE_SUBGRAPHS)
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ from langgraph.channels.ephemeral_value import EphemeralValue
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.named_barrier_value import NamedBarrierValue
|
||||
from langgraph.constants import NS_END, NS_SEP, TAG_HIDDEN
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.errors import ErrorCode, InvalidUpdateError, create_error_message
|
||||
from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph, Send
|
||||
from langgraph.managed.base import (
|
||||
ChannelKeyPlaceholder,
|
||||
@@ -538,7 +538,11 @@ class CompiledStateGraph(CompiledGraph):
|
||||
value = getattr(input, key, SKIP_WRITE)
|
||||
return value if value is not None else SKIP_WRITE
|
||||
else:
|
||||
raise InvalidUpdateError(f"Expected dict, got {input}")
|
||||
msg = create_error_message(
|
||||
message=f"Expected dict, got {input}",
|
||||
error_code=ErrorCode.INVALID_GRAPH_NODE_RETURN_VALUE,
|
||||
)
|
||||
raise InvalidUpdateError(msg)
|
||||
|
||||
# state updaters
|
||||
write_entries = (
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from langgraph.managed.is_last_step import IsLastStep
|
||||
from langgraph.managed.is_last_step import IsLastStep, RemainingSteps
|
||||
|
||||
__all__ = ["IsLastStep"]
|
||||
__all__ = ["IsLastStep", "RemainingSteps"]
|
||||
|
||||
@@ -13,22 +13,23 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from typing_extensions import Self, TypeGuard
|
||||
|
||||
from langgraph.types import LoopProtocol
|
||||
|
||||
V = TypeVar("V")
|
||||
U = TypeVar("U")
|
||||
|
||||
|
||||
class ManagedValue(ABC, Generic[V]):
|
||||
def __init__(self, config: RunnableConfig) -> None:
|
||||
self.config = config
|
||||
def __init__(self, loop: LoopProtocol) -> None:
|
||||
self.loop = loop
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]:
|
||||
def enter(cls, loop: LoopProtocol, **kwargs: Any) -> Iterator[Self]:
|
||||
try:
|
||||
value = cls(config, **kwargs)
|
||||
value = cls(loop, **kwargs)
|
||||
yield value
|
||||
finally:
|
||||
# because managed value and Pregel have reference to each other
|
||||
@@ -40,9 +41,9 @@ class ManagedValue(ABC, Generic[V]):
|
||||
|
||||
@classmethod
|
||||
@asynccontextmanager
|
||||
async def aenter(cls, config: RunnableConfig, **kwargs: Any) -> AsyncIterator[Self]:
|
||||
async def aenter(cls, loop: LoopProtocol, **kwargs: Any) -> AsyncIterator[Self]:
|
||||
try:
|
||||
value = cls(config, **kwargs)
|
||||
value = cls(loop, **kwargs)
|
||||
yield value
|
||||
finally:
|
||||
# because managed value and Pregel have reference to each other
|
||||
@@ -53,7 +54,7 @@ class ManagedValue(ABC, Generic[V]):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __call__(self, step: int) -> V: ...
|
||||
def __call__(self) -> V: ...
|
||||
|
||||
|
||||
class WritableManagedValue(Generic[V, U], ManagedValue[V], ABC):
|
||||
|
||||
@@ -13,10 +13,10 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.managed.base import ConfiguredManagedValue, ManagedValue, V
|
||||
from langgraph.types import LoopProtocol
|
||||
|
||||
|
||||
class Context(ManagedValue[V], Generic[V]):
|
||||
@@ -46,14 +46,14 @@ class Context(ManagedValue[V], Generic[V]):
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]:
|
||||
with super().enter(config, **kwargs) as self:
|
||||
def enter(cls, loop: LoopProtocol, **kwargs: Any) -> Iterator[Self]:
|
||||
with super().enter(loop, **kwargs) as self:
|
||||
if self.ctx is None:
|
||||
raise ValueError(
|
||||
"Synchronous context manager not found. Please initialize Context value with a sync context manager, or invoke your graph asynchronously."
|
||||
)
|
||||
ctx = (
|
||||
self.ctx(config) # type: ignore[call-arg]
|
||||
self.ctx(loop.config) # type: ignore[call-arg]
|
||||
if signature(self.ctx).parameters.get("config")
|
||||
else self.ctx()
|
||||
)
|
||||
@@ -63,17 +63,17 @@ class Context(ManagedValue[V], Generic[V]):
|
||||
|
||||
@classmethod
|
||||
@asynccontextmanager
|
||||
async def aenter(cls, config: RunnableConfig, **kwargs: Any) -> AsyncIterator[Self]:
|
||||
async with super().aenter(config, **kwargs) as self:
|
||||
async def aenter(cls, loop: LoopProtocol, **kwargs: Any) -> AsyncIterator[Self]:
|
||||
async with super().aenter(loop, **kwargs) as self:
|
||||
if self.actx is not None:
|
||||
ctx = (
|
||||
self.actx(config) # type: ignore[call-arg]
|
||||
self.actx(loop.config) # type: ignore[call-arg]
|
||||
if signature(self.actx).parameters.get("config")
|
||||
else self.actx()
|
||||
)
|
||||
elif self.ctx is not None:
|
||||
ctx = (
|
||||
self.ctx(config) # type: ignore
|
||||
self.ctx(loop.config) # type: ignore
|
||||
if signature(self.ctx).parameters.get("config")
|
||||
else self.ctx()
|
||||
)
|
||||
@@ -96,7 +96,7 @@ class Context(ManagedValue[V], Generic[V]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: RunnableConfig,
|
||||
loop: LoopProtocol,
|
||||
*,
|
||||
ctx: Union[None, Type[ContextManager[V]], Type[AsyncContextManager[V]]] = None,
|
||||
actx: Optional[Type[AsyncContextManager[V]]] = None,
|
||||
@@ -104,5 +104,5 @@ class Context(ManagedValue[V], Generic[V]):
|
||||
self.ctx = ctx
|
||||
self.actx = actx
|
||||
|
||||
def __call__(self, step: int) -> V:
|
||||
def __call__(self) -> V:
|
||||
return self.value
|
||||
|
||||
@@ -4,8 +4,16 @@ from langgraph.managed.base import ManagedValue
|
||||
|
||||
|
||||
class IsLastStepManager(ManagedValue[bool]):
|
||||
def __call__(self, step: int) -> bool:
|
||||
return step == self.config.get("recursion_limit", 0) - 1
|
||||
def __call__(self) -> bool:
|
||||
return self.loop.step == self.loop.stop - 1
|
||||
|
||||
|
||||
IsLastStep = Annotated[bool, IsLastStepManager]
|
||||
|
||||
|
||||
class RemainingStepsManager(ManagedValue[int]):
|
||||
def __call__(self) -> int:
|
||||
return self.loop.stop - self.loop.step
|
||||
|
||||
|
||||
RemainingSteps = Annotated[int, RemainingStepsManager]
|
||||
|
||||
@@ -7,13 +7,11 @@ from typing import (
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
cast,
|
||||
)
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from typing_extensions import NotRequired, Required, Self
|
||||
|
||||
from langgraph.constants import CONF, CONFIG_KEY_STORE
|
||||
from langgraph.constants import CONF
|
||||
from langgraph.errors import InvalidUpdateError
|
||||
from langgraph.managed.base import (
|
||||
ChannelKeyPlaceholder,
|
||||
@@ -21,7 +19,8 @@ from langgraph.managed.base import (
|
||||
ConfiguredManagedValue,
|
||||
WritableManagedValue,
|
||||
)
|
||||
from langgraph.store.base import BaseStore, PutOp
|
||||
from langgraph.store.base import PutOp
|
||||
from langgraph.types import LoopProtocol
|
||||
|
||||
V = dict[str, Any]
|
||||
|
||||
@@ -55,25 +54,26 @@ class SharedValue(WritableManagedValue[Value, Update]):
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def enter(cls, config: RunnableConfig, **kwargs: Any) -> Iterator[Self]:
|
||||
with super().enter(config, **kwargs) as value:
|
||||
if value.store is not None:
|
||||
saved = value.store.search(value.ns)
|
||||
def enter(cls, loop: LoopProtocol, **kwargs: Any) -> Iterator[Self]:
|
||||
with super().enter(loop, **kwargs) as value:
|
||||
if loop.store is not None:
|
||||
saved = loop.store.search(value.ns)
|
||||
value.value = {it.key: it.value for it in saved}
|
||||
yield value
|
||||
|
||||
@classmethod
|
||||
@asynccontextmanager
|
||||
async def aenter(cls, config: RunnableConfig, **kwargs: Any) -> AsyncIterator[Self]:
|
||||
async with super().aenter(config, **kwargs) as value:
|
||||
if value.store is not None:
|
||||
saved = await value.store.asearch(value.ns)
|
||||
async def aenter(cls, loop: LoopProtocol, **kwargs: Any) -> AsyncIterator[Self]:
|
||||
async with super().aenter(loop, **kwargs) as value:
|
||||
if loop.store is not None:
|
||||
saved = await loop.store.asearch(value.ns)
|
||||
value.value = {it.key: it.value for it in saved}
|
||||
yield value
|
||||
|
||||
def __init__(
|
||||
self, config: RunnableConfig, *, typ: Type[Any], scope: str, key: str
|
||||
self, loop: LoopProtocol, *, typ: Type[Any], scope: str, key: str
|
||||
) -> None:
|
||||
super().__init__(loop)
|
||||
if typ := _strip_extras(typ):
|
||||
if typ not in (
|
||||
dict,
|
||||
@@ -83,18 +83,17 @@ class SharedValue(WritableManagedValue[Value, Update]):
|
||||
raise ValueError("SharedValue must be a dict")
|
||||
self.scope = scope
|
||||
self.value: Value = {}
|
||||
self.store = cast(BaseStore, config[CONF].get(CONFIG_KEY_STORE))
|
||||
if self.store is None:
|
||||
if self.loop.store is None:
|
||||
pass
|
||||
elif scope_value := config[CONF].get(self.scope):
|
||||
elif scope_value := self.loop.config[CONF].get(self.scope):
|
||||
self.ns = ("scoped", scope, key, scope_value)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Scope {scope} for shared state key not in config.configurable"
|
||||
)
|
||||
|
||||
def __call__(self, step: int) -> Value:
|
||||
return self.value.copy()
|
||||
def __call__(self) -> Value:
|
||||
return self.value
|
||||
|
||||
def _process_update(self, values: Sequence[Update]) -> list[PutOp]:
|
||||
writes: list[PutOp] = []
|
||||
@@ -112,13 +111,13 @@ class SharedValue(WritableManagedValue[Value, Update]):
|
||||
return writes
|
||||
|
||||
def update(self, values: Sequence[Update]) -> None:
|
||||
if self.store is None:
|
||||
if self.loop.store is None:
|
||||
self._process_update(values)
|
||||
else:
|
||||
return self.store.batch(self._process_update(values))
|
||||
return self.loop.store.batch(self._process_update(values))
|
||||
|
||||
async def aupdate(self, writes: Sequence[Update]) -> None:
|
||||
if self.store is None:
|
||||
if self.loop.store is None:
|
||||
self._process_update(writes)
|
||||
else:
|
||||
return await self.store.abatch(self._process_update(writes))
|
||||
return await self.loop.store.abatch(self._process_update(writes))
|
||||
|
||||
@@ -14,7 +14,7 @@ from langgraph._api.deprecation import deprecated_parameter
|
||||
from langgraph.graph import StateGraph
|
||||
from langgraph.graph.graph import CompiledGraph
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.managed import IsLastStep
|
||||
from langgraph.managed import IsLastStep, RemainingSteps
|
||||
from langgraph.prebuilt.tool_executor import ToolExecutor
|
||||
from langgraph.prebuilt.tool_node import ToolNode
|
||||
from langgraph.store.base import BaseStore
|
||||
@@ -33,6 +33,8 @@ class AgentState(TypedDict):
|
||||
|
||||
is_last_step: IsLastStep
|
||||
|
||||
remaining_steps: RemainingSteps
|
||||
|
||||
|
||||
StateSchema = TypeVar("StateSchema", bound=AgentState)
|
||||
StateSchemaType = Type[StateSchema]
|
||||
@@ -529,10 +531,28 @@ def create_react_agent(
|
||||
# Define the function that calls the model
|
||||
def call_model(state: AgentState, config: RunnableConfig) -> AgentState:
|
||||
response = model_runnable.invoke(state, config)
|
||||
has_tool_calls = isinstance(response, AIMessage) and response.tool_calls
|
||||
all_tools_return_direct = (
|
||||
all(call["name"] in should_return_direct for call in response.tool_calls)
|
||||
if isinstance(response, AIMessage)
|
||||
else False
|
||||
)
|
||||
if (
|
||||
state["is_last_step"]
|
||||
and isinstance(response, AIMessage)
|
||||
and response.tool_calls
|
||||
(
|
||||
"remaining_steps" not in state
|
||||
and state["is_last_step"]
|
||||
and has_tool_calls
|
||||
)
|
||||
or (
|
||||
"remaining_steps" in state
|
||||
and state["remaining_steps"] < 1
|
||||
and all_tools_return_direct
|
||||
)
|
||||
or (
|
||||
"remaining_steps" in state
|
||||
and state["remaining_steps"] < 2
|
||||
and has_tool_calls
|
||||
)
|
||||
):
|
||||
return {
|
||||
"messages": [
|
||||
@@ -547,10 +567,28 @@ def create_react_agent(
|
||||
|
||||
async def acall_model(state: AgentState, config: RunnableConfig) -> AgentState:
|
||||
response = await model_runnable.ainvoke(state, config)
|
||||
has_tool_calls = isinstance(response, AIMessage) and response.tool_calls
|
||||
all_tools_return_direct = (
|
||||
all(call["name"] in should_return_direct for call in response.tool_calls)
|
||||
if isinstance(response, AIMessage)
|
||||
else False
|
||||
)
|
||||
if (
|
||||
state["is_last_step"]
|
||||
and isinstance(response, AIMessage)
|
||||
and response.tool_calls
|
||||
(
|
||||
"remaining_steps" not in state
|
||||
and state["is_last_step"]
|
||||
and has_tool_calls
|
||||
)
|
||||
or (
|
||||
"remaining_steps" in state
|
||||
and state["remaining_steps"] < 1
|
||||
and all_tools_return_direct
|
||||
)
|
||||
or (
|
||||
"remaining_steps" in state
|
||||
and state["remaining_steps"] < 2
|
||||
and has_tool_calls
|
||||
)
|
||||
):
|
||||
return {
|
||||
"messages": [
|
||||
|
||||
@@ -67,7 +67,12 @@ from langgraph.constants import (
|
||||
NS_END,
|
||||
NS_SEP,
|
||||
)
|
||||
from langgraph.errors import GraphRecursionError, InvalidUpdateError
|
||||
from langgraph.errors import (
|
||||
ErrorCode,
|
||||
GraphRecursionError,
|
||||
InvalidUpdateError,
|
||||
create_error_message,
|
||||
)
|
||||
from langgraph.managed.base import ManagedValueSpec
|
||||
from langgraph.pregel.algo import (
|
||||
PregelTaskWrites,
|
||||
@@ -88,7 +93,7 @@ from langgraph.pregel.utils import find_subgraph_pregel, get_new_channel_version
|
||||
from langgraph.pregel.validate import validate_graph, validate_keys
|
||||
from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import All, Checkpointer, StateSnapshot, StreamMode
|
||||
from langgraph.types import All, Checkpointer, LoopProtocol, StateSnapshot, StreamMode
|
||||
from langgraph.utils.config import (
|
||||
ensure_config,
|
||||
merge_configs,
|
||||
@@ -164,9 +169,11 @@ class Channel:
|
||||
return ChannelWrite(
|
||||
[ChannelWriteEntry(c) for c in channels]
|
||||
+ [
|
||||
ChannelWriteEntry(k, mapper=v)
|
||||
if callable(v)
|
||||
else ChannelWriteEntry(k, value=v)
|
||||
(
|
||||
ChannelWriteEntry(k, mapper=v)
|
||||
if callable(v)
|
||||
else ChannelWriteEntry(k, value=v)
|
||||
)
|
||||
for k, v in kwargs.items()
|
||||
]
|
||||
)
|
||||
@@ -433,7 +440,14 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
)
|
||||
|
||||
with ChannelsManager(
|
||||
self.channels, saved.checkpoint, saved.config, skip_context=True
|
||||
self.channels,
|
||||
saved.checkpoint,
|
||||
LoopProtocol(
|
||||
config=saved.config,
|
||||
step=saved.metadata.get("step", -1) + 1,
|
||||
stop=saved.metadata.get("step", -1) + 2,
|
||||
),
|
||||
skip_context=True,
|
||||
) as (channels, managed):
|
||||
# tasks for this checkpoint
|
||||
next_tasks = prepare_next_tasks(
|
||||
@@ -484,7 +498,7 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
patch_checkpoint_map(saved.config, saved.metadata),
|
||||
saved.metadata,
|
||||
saved.checkpoint["ts"],
|
||||
saved.parent_config,
|
||||
patch_checkpoint_map(saved.parent_config, saved.metadata),
|
||||
tasks_w_writes(
|
||||
next_tasks.values(),
|
||||
saved.pending_writes,
|
||||
@@ -511,7 +525,14 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
)
|
||||
|
||||
async with AsyncChannelsManager(
|
||||
self.channels, saved.checkpoint, saved.config, skip_context=True
|
||||
self.channels,
|
||||
saved.checkpoint,
|
||||
LoopProtocol(
|
||||
config=saved.config,
|
||||
step=saved.metadata.get("step", -1) + 1,
|
||||
stop=saved.metadata.get("step", -1) + 2,
|
||||
),
|
||||
skip_context=True,
|
||||
) as (
|
||||
channels,
|
||||
managed,
|
||||
@@ -565,7 +586,7 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
patch_checkpoint_map(saved.config, saved.metadata),
|
||||
saved.metadata,
|
||||
saved.checkpoint["ts"],
|
||||
saved.parent_config,
|
||||
patch_checkpoint_map(saved.parent_config, saved.metadata),
|
||||
tasks_w_writes(
|
||||
next_tasks.values(),
|
||||
saved.pending_writes,
|
||||
@@ -835,7 +856,11 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
if as_node not in self.nodes:
|
||||
raise InvalidUpdateError(f"Node {as_node} does not exist")
|
||||
# update channels
|
||||
with ChannelsManager(self.channels, checkpoint, config) as (
|
||||
with ChannelsManager(
|
||||
self.channels,
|
||||
checkpoint,
|
||||
LoopProtocol(config=config, step=step + 1, stop=step + 2),
|
||||
) as (
|
||||
channels,
|
||||
managed,
|
||||
):
|
||||
@@ -981,7 +1006,11 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
if as_node not in self.nodes:
|
||||
raise InvalidUpdateError(f"Node {as_node} does not exist")
|
||||
# update channels, acting as the chosen node
|
||||
async with AsyncChannelsManager(self.channels, checkpoint, config) as (
|
||||
async with AsyncChannelsManager(
|
||||
self.channels,
|
||||
checkpoint,
|
||||
LoopProtocol(config=config, step=step + 1, stop=step + 2),
|
||||
) as (
|
||||
channels,
|
||||
managed,
|
||||
):
|
||||
@@ -1269,6 +1298,7 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
return waiter
|
||||
else:
|
||||
return waiter
|
||||
|
||||
else:
|
||||
get_waiter = None # type: ignore[assignment]
|
||||
# Similarly to Bulk Synchronous Parallel / Pregel model
|
||||
@@ -1294,11 +1324,15 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
yield from output()
|
||||
# handle exit
|
||||
if loop.status == "out_of_steps":
|
||||
raise GraphRecursionError(
|
||||
f"Recursion limit of {config['recursion_limit']} reached "
|
||||
"without hitting a stop condition. You can increase the "
|
||||
"limit by setting the `recursion_limit` config key."
|
||||
msg = create_error_message(
|
||||
message=(
|
||||
f"Recursion limit of {config['recursion_limit']} reached "
|
||||
"without hitting a stop condition. You can increase the "
|
||||
"limit by setting the `recursion_limit` config key."
|
||||
),
|
||||
error_code=ErrorCode.GRAPH_RECURSION_LIMIT,
|
||||
)
|
||||
raise GraphRecursionError(msg)
|
||||
# set final channel values as run output
|
||||
run_manager.on_chain_end(loop.output)
|
||||
except BaseException as e:
|
||||
@@ -1473,6 +1507,7 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
|
||||
def get_waiter() -> asyncio.Task[None]:
|
||||
return aioloop.create_task(stream.wait())
|
||||
|
||||
else:
|
||||
get_waiter = None # type: ignore[assignment]
|
||||
# Similarly to Bulk Synchronous Parallel / Pregel model
|
||||
@@ -1500,11 +1535,15 @@ class Pregel(Runnable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]]):
|
||||
yield o
|
||||
# handle exit
|
||||
if loop.status == "out_of_steps":
|
||||
raise GraphRecursionError(
|
||||
f"Recursion limit of {config['recursion_limit']} reached "
|
||||
"without hitting a stop condition. You can increase the "
|
||||
"limit by setting the `recursion_limit` config key."
|
||||
msg = create_error_message(
|
||||
message=(
|
||||
f"Recursion limit of {config['recursion_limit']} reached "
|
||||
"without hitting a stop condition. You can increase the "
|
||||
"limit by setting the `recursion_limit` config key."
|
||||
),
|
||||
error_code=ErrorCode.GRAPH_RECURSION_LIMIT,
|
||||
)
|
||||
raise GraphRecursionError(msg)
|
||||
# set final channel values as run output
|
||||
await run_manager.on_chain_end(loop.output)
|
||||
except BaseException as e:
|
||||
|
||||
@@ -57,7 +57,7 @@ from langgraph.pregel.log import logger
|
||||
from langgraph.pregel.manager import ChannelsManager
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import All, PregelExecutableTask, PregelTask
|
||||
from langgraph.types import All, LoopProtocol, PregelExecutableTask, PregelTask
|
||||
from langgraph.utils.config import merge_configs, patch_config
|
||||
|
||||
GetNextVersion = Callable[[Optional[V], BaseChannel], V]
|
||||
@@ -148,7 +148,7 @@ def local_read(
|
||||
with ChannelsManager(
|
||||
{k: v for k, v in channels.items() if k in updated},
|
||||
checkpoint,
|
||||
config,
|
||||
LoopProtocol(config=config, step=step, stop=step + 1),
|
||||
skip_context=True,
|
||||
) as (local_channels, _):
|
||||
apply_writes(copy_checkpoint(checkpoint), local_channels, [task], None)
|
||||
@@ -156,7 +156,7 @@ def local_read(
|
||||
else:
|
||||
values = read_channels(channels, select)
|
||||
if managed_keys:
|
||||
values.update({k: managed[k](step) for k in managed_keys})
|
||||
values.update({k: managed[k]() for k in managed_keys})
|
||||
return values
|
||||
|
||||
|
||||
@@ -493,9 +493,7 @@ def prepare_single_task(
|
||||
):
|
||||
try:
|
||||
val = next(
|
||||
_proc_input(
|
||||
step, proc, managed, channels, for_execution=for_execution
|
||||
)
|
||||
_proc_input(proc, managed, channels, for_execution=for_execution)
|
||||
)
|
||||
except StopIteration:
|
||||
return
|
||||
@@ -583,7 +581,6 @@ def prepare_single_task(
|
||||
|
||||
|
||||
def _proc_input(
|
||||
step: int,
|
||||
proc: PregelNode,
|
||||
managed: ManagedValueMapping,
|
||||
channels: Mapping[str, BaseChannel],
|
||||
@@ -605,7 +602,7 @@ def _proc_input(
|
||||
except EmptyChannelError:
|
||||
continue
|
||||
else:
|
||||
val[k] = managed[k](step)
|
||||
val[k] = managed[k]()
|
||||
except EmptyChannelError:
|
||||
return
|
||||
elif isinstance(proc.channels, list):
|
||||
|
||||
@@ -32,6 +32,7 @@ from langgraph.constants import (
|
||||
from langgraph.pregel.io import read_channels
|
||||
from langgraph.pregel.utils import find_subgraph_pregel
|
||||
from langgraph.types import PregelExecutableTask, PregelTask, StateSnapshot
|
||||
from langgraph.utils.config import patch_checkpoint_map
|
||||
|
||||
|
||||
class TaskPayload(TypedDict):
|
||||
@@ -177,8 +178,8 @@ def map_debug_checkpoint(
|
||||
"timestamp": checkpoint["ts"],
|
||||
"step": step,
|
||||
"payload": {
|
||||
"config": config,
|
||||
"parent_config": parent_config,
|
||||
"config": patch_checkpoint_map(config, metadata),
|
||||
"parent_config": patch_checkpoint_map(parent_config, metadata),
|
||||
"values": read_channels(channels, stream_channels),
|
||||
"metadata": metadata,
|
||||
"next": [t.name for t in tasks],
|
||||
|
||||
@@ -86,19 +86,21 @@ class BackgroundExecutor(ContextManager):
|
||||
exc_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> Optional[bool]:
|
||||
# copy the tasks as done() callback may modify the dict
|
||||
tasks = self.tasks.copy()
|
||||
# cancel all tasks that should be cancelled
|
||||
for task, (cancel, _) in self.tasks.items():
|
||||
for task, (cancel, _) in tasks.items():
|
||||
if cancel:
|
||||
task.cancel()
|
||||
# wait for all tasks to finish
|
||||
if tasks := {t for t in self.tasks if not t.done()}:
|
||||
concurrent.futures.wait(tasks)
|
||||
if pending := {t for t in tasks if not t.done()}:
|
||||
concurrent.futures.wait(pending)
|
||||
# shutdown the executor
|
||||
self.stack.__exit__(exc_type, exc_value, traceback)
|
||||
# re-raise the first exception that occurred in a task
|
||||
if exc_type is None:
|
||||
# if there's already an exception being raised, don't raise another one
|
||||
for task, (_, reraise) in self.tasks.items():
|
||||
for task, (_, reraise) in tasks.items():
|
||||
if not reraise:
|
||||
continue
|
||||
try:
|
||||
@@ -161,17 +163,19 @@ class AsyncBackgroundExecutor(AsyncContextManager):
|
||||
exc_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> None:
|
||||
# copy the tasks as done() callback may modify the dict
|
||||
tasks = self.tasks.copy()
|
||||
# cancel all tasks that should be cancelled
|
||||
for task, (cancel, _) in self.tasks.items():
|
||||
for task, (cancel, _) in tasks.items():
|
||||
if cancel:
|
||||
task.cancel(self.sentinel)
|
||||
# wait for all tasks to finish
|
||||
if self.tasks:
|
||||
await asyncio.wait(self.tasks)
|
||||
if tasks:
|
||||
await asyncio.wait(tasks)
|
||||
# if there's already an exception being raised, don't raise another one
|
||||
if exc_type is None:
|
||||
# re-raise the first exception that occurred in a task
|
||||
for task, (_, reraise) in self.tasks.items():
|
||||
for task, (_, reraise) in tasks.items():
|
||||
if not reraise:
|
||||
continue
|
||||
try:
|
||||
|
||||
@@ -100,7 +100,7 @@ from langgraph.pregel.manager import AsyncChannelsManager, ChannelsManager
|
||||
from langgraph.pregel.read import PregelNode
|
||||
from langgraph.pregel.utils import get_new_channel_versions
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import All, PregelExecutableTask, StreamMode
|
||||
from langgraph.types import All, LoopProtocol, PregelExecutableTask, StreamProtocol
|
||||
from langgraph.utils.config import patch_configurable
|
||||
|
||||
V = TypeVar("V")
|
||||
@@ -112,22 +112,6 @@ INPUT_RESUMING = object()
|
||||
SPECIAL_CHANNELS = (ERROR, INTERRUPT, SCHEDULED)
|
||||
|
||||
|
||||
class StreamProtocol:
|
||||
__slots__ = ("modes", "__call__")
|
||||
|
||||
modes: set[StreamMode]
|
||||
|
||||
__call__: Callable[[StreamChunk], None]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
__call__: Callable[[StreamChunk], None],
|
||||
modes: set[StreamMode],
|
||||
) -> None:
|
||||
self.__call__ = __call__
|
||||
self.modes = modes
|
||||
|
||||
|
||||
def DuplexStream(*streams: StreamProtocol) -> StreamProtocol:
|
||||
def __call__(value: StreamChunk) -> None:
|
||||
for stream in streams:
|
||||
@@ -137,16 +121,13 @@ def DuplexStream(*streams: StreamProtocol) -> StreamProtocol:
|
||||
return StreamProtocol(__call__, {mode for s in streams for mode in s.modes})
|
||||
|
||||
|
||||
class PregelLoop:
|
||||
class PregelLoop(LoopProtocol):
|
||||
input: Optional[Any]
|
||||
config: RunnableConfig
|
||||
store: Optional[BaseStore]
|
||||
checkpointer: Optional[BaseCheckpointSaver]
|
||||
nodes: Mapping[str, PregelNode]
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]]
|
||||
output_keys: Union[str, Sequence[str]]
|
||||
stream_keys: Union[str, Sequence[str]]
|
||||
stream: Optional[StreamProtocol]
|
||||
skip_done_tasks: bool
|
||||
is_nested: bool
|
||||
|
||||
@@ -177,8 +158,6 @@ class PregelLoop:
|
||||
checkpoint_previous_versions: dict[str, Union[str, float, int]]
|
||||
prev_checkpoint_config: Optional[RunnableConfig]
|
||||
|
||||
step: int
|
||||
stop: int
|
||||
status: Literal[
|
||||
"pending", "done", "interrupt_before", "interrupt_after", "out_of_steps"
|
||||
]
|
||||
@@ -202,10 +181,14 @@ class PregelLoop:
|
||||
check_subgraphs: bool = True,
|
||||
debug: bool = False,
|
||||
) -> None:
|
||||
self.stream = stream
|
||||
super().__init__(
|
||||
step=0,
|
||||
stop=0,
|
||||
config=config,
|
||||
stream=stream,
|
||||
store=store,
|
||||
)
|
||||
self.input = input
|
||||
self.config = config
|
||||
self.store = store
|
||||
self.checkpointer = checkpointer
|
||||
self.nodes = nodes
|
||||
self.specs = specs
|
||||
@@ -226,7 +209,11 @@ class PregelLoop:
|
||||
)
|
||||
if check_subgraphs and self.is_nested and self.checkpointer is not None:
|
||||
if self.config[CONF][CONFIG_KEY_CHECKPOINT_NS] in _SEEN_CHECKPOINT_NS:
|
||||
raise MultipleSubgraphsError
|
||||
raise MultipleSubgraphsError(
|
||||
"Multiple subgraphs called inside the same node\n\n"
|
||||
"Troubleshooting URL: https://python.langchain.com/docs"
|
||||
"/troubleshooting/errors/MULTIPLE_SUBGRAPHS/"
|
||||
)
|
||||
else:
|
||||
_SEEN_CHECKPOINT_NS.add(self.config[CONF][CONFIG_KEY_CHECKPOINT_NS])
|
||||
if (
|
||||
@@ -298,9 +285,11 @@ class PregelLoop:
|
||||
print_step_writes(
|
||||
self.step,
|
||||
writes,
|
||||
[self.stream_keys]
|
||||
if isinstance(self.stream_keys, str)
|
||||
else self.stream_keys,
|
||||
(
|
||||
[self.stream_keys]
|
||||
if isinstance(self.stream_keys, str)
|
||||
else self.stream_keys
|
||||
),
|
||||
)
|
||||
# all tasks have finished
|
||||
mv_writes = apply_writes(
|
||||
@@ -502,7 +491,7 @@ class PregelLoop:
|
||||
)
|
||||
|
||||
def _put_checkpoint(self, metadata: CheckpointMetadata) -> None:
|
||||
# assign step
|
||||
# assign step and parents
|
||||
metadata["step"] = self.step
|
||||
metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {})
|
||||
# debug flag
|
||||
@@ -510,21 +499,24 @@ class PregelLoop:
|
||||
print_step_checkpoint(
|
||||
metadata,
|
||||
self.channels,
|
||||
[self.stream_keys]
|
||||
if isinstance(self.stream_keys, str)
|
||||
else self.stream_keys,
|
||||
(
|
||||
[self.stream_keys]
|
||||
if isinstance(self.stream_keys, str)
|
||||
else self.stream_keys
|
||||
),
|
||||
)
|
||||
# create new checkpoint
|
||||
self.checkpoint = create_checkpoint(self.checkpoint, self.channels, self.step)
|
||||
# bail if no checkpointer
|
||||
if self._checkpointer_put_after_previous is not None:
|
||||
self.checkpoint_metadata = metadata
|
||||
|
||||
self.prev_checkpoint_config = (
|
||||
self.checkpoint_config
|
||||
if CONFIG_KEY_CHECKPOINT_ID in self.checkpoint_config[CONF]
|
||||
and self.checkpoint_config[CONF][CONFIG_KEY_CHECKPOINT_ID]
|
||||
else None
|
||||
)
|
||||
self.checkpoint_metadata = metadata
|
||||
self.checkpoint_config = {
|
||||
**self.checkpoint_config,
|
||||
CONF: {
|
||||
@@ -729,7 +721,7 @@ class SyncPregelLoop(PregelLoop, ContextManager):
|
||||
|
||||
self.submit = self.stack.enter_context(BackgroundExecutor(self.config))
|
||||
self.channels, self.managed = self.stack.enter_context(
|
||||
ChannelsManager(self.specs, self.checkpoint, self.config, self.store)
|
||||
ChannelsManager(self.specs, self.checkpoint, self)
|
||||
)
|
||||
self.stack.push(self._suppress_interrupt)
|
||||
self.status = "pending"
|
||||
@@ -857,7 +849,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager):
|
||||
|
||||
self.submit = await self.stack.enter_async_context(AsyncBackgroundExecutor())
|
||||
self.channels, self.managed = await self.stack.enter_async_context(
|
||||
AsyncChannelsManager(self.specs, self.checkpoint, self.config, self.store)
|
||||
AsyncChannelsManager(self.specs, self.checkpoint, self)
|
||||
)
|
||||
self.stack.push(self._suppress_interrupt)
|
||||
self.status = "pending"
|
||||
|
||||
@@ -1,33 +1,27 @@
|
||||
import asyncio
|
||||
from contextlib import AsyncExitStack, ExitStack, asynccontextmanager, contextmanager
|
||||
from typing import AsyncIterator, Iterator, Mapping, Optional, Union
|
||||
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
from typing import AsyncIterator, Iterator, Mapping, Union
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
from langgraph.constants import CONFIG_KEY_STORE
|
||||
from langgraph.managed.base import (
|
||||
ConfiguredManagedValue,
|
||||
ManagedValueMapping,
|
||||
ManagedValueSpec,
|
||||
)
|
||||
from langgraph.managed.context import Context
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.utils.config import patch_configurable
|
||||
from langgraph.types import LoopProtocol
|
||||
|
||||
|
||||
@contextmanager
|
||||
def ChannelsManager(
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
||||
checkpoint: Checkpoint,
|
||||
config: RunnableConfig,
|
||||
store: Optional[BaseStore] = None,
|
||||
loop: LoopProtocol,
|
||||
*,
|
||||
skip_context: bool = False,
|
||||
) -> Iterator[tuple[Mapping[str, BaseChannel], ManagedValueMapping]]:
|
||||
"""Manage channels for the lifetime of a Pregel invocation (multiple steps)."""
|
||||
config_for_managed = patch_configurable(config, {CONFIG_KEY_STORE: store})
|
||||
channel_specs: dict[str, BaseChannel] = {}
|
||||
managed_specs: dict[str, ManagedValueSpec] = {}
|
||||
for k, v in specs.items():
|
||||
@@ -48,9 +42,9 @@ def ChannelsManager(
|
||||
ManagedValueMapping(
|
||||
{
|
||||
key: stack.enter_context(
|
||||
value.cls.enter(config_for_managed, **value.kwargs)
|
||||
value.cls.enter(loop, **value.kwargs)
|
||||
if isinstance(value, ConfiguredManagedValue)
|
||||
else value.enter(config_for_managed)
|
||||
else value.enter(loop)
|
||||
)
|
||||
for key, value in managed_specs.items()
|
||||
}
|
||||
@@ -62,13 +56,11 @@ def ChannelsManager(
|
||||
async def AsyncChannelsManager(
|
||||
specs: Mapping[str, Union[BaseChannel, ManagedValueSpec]],
|
||||
checkpoint: Checkpoint,
|
||||
config: RunnableConfig,
|
||||
store: Optional[BaseStore] = None,
|
||||
loop: LoopProtocol,
|
||||
*,
|
||||
skip_context: bool = False,
|
||||
) -> AsyncIterator[tuple[Mapping[str, BaseChannel], ManagedValueMapping]]:
|
||||
"""Manage channels for the lifetime of a Pregel invocation (multiple steps)."""
|
||||
config_for_managed = patch_configurable(config, {CONFIG_KEY_STORE: store})
|
||||
channel_specs: dict[str, BaseChannel] = {}
|
||||
managed_specs: dict[str, ManagedValueSpec] = {}
|
||||
for k, v in specs.items():
|
||||
@@ -85,9 +77,9 @@ async def AsyncChannelsManager(
|
||||
if tasks := {
|
||||
asyncio.create_task(
|
||||
stack.enter_async_context(
|
||||
value.cls.aenter(config_for_managed, **value.kwargs)
|
||||
value.cls.aenter(loop, **value.kwargs)
|
||||
if isinstance(value, ConfiguredManagedValue)
|
||||
else value.aenter(config_for_managed)
|
||||
else value.aenter(loop)
|
||||
)
|
||||
): key
|
||||
for key, value in managed_specs.items()
|
||||
|
||||
@@ -17,7 +17,7 @@ from langchain_core.messages import BaseMessage
|
||||
from langchain_core.outputs import ChatGenerationChunk, LLMResult
|
||||
from langchain_core.tracers._streaming import T, _StreamingCallbackHandler
|
||||
|
||||
from langgraph.constants import NS_SEP
|
||||
from langgraph.constants import NS_SEP, TAG_HIDDEN, TAG_NOSTREAM
|
||||
from langgraph.pregel.loop import StreamChunk
|
||||
|
||||
Meta = tuple[tuple[str, ...], dict[str, Any]]
|
||||
@@ -63,7 +63,7 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
||||
metadata: Optional[dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
if metadata:
|
||||
if metadata and (not tags or TAG_NOSTREAM not in tags):
|
||||
self.metadata[run_id] = (
|
||||
tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP)),
|
||||
metadata,
|
||||
@@ -114,7 +114,11 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
if metadata and kwargs.get("name") == metadata.get("langgraph_node"):
|
||||
if (
|
||||
metadata
|
||||
and kwargs.get("name") == metadata.get("langgraph_node")
|
||||
and (not tags or TAG_HIDDEN not in tags)
|
||||
):
|
||||
self.metadata[run_id] = (
|
||||
tuple(cast(str, metadata["langgraph_checkpoint_ns"]).split(NS_SEP)),
|
||||
metadata,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Literal,
|
||||
@@ -15,6 +16,9 @@ from langchain_core.runnables import Runnable, RunnableConfig
|
||||
|
||||
from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langgraph.store.base import BaseStore
|
||||
|
||||
All = Literal["*"]
|
||||
"""Special value to indicate that graph should interrupt on all nodes."""
|
||||
|
||||
@@ -213,3 +217,45 @@ class Send:
|
||||
and self.node == value.node
|
||||
and self.arg == value.arg
|
||||
)
|
||||
|
||||
|
||||
StreamChunk = tuple[tuple[str, ...], str, Any]
|
||||
|
||||
|
||||
class StreamProtocol:
|
||||
__slots__ = ("modes", "__call__")
|
||||
|
||||
modes: set[StreamMode]
|
||||
|
||||
__call__: Callable[[StreamChunk], None]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
__call__: Callable[[StreamChunk], None],
|
||||
modes: set[StreamMode],
|
||||
) -> None:
|
||||
self.__call__ = __call__
|
||||
self.modes = modes
|
||||
|
||||
|
||||
class LoopProtocol:
|
||||
config: RunnableConfig
|
||||
store: Optional["BaseStore"]
|
||||
stream: Optional[StreamProtocol]
|
||||
step: int
|
||||
stop: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
step: int,
|
||||
stop: int,
|
||||
config: RunnableConfig,
|
||||
store: Optional["BaseStore"] = None,
|
||||
stream: Optional[StreamProtocol] = None,
|
||||
) -> None:
|
||||
self.stream = stream
|
||||
self.config = config
|
||||
self.store = store
|
||||
self.step = step
|
||||
self.stop = stop
|
||||
|
||||
@@ -36,9 +36,11 @@ def patch_configurable(
|
||||
|
||||
|
||||
def patch_checkpoint_map(
|
||||
config: RunnableConfig, metadata: Optional[CheckpointMetadata]
|
||||
config: Optional[RunnableConfig], metadata: Optional[CheckpointMetadata]
|
||||
) -> RunnableConfig:
|
||||
if parents := (metadata.get("parents") if metadata else None):
|
||||
if config is None:
|
||||
return config
|
||||
elif parents := (metadata.get("parents") if metadata else None):
|
||||
conf = config[CONF]
|
||||
return patch_configurable(
|
||||
config,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.36"
|
||||
version = "0.2.38"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -31,7 +31,7 @@ langgraph-checkpoint-sqlite = {path = "../checkpoint-sqlite", develop = true}
|
||||
langgraph-checkpoint-postgres = {path = "../checkpoint-postgres", develop = true}
|
||||
langgraph-sdk = {path = "../sdk-py", develop = true}
|
||||
psycopg = {extras = ["binary"], version = ">=3.0.0", python = ">=3.10"}
|
||||
uvloop = "0.21.0beta1"
|
||||
uvloop = "0.21.0"
|
||||
pyperf = "^2.7.0"
|
||||
py-spy = "^0.3.14"
|
||||
types-requests = "^2.32.0.20240914"
|
||||
|
||||
@@ -4078,18 +4078,6 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None:
|
||||
stream_mode="messages",
|
||||
)
|
||||
] == [
|
||||
(
|
||||
_AnyIdHumanMessage(
|
||||
content="what is weather in sf",
|
||||
),
|
||||
{
|
||||
"langgraph_step": 0,
|
||||
"langgraph_node": "__start__",
|
||||
"langgraph_triggers": ["__start__"],
|
||||
"langgraph_path": ("__pregel_pull", "__start__"),
|
||||
"langgraph_checkpoint_ns": AnyStr("__start__:"),
|
||||
},
|
||||
),
|
||||
(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
@@ -9092,6 +9080,9 @@ def test_nested_graph_state(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("inner:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("child:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -9250,6 +9241,9 @@ def test_nested_graph_state(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("inner:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("child:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(PregelTask(AnyStr(), "inner_2", (PULL, "inner_2")),),
|
||||
@@ -9279,6 +9273,9 @@ def test_nested_graph_state(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("inner:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("child:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(
|
||||
@@ -9707,6 +9704,13 @@ def test_doubly_nested_graph_state(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr(),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("child:"): AnyStr(),
|
||||
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -9770,6 +9774,15 @@ def test_doubly_nested_graph_state(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr(),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("child:"): AnyStr(),
|
||||
AnyStr(
|
||||
re.compile(r"child:.+|child1:")
|
||||
): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -9798,6 +9811,9 @@ def test_doubly_nested_graph_state(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("child:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("child:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -10056,6 +10072,9 @@ def test_doubly_nested_graph_state(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("child:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("child:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(),
|
||||
@@ -10085,6 +10104,9 @@ def test_doubly_nested_graph_state(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("child:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("child:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(
|
||||
@@ -10170,6 +10192,13 @@ def test_doubly_nested_graph_state(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr(),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("child:"): AnyStr(),
|
||||
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(),
|
||||
@@ -10208,6 +10237,13 @@ def test_doubly_nested_graph_state(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr(),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("child:"): AnyStr(),
|
||||
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(
|
||||
@@ -10253,6 +10289,13 @@ def test_doubly_nested_graph_state(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr(),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("child:"): AnyStr(),
|
||||
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(
|
||||
@@ -10444,6 +10487,12 @@ def test_send_to_nested_graphs(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("generate_joke:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("generate_joke:"): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(PregelTask(id=AnyStr(""), name="generate", path=(PULL, "generate")),),
|
||||
@@ -10476,6 +10525,12 @@ def test_send_to_nested_graphs(
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("generate_joke:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("generate_joke:"): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(PregelTask(id=AnyStr(""), name="generate", path=(PULL, "generate")),),
|
||||
@@ -10931,6 +10986,12 @@ def test_weather_subgraph(
|
||||
"thread_id": "14",
|
||||
"checkpoint_ns": AnyStr("weather_graph:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("weather_graph:"): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(
|
||||
@@ -11020,6 +11081,12 @@ def test_weather_subgraph(
|
||||
"thread_id": "14",
|
||||
"checkpoint_ns": AnyStr("weather_graph:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("weather_graph:"): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(),
|
||||
@@ -11917,6 +11984,8 @@ def test_debug_nested_subgraphs():
|
||||
clean_config["thread_id"] = config["configurable"]["thread_id"]
|
||||
clean_config["checkpoint_id"] = config["configurable"]["checkpoint_id"]
|
||||
clean_config["checkpoint_ns"] = config["configurable"]["checkpoint_ns"]
|
||||
if "checkpoint_map" in config["configurable"]:
|
||||
clean_config["checkpoint_map"] = config["configurable"]["checkpoint_map"]
|
||||
|
||||
return clean_config
|
||||
|
||||
|
||||
@@ -3999,18 +3999,6 @@ async def test_prebuilt_tool_chat() -> None:
|
||||
stream_mode="messages",
|
||||
)
|
||||
] == [
|
||||
(
|
||||
_AnyIdHumanMessage(
|
||||
content="what is weather in sf",
|
||||
),
|
||||
{
|
||||
"langgraph_step": 0,
|
||||
"langgraph_node": "__start__",
|
||||
"langgraph_triggers": ["__start__"],
|
||||
"langgraph_path": ("__pregel_pull", "__start__"),
|
||||
"langgraph_checkpoint_ns": AnyStr("__start__:"),
|
||||
},
|
||||
),
|
||||
(
|
||||
_AnyIdAIMessageChunk(
|
||||
content="",
|
||||
@@ -7738,6 +7726,9 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("inner:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("child:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -7903,6 +7894,9 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("inner:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("inner:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(
|
||||
@@ -7934,6 +7928,9 @@ async def test_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("inner:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("inner:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(
|
||||
@@ -8328,6 +8325,9 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("child:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("child:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
).tasks[0]
|
||||
@@ -8374,6 +8374,13 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr(),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("child:"): AnyStr(),
|
||||
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -8439,6 +8446,15 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr(),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("child:"): AnyStr(),
|
||||
AnyStr(
|
||||
re.compile(r"child:.+|child1:")
|
||||
): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -8467,6 +8483,9 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("child:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("child:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -8732,6 +8751,9 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("child:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("child:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(),
|
||||
@@ -8761,6 +8783,9 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr("child:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{"": AnyStr(), AnyStr("child:"): AnyStr()}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(
|
||||
@@ -8850,6 +8875,13 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr(),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("child:"): AnyStr(),
|
||||
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(),
|
||||
@@ -8888,6 +8920,13 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr(),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("child:"): AnyStr(),
|
||||
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(
|
||||
@@ -8933,6 +8972,13 @@ async def test_doubly_nested_graph_state(checkpointer_name: str) -> None:
|
||||
"thread_id": "1",
|
||||
"checkpoint_ns": AnyStr(),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("child:"): AnyStr(),
|
||||
AnyStr(re.compile(r"child:.+|child1:")): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(
|
||||
@@ -9568,6 +9614,12 @@ async def test_weather_subgraph(
|
||||
"thread_id": "14",
|
||||
"checkpoint_ns": AnyStr("weather_graph:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("weather_graph:"): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(
|
||||
@@ -9659,6 +9711,12 @@ async def test_weather_subgraph(
|
||||
"thread_id": "14",
|
||||
"checkpoint_ns": AnyStr("weather_graph:"),
|
||||
"checkpoint_id": AnyStr(),
|
||||
"checkpoint_map": AnyDict(
|
||||
{
|
||||
"": AnyStr(),
|
||||
AnyStr("weather_graph:"): AnyStr(),
|
||||
}
|
||||
),
|
||||
}
|
||||
},
|
||||
tasks=(),
|
||||
@@ -10143,6 +10201,8 @@ async def test_debug_nested_subgraphs():
|
||||
clean_config["thread_id"] = config["configurable"]["thread_id"]
|
||||
clean_config["checkpoint_id"] = config["configurable"]["checkpoint_id"]
|
||||
clean_config["checkpoint_ns"] = config["configurable"]["checkpoint_ns"]
|
||||
if "checkpoint_map" in config["configurable"]:
|
||||
clean_config["checkpoint_map"] = config["configurable"]["checkpoint_map"]
|
||||
|
||||
return clean_config
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ from langgraph.scheduler.kafka.types import (
|
||||
Sendable,
|
||||
Topics,
|
||||
)
|
||||
from langgraph.types import RetryPolicy
|
||||
from langgraph.types import LoopProtocol, RetryPolicy
|
||||
from langgraph.utils.config import patch_configurable
|
||||
|
||||
|
||||
@@ -183,7 +183,14 @@ class AsyncKafkaExecutor(AbstractAsyncContextManager):
|
||||
if saved.checkpoint["id"] != msg["config"]["configurable"]["checkpoint_id"]:
|
||||
raise CheckpointNotLatest()
|
||||
async with AsyncChannelsManager(
|
||||
graph.channels, saved.checkpoint, msg["config"], self.graph.store
|
||||
graph.channels,
|
||||
saved.checkpoint,
|
||||
LoopProtocol(
|
||||
config=msg["config"],
|
||||
store=self.graph.store,
|
||||
step=saved.metadata["step"] + 1,
|
||||
stop=saved.metadata["step"] + 2,
|
||||
),
|
||||
) as (channels, managed), AsyncBackgroundExecutor() as submit:
|
||||
if task := await asyncio.to_thread(
|
||||
prepare_single_task,
|
||||
@@ -379,7 +386,14 @@ class KafkaExecutor(AbstractContextManager):
|
||||
if saved.checkpoint["id"] != msg["config"]["configurable"]["checkpoint_id"]:
|
||||
raise CheckpointNotLatest()
|
||||
with ChannelsManager(
|
||||
graph.channels, saved.checkpoint, msg["config"], self.graph.store
|
||||
graph.channels,
|
||||
saved.checkpoint,
|
||||
LoopProtocol(
|
||||
config=msg["config"],
|
||||
store=self.graph.store,
|
||||
step=saved.metadata["step"] + 1,
|
||||
stop=saved.metadata["step"] + 2,
|
||||
),
|
||||
) as (channels, managed), BackgroundExecutor({}) as submit:
|
||||
if task := prepare_single_task(
|
||||
msg["task"]["path"],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.16",
|
||||
"version": "0.0.17",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
|
||||