Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

langgraph: add prebuilt chain graph #2164

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions libs/langgraph/langgraph/prebuilt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""langgraph.prebuilt exposes a higher-level API for creating and executing agents and tools."""

Check notice on line 1 in libs/langgraph/langgraph/prebuilt/__init__.py

View workflow job for this annotation

GitHub Actions / benchmark

Benchmark results

......................................... fanout_to_subgraph_10x: Mean +- std dev: 47.9 ms +- 0.7 ms ......................................... fanout_to_subgraph_10x_sync: Mean +- std dev: 45.7 ms +- 3.1 ms ......................................... fanout_to_subgraph_10x_checkpoint: Mean +- std dev: 76.7 ms +- 2.0 ms ......................................... fanout_to_subgraph_10x_checkpoint_sync: Mean +- std dev: 84.6 ms +- 0.6 ms ......................................... fanout_to_subgraph_100x: Mean +- std dev: 467 ms +- 11 ms ......................................... fanout_to_subgraph_100x_sync: Mean +- std dev: 433 ms +- 14 ms ......................................... fanout_to_subgraph_100x_checkpoint: Mean +- std dev: 779 ms +- 40 ms ......................................... fanout_to_subgraph_100x_checkpoint_sync: Mean +- std dev: 831 ms +- 15 ms ......................................... react_agent_10x: Mean +- std dev: 28.9 ms +- 0.7 ms ......................................... react_agent_10x_sync: Mean +- std dev: 22.2 ms +- 1.7 ms ......................................... react_agent_10x_checkpoint: Mean +- std dev: 46.9 ms +- 2.8 ms ......................................... react_agent_10x_checkpoint_sync: Mean +- std dev: 36.3 ms +- 2.9 ms ......................................... react_agent_100x: Mean +- std dev: 323 ms +- 13 ms ......................................... react_agent_100x_sync: Mean +- std dev: 259 ms +- 14 ms ......................................... react_agent_100x_checkpoint: Mean +- std dev: 933 ms +- 39 ms ......................................... react_agent_100x_checkpoint_sync: Mean +- std dev: 829 ms +- 41 ms ......................................... wide_state_25x300: Mean +- std dev: 18.5 ms +- 0.4 ms ......................................... wide_state_25x300_sync: Mean +- std dev: 11.0 ms +- 0.1 ms ......................................... wide_state_25x300_checkpoint: Mean +- std dev: 271 ms +- 3 ms ......................................... wide_state_25x300_checkpoint_sync: Mean +- std dev: 261 ms +- 5 ms ......................................... wide_state_15x600: Mean +- std dev: 21.4 ms +- 0.4 ms ......................................... wide_state_15x600_sync: Mean +- std dev: 12.8 ms +- 0.4 ms ......................................... wide_state_15x600_checkpoint: Mean +- std dev: 471 ms +- 7 ms ......................................... wide_state_15x600_checkpoint_sync: Mean +- std dev: 458 ms +- 8 ms ......................................... wide_state_9x1200: Mean +- std dev: 21.4 ms +- 0.4 ms ......................................... wide_state_9x1200_sync: Mean +- std dev: 12.6 ms +- 0.2 ms ......................................... wide_state_9x1200_checkpoint: Mean +- std dev: 304 ms +- 4 ms ......................................... wide_state_9x1200_checkpoint_sync: Mean +- std dev: 291 ms +- 3 ms

Check notice on line 1 in libs/langgraph/langgraph/prebuilt/__init__.py

View workflow job for this annotation

GitHub Actions / benchmark

Comparison against main

+-----------------------------------------+---------+-----------------------+ | Benchmark | main | changes | +=========================================+=========+=======================+ | fanout_to_subgraph_10x | 54.0 ms | 47.9 ms: 1.13x faster | +-----------------------------------------+---------+-----------------------+ | wide_state_9x1200_checkpoint | 307 ms | 304 ms: 1.01x faster | +-----------------------------------------+---------+-----------------------+ | wide_state_15x600_checkpoint | 474 ms | 471 ms: 1.01x faster | +-----------------------------------------+---------+-----------------------+ | wide_state_9x1200_checkpoint_sync | 293 ms | 291 ms: 1.00x faster | +-----------------------------------------+---------+-----------------------+ | wide_state_25x300_checkpoint | 272 ms | 271 ms: 1.00x faster | +-----------------------------------------+---------+-----------------------+ | fanout_to_subgraph_100x_checkpoint_sync | 822 ms | 831 ms: 1.01x slower | +-----------------------------------------+---------+-----------------------+ | fanout_to_subgraph_10x_checkpoint_sync | 83.7 ms | 84.6 ms: 1.01x slower | +-----------------------------------------+---------+-----------------------+ | fanout_to_subgraph_100x_sync | 428 ms | 433 ms: 1.01x slower | +-----------------------------------------+---------+-----------------------+ | react_agent_100x | 319 ms | 323 ms: 1.01x slower | +-----------------------------------------+---------+-----------------------+ | fanout_to_subgraph_10x_checkpoint | 75.7 ms | 76.7 ms: 1.01x slower | +-----------------------------------------+---------+-----------------------+ | wide_state_9x1200_sync | 12.4 ms | 12.6 ms: 1.02x slower | +-----------------------------------------+---------+-----------------------+ | react_agent_10x_sync | 21.8 ms | 22.2 ms: 1.02x slower | +-----------------------------------------+---------+-----------------------+ | wide_state_25x300_sync | 10.8 ms | 11.0 ms: 1.02x slower | +-----------------------------------------+---------+-----------------------+ | react_agent_100x_sync | 254 ms | 259 ms: 1.02x slower | +-----------------------------------------+---------+-----------------------+ | wide_state_9x1200 | 20.9 ms | 21.4 ms: 1.02x slower | +-----------------------------------------+---------+-----------------------+ | react_agent_100x_checkpoint_sync | 810 ms | 829 ms: 1.02x slower | +-----------------------------------------+---------+-----------------------+ | wide_state_15x600 | 20.9 ms | 21.4 ms: 1.02x slower | +-----------------------------------------+---------+-----------------------+ | wide_state_25x300 | 18.0 ms | 18.5 ms: 1.02x slower | +-----------------------------------------+---------+-----------------------+ | react_agent_10x | 28.2 ms | 28.9 ms: 1.03x slower | +-----------------------------------------+---------+-----------------------+ | wide_state_15x600_sync | 12.4 ms | 12.8 ms: 1.03x slower | +-----------------------------------------+---------+-----------------------+ | react_agent_100x_checkpoint | 907 ms | 933 ms: 1.03x slower | +-----------------------------------------+---------+-----------------------+ | Geometric mean | (ref) | 1.01x slower | +-----------------------------------------+---------+-----------------------+ Benchmark hidden because not significant (7): fanout_to_subgraph_100x_checkpoint, wide_state_15x600_checkpoint_sync, fanout_to_subgraph_100x, wide_state_25x300_checkpoint_sync, react_agent_10x_checkpoint_sync, fanout_to_subgraph_10x_sync, react_agent_10x_checkpoint

from langgraph.prebuilt.chat_agent_executor import create_react_agent
from langgraph.prebuilt.sequential_executor import create_sequential_executor
vbarda marked this conversation as resolved.
Show resolved Hide resolved
from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation
from langgraph.prebuilt.tool_node import (
InjectedState,
Expand All @@ -12,6 +13,7 @@

__all__ = [
"create_react_agent",
"create_sequential_executor",
"ToolExecutor",
"ToolInvocation",
"ToolNode",
Expand Down
77 changes: 77 additions & 0 deletions libs/langgraph/langgraph/prebuilt/sequential_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from typing import Any, Optional, Type, Union, cast

from langchain_core.runnables.base import Runnable, RunnableLike

from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.graph.state import END, START, CompiledStateGraph, StateGraph
from langgraph.store.base import BaseStore


def _get_name(step: RunnableLike) -> str:
if isinstance(step, Runnable):
if step.name is None:
raise ValueError(
f"Runnable ({step}) needs to have a name attribute. "
"Consider setting the name or passing it as a tuple (name, runnable)."
)
return step.name
elif callable(step):
return getattr(step, "__name__", step.__class__.__name__)
else:
raise TypeError(f"Unsupported step type: {step}")


def create_sequential_executor(
*steps: Union[RunnableLike, tuple[str, RunnableLike]],
state_schema: Type[Any],
vbarda marked this conversation as resolved.
Show resolved Hide resolved
checkpointer: Optional[BaseCheckpointSaver] = None,
store: Optional[BaseStore] = None,
interrupt_before: Optional[list[str]] = None,
vbarda marked this conversation as resolved.
Show resolved Hide resolved
interrupt_after: Optional[list[str]] = None,
debug: bool = False,
) -> CompiledStateGraph:
"""Creates a sequential executor graph that runs a series of provided steps in order.

Args:
*steps: A sequence of RunnableLike objects or (name, RunnableLike) tuples.
vbarda marked this conversation as resolved.
Show resolved Hide resolved
If no names are provided, the name will be inferred from the step object (e.g. a runnable or a callable name).
Each step will be executed in the order provided.
state_schema: The state schema for the graph.
checkpointer: An optional checkpoint saver object. This is used for persisting
the state of the graph (e.g., as chat memory) for a single thread (e.g., a single conversation).
store: An optional store object. This is used for persisting data
across multiple threads (e.g., multiple conversations / users).
interrupt_before: An optional list of step names to interrupt before execution.
interrupt_after: An optional list of step names to interrupt after execution.
debug: A flag to enable debug mode.

Returns:
A CompiledStateGraph object.
"""
if len(steps) < 2:
vbarda marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError("Sequential executor requires at least two steps.")

builder = StateGraph(state_schema)
previous_name: Optional[str] = None
for step in steps:
if isinstance(step, tuple) and len(step) == 2:
name, step = step
else:
name = _get_name(step)

builder.add_node(name, step)
if previous_name is None:
builder.add_edge(START, name)
else:
builder.add_edge(previous_name, name)

previous_name = name

builder.add_edge(cast(str, previous_name), END)
return builder.compile(
vbarda marked this conversation as resolved.
Show resolved Hide resolved
checkpointer=checkpointer,
store=store,
interrupt_before=interrupt_before,
interrupt_after=interrupt_after,
debug=debug,
)
43 changes: 43 additions & 0 deletions libs/langgraph/tests/test_prebuilt.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dataclasses
import json
import operator
from functools import partial
from typing import (
Annotated,
Expand Down Expand Up @@ -41,6 +42,7 @@
ToolNode,
ValidationNode,
create_react_agent,
create_sequential_executor,
tools_condition,
)
from langgraph.prebuilt.tool_node import InjectedState, InjectedStore
Expand Down Expand Up @@ -968,3 +970,44 @@ def tool_normal(input: str) -> str:
id=result["messages"][3].id,
),
]


def test_sequential_executor():
class State(TypedDict):
foo: Annotated[list[str], operator.add]
bar: str

def step1(state: State):
return {"foo": ["step1"], "bar": "baz"}

def step2(state: State):
return {"foo": ["step2"]}

# test raising if less than 2 steps
with pytest.raises(ValueError):
create_sequential_executor(state_schema=State)

with pytest.raises(ValueError):
create_sequential_executor(step1, state_schema=State)

# test unnamed steps
executor = create_sequential_executor(step1, step2, state_schema=State)
result = executor.invoke({"foo": []})
assert result == {"foo": ["step1", "step2"], "bar": "baz"}
stream_chunks = list(executor.stream({"foo": []}))
assert stream_chunks == [
{"step1": {"foo": ["step1"], "bar": "baz"}},
{"step2": {"foo": ["step2"]}},
]

# test named steps
executor_named_steps = create_sequential_executor(
("meow1", step1), ("meow2", step2), state_schema=State
)
result = executor_named_steps.invoke({"foo": []})
stream_chunks = list(executor_named_steps.stream({"foo": []}))
assert result == {"foo": ["step1", "step2"], "bar": "baz"}
assert stream_chunks == [
{"meow1": {"foo": ["step1"], "bar": "baz"}},
{"meow2": {"foo": ["step2"]}},
]
Loading