mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-21 15:12:26 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af6b812c4 |
@@ -45,7 +45,7 @@ agent.invoke(
|
||||
LangGraph is built for developers who want to build powerful, adaptable AI agents. Developers choose LangGraph for:
|
||||
|
||||
- **Reliability and controllability.** Steer agent actions with moderation checks and human-in-the-loop approvals. LangGraph persists context for long-running workflows, keeping your agents on course.
|
||||
- **Low-level and extensible.** Build custom agents with fully descriptive, low-level primitives – free from rigid abstractions that limit customization. Design scalable multi-agent systems, with each agent serving a specific role tailored to your use case.
|
||||
- **Low-level and extensible.** Build custom agents with low-level primitives, avoiding both rigid high-level frameworks and limited DAG-only orchestrators. LangGraph supports cyclic workflows and enables multi-agent systems, with each agent tailored to your use case.
|
||||
- **First-class streaming support.** With token-by-token streaming and streaming of intermediate steps, LangGraph gives users clear visibility into agent reasoning and actions as they unfold in real time.
|
||||
|
||||
LangGraph is trusted in production and powering agents for companies like:
|
||||
|
||||
@@ -2,22 +2,6 @@
|
||||
|
||||
The LangGraph Cloud Server supports specific environment variables for configuring a deployment.
|
||||
|
||||
## `BG_JOB_ISOLATED_LOOPS`
|
||||
|
||||
Set `BG_JOB_ISOLATED_LOOPS` to `True` to execute background runs in an isolated event loop separate from the serving API event loop.
|
||||
|
||||
This environment variable should be set to `True` if the implementation of a graph/node contains synchronous code. In this situation, the synchronous code will block the serving API event loop, which may cause the API to be unavailable. A symptom of an unavailable API is continuous application restarts due to failing health checks.
|
||||
|
||||
Defaults to `False`.
|
||||
|
||||
## `BG_JOB_TIMEOUT_SECS`
|
||||
|
||||
The timeout of a background run can be increased. However, the infrastructure for a Cloud SaaS deployment enforces a 1 hour timeout limit for API requests. This means the connection between client and server will timeout after 1 hour. This is not configurable.
|
||||
|
||||
A background run can execute for longer than 1 hour, but a client must reconnect to the server (e.g. join stream via `POST /threads/{thread_id}/runs/{run_id}/stream`) to retrieve output from the run if the run is taking longer than 1 hour.
|
||||
|
||||
Defaults to `3600`.
|
||||
|
||||
## `DD_API_KEY`
|
||||
|
||||
Specify `DD_API_KEY` (your [Datadog API Key](https://docs.datadoghq.com/account_management/api-app-keys/)) to automatically enable Datadog tracing for the deployment. Specify other [`DD_*` environment variables](https://ddtrace.readthedocs.io/en/stable/configuration.html) to configure the tracing instrumentation.
|
||||
@@ -44,10 +28,6 @@ Set this environment variable to have a BYOC deployment send traces to a self-ho
|
||||
|
||||
`SELF_HOSTED_LANGSMITH_HOSTNAME` is the hostname of the self-hosted LangSmith instance. It must be accessible to the BYOC deployment. `LANGSMITH_API_KEY` is a LangSmith API generated from the self-hosted LangSmith instance.
|
||||
|
||||
## `LOG_LEVEL`
|
||||
|
||||
Configure [log level](https://docs.python.org/3/library/logging.html#logging-levels). Defaults to `INFO`.
|
||||
|
||||
## `N_JOBS_PER_WORKER`
|
||||
|
||||
Number of jobs per worker for the LangGraph Cloud task queue. Defaults to `10`.
|
||||
@@ -75,9 +55,3 @@ Database Connectivity:
|
||||
|
||||
- The externally managed Postgres instance must be accessible by the LangGraph Server service in the ECS cluster. The BYOC user is responsible for ensuring connectivity.
|
||||
- For example, if an AWS RDS Postgres instance is provisioned, it can be provisioned in the same VPC (`langgraph-cloud-vpc`) as the ECS cluster with the `langgraph-cloud-service-sg` security group to ensure connectivity.
|
||||
|
||||
## `REDIS_URI_CUSTOM`
|
||||
|
||||
For [Bring Your Own Cloud (BYOC)](../../concepts/bring_your_own_cloud.md) deployments only.
|
||||
|
||||
Specify `REDIS_URI_CUSTOM` to use an externally managed Redis instance. The value of `REDIS_URI_CUSTOM` must be a valid [Redis connection URI](https://redis-py.readthedocs.io/en/stable/connections.html#redis.Redis.from_url).
|
||||
|
||||
@@ -124,7 +124,6 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint:
|
||||
)
|
||||
|
||||
|
||||
# Kept for backwards compat, newer versions of LangGraph no longer use this.
|
||||
def create_checkpoint(
|
||||
checkpoint: Checkpoint,
|
||||
channels: Optional[Mapping[str, ChannelProtocol]],
|
||||
|
||||
@@ -30,7 +30,6 @@ from langgraph.checkpoint.serde.types import SendProtocol
|
||||
from langgraph.store.base import Item
|
||||
|
||||
LC_REVIVER = Reviver()
|
||||
EMPTY_BYTES = b""
|
||||
|
||||
|
||||
class JsonPlusSerializer(SerializerProtocol):
|
||||
@@ -195,9 +194,7 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
)
|
||||
|
||||
def dumps_typed(self, obj: Any) -> tuple[str, bytes]:
|
||||
if obj is None:
|
||||
return "null", EMPTY_BYTES
|
||||
elif isinstance(obj, bytes):
|
||||
if isinstance(obj, bytes):
|
||||
return "bytes", obj
|
||||
elif isinstance(obj, bytearray):
|
||||
return "bytearray", obj
|
||||
@@ -214,9 +211,7 @@ class JsonPlusSerializer(SerializerProtocol):
|
||||
|
||||
def loads_typed(self, data: tuple[str, bytes]) -> Any:
|
||||
type_, data_ = data
|
||||
if type_ == "null":
|
||||
return None
|
||||
elif type_ == "bytes":
|
||||
if type_ == "bytes":
|
||||
return data_
|
||||
elif type_ == "bytearray":
|
||||
return bytearray(data_)
|
||||
|
||||
@@ -580,12 +580,6 @@ def dockerfile(save_path: str, config: pathlib.Path, add_docker_compose: bool) -
|
||||
default=None,
|
||||
help="URL of the LangGraph Studio instance to connect to. Defaults to https://smith.langchain.com",
|
||||
)
|
||||
@click.option(
|
||||
"--allow-blocking",
|
||||
is_flag=True,
|
||||
help="Don't raise errors for synchronous I/O blocking operations in your code.",
|
||||
default=False,
|
||||
)
|
||||
@cli.command(
|
||||
"dev",
|
||||
help="🏃♀️➡️ Run LangGraph API server in development mode with hot reloading and debugging support",
|
||||
@@ -601,7 +595,6 @@ def dev(
|
||||
debug_port: Optional[int],
|
||||
wait_for_client: bool,
|
||||
studio_url: Optional[str],
|
||||
allow_blocking: bool,
|
||||
):
|
||||
"""CLI entrypoint for running the LangGraph API server."""
|
||||
try:
|
||||
@@ -666,7 +659,6 @@ def dev(
|
||||
auth=config_json.get("auth"),
|
||||
http=config_json.get("http"),
|
||||
studio_url=studio_url,
|
||||
allow_blocking=allow_blocking,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -405,7 +405,6 @@ def validate_config(config: Config) -> Config:
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
"ui": config.get("ui"),
|
||||
"ui_config": config.get("ui_config"),
|
||||
}
|
||||
if config.get("node_version")
|
||||
else {
|
||||
@@ -419,7 +418,6 @@ def validate_config(config: Config) -> Config:
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
"ui": config.get("ui"),
|
||||
"ui_config": config.get("ui_config"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1100,7 +1098,6 @@ RUN cd {faux_path} && {install_cmd}
|
||||
{env_additional_config}
|
||||
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
||||
{f"ENV LANGGRAPH_UI='{json.dumps(config['ui'])}'" if config.get("ui") else ""}
|
||||
{f"ENV LANGGRAPH_UI_CONFIG='{json.dumps(config['ui_config'])}'" if config.get("ui_config") else ""}
|
||||
|
||||
WORKDIR {faux_path}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.1.82"
|
||||
version = "0.1.80"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -34,7 +34,6 @@ def test_validate_config():
|
||||
"auth": None,
|
||||
"http": None,
|
||||
"ui": None,
|
||||
"ui_config": None,
|
||||
**expected_config,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
@@ -55,7 +54,6 @@ def test_validate_config():
|
||||
"auth": None,
|
||||
"http": None,
|
||||
"ui": None,
|
||||
"ui_config": None,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
assert actual_config == expected_config
|
||||
@@ -472,7 +470,6 @@ def test_config_to_docker_nodejs():
|
||||
"graphs": graphs,
|
||||
"dockerfile_lines": ["ARG meow", "ARG foo"],
|
||||
"ui": {"agent": "./graphs/agent.ui.jsx"},
|
||||
"ui_config": {"shared": ["nuqs"]},
|
||||
}
|
||||
),
|
||||
"langchain/langgraphjs-api",
|
||||
@@ -484,7 +481,6 @@ ADD . /deps/unit_tests
|
||||
RUN cd /deps/unit_tests && npm i
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
|
||||
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
|
||||
ENV LANGGRAPH_UI_CONFIG='{"shared": ["nuqs"]}'
|
||||
WORKDIR /deps/unit_tests
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ agent.invoke(
|
||||
LangGraph is built for developers who want to build powerful, adaptable AI agents. Developers choose LangGraph for:
|
||||
|
||||
- **Reliability and controllability.** Steer agent actions with moderation checks and human-in-the-loop approvals. LangGraph persists context for long-running workflows, keeping your agents on course.
|
||||
- **Low-level and extensible.** Build custom agents with fully descriptive, low-level primitives – free from rigid abstractions that limit customization. Design scalable multi-agent systems, with each agent serving a specific role tailored to your use case.
|
||||
- **Low-level and extensible.** Build custom agents with low-level primitives, avoiding both rigid high-level frameworks and limited DAG-only orchestrators. LangGraph supports cyclic workflows and enables multi-agent systems with each agent tailored to your use case.
|
||||
- **First-class streaming support.** With token-by-token streaming and streaming of intermediate steps, LangGraph gives users clear visibility into agent reasoning and actions as they unfold in real time.
|
||||
|
||||
LangGraph is trusted in production and powering agents for companies like:
|
||||
|
||||
@@ -34,7 +34,7 @@ if __name__ == "__main__":
|
||||
|
||||
import uvloop
|
||||
|
||||
graph = create_sequential(3000).compile()
|
||||
graph = create_sequential(2000).compile()
|
||||
input = {"messages": []} # Empty list of messages
|
||||
config = {"recursion_limit": 20000000000}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
from typing import Any, Generic, Optional, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -30,15 +30,10 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.key)
|
||||
empty.value = self.value
|
||||
return empty
|
||||
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.key)
|
||||
if checkpoint is not MISSING:
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
@@ -60,6 +55,3 @@ class AnyValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
def checkpoint(self) -> Value:
|
||||
return self.value
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Generic, Sequence, TypeVar
|
||||
from typing import Any, Generic, Optional, Sequence, TypeVar
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
Value = TypeVar("Value")
|
||||
@@ -30,23 +29,14 @@ class BaseChannel(Generic[Value, Update, C], ABC):
|
||||
|
||||
# serialize/deserialize methods
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel.
|
||||
By default, delegates to checkpoint() and from_checkpoint().
|
||||
Subclasses can override this method with a more efficient implementation."""
|
||||
return self.from_checkpoint(self.checkpoint())
|
||||
|
||||
def checkpoint(self) -> C:
|
||||
def checkpoint(self) -> Optional[C]:
|
||||
"""Return a serializable representation of the channel's current state.
|
||||
Raises EmptyChannelError if the channel is empty (never updated yet),
|
||||
or doesn't support checkpoints."""
|
||||
try:
|
||||
return self.get()
|
||||
except EmptyChannelError:
|
||||
return MISSING
|
||||
return self.get()
|
||||
|
||||
@abstractmethod
|
||||
def from_checkpoint(self, checkpoint: C) -> Self:
|
||||
def from_checkpoint(self, checkpoint: Optional[C]) -> Self:
|
||||
"""Return a new identical channel, optionally initialized from a checkpoint.
|
||||
If the checkpoint contains complex data structures, they should be copied."""
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import collections.abc
|
||||
from typing import Callable, Generic, Sequence, Type
|
||||
from typing import (
|
||||
Callable,
|
||||
Generic,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
)
|
||||
|
||||
from typing_extensions import NotRequired, Required, Self
|
||||
|
||||
@@ -66,17 +72,10 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
empty = self.__class__(self.typ, self.operator)
|
||||
empty.key = self.key
|
||||
empty.value = self.value
|
||||
return empty
|
||||
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.operator)
|
||||
empty.key = self.key
|
||||
if checkpoint is not MISSING:
|
||||
if checkpoint is not None:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
@@ -97,6 +96,3 @@ class BinaryOperatorAggregate(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
def checkpoint(self) -> Value:
|
||||
return self.value
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Any, Generic, NamedTuple, Optional, Sequence, Type, Union
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
|
||||
@@ -46,23 +45,16 @@ class DynamicBarrierValue(
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
empty.names = self.names
|
||||
empty.seen = self.seen.copy()
|
||||
return empty
|
||||
|
||||
def checkpoint(self) -> tuple[Optional[set[Value]], set[Value]]:
|
||||
return (self.names, self.seen)
|
||||
|
||||
def from_checkpoint(
|
||||
self, checkpoint: tuple[Optional[set[Value]], set[Value]]
|
||||
self,
|
||||
checkpoint: Optional[tuple[Optional[set[Value]], set[Value]]],
|
||||
) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
if checkpoint is not MISSING:
|
||||
if checkpoint is not None:
|
||||
names, seen = checkpoint
|
||||
empty.names = names if names is not None else None
|
||||
empty.seen = seen
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
from typing import Any, Generic, Optional, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -30,17 +30,10 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
empty = self.__class__(self.typ, self.guard)
|
||||
empty.key = self.key
|
||||
empty.value = self.value
|
||||
return empty
|
||||
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.guard)
|
||||
empty.key = self.key
|
||||
if checkpoint is not MISSING:
|
||||
if checkpoint is not None:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
@@ -66,6 +59,3 @@ class EphemeralValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
def checkpoint(self) -> Value:
|
||||
return self.value
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Generic, Sequence, Type
|
||||
from typing import Any, Generic, Optional, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -34,15 +34,10 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.key)
|
||||
empty.value = self.value
|
||||
return empty
|
||||
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
empty = self.__class__(self.typ, self.key)
|
||||
if checkpoint is not MISSING:
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
empty = self.__class__(self.typ)
|
||||
empty.key = self.key
|
||||
if checkpoint is not None:
|
||||
empty.value = checkpoint
|
||||
return empty
|
||||
|
||||
@@ -66,6 +61,3 @@ class LastValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
|
||||
def is_available(self) -> bool:
|
||||
return self.value is not MISSING
|
||||
|
||||
def checkpoint(self) -> Value:
|
||||
return self.value
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from typing import Generic, Sequence, Type
|
||||
from typing import Generic, Optional, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
|
||||
@@ -33,20 +32,13 @@ class NamedBarrierValue(Generic[Value], BaseChannel[Value, Value, set[Value]]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.names)
|
||||
empty.key = self.key
|
||||
empty.seen = self.seen.copy()
|
||||
return empty
|
||||
|
||||
def checkpoint(self) -> set[Value]:
|
||||
return self.seen
|
||||
|
||||
def from_checkpoint(self, checkpoint: set[Value]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: Optional[set[Value]]) -> Self:
|
||||
empty = self.__class__(self.typ, self.names)
|
||||
empty.key = self.key
|
||||
if checkpoint is not MISSING:
|
||||
if checkpoint is not None:
|
||||
empty.seen = checkpoint
|
||||
return empty
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from typing import Any, Generic, Iterator, Sequence, Type, Union
|
||||
from typing import Any, Generic, Iterator, Optional, Sequence, Type, Union
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from langgraph.channels.base import BaseChannel, Value
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError
|
||||
|
||||
|
||||
@@ -17,7 +16,9 @@ def flatten(values: Sequence[Union[Value, list[Value]]]) -> Iterator[Value]:
|
||||
|
||||
class Topic(
|
||||
Generic[Value],
|
||||
BaseChannel[Sequence[Value], Union[Value, list[Value]], list[Value]],
|
||||
BaseChannel[
|
||||
Sequence[Value], Union[Value, list[Value]], tuple[set[Value], list[Value]]
|
||||
],
|
||||
):
|
||||
"""A configurable PubSub Topic.
|
||||
|
||||
@@ -48,22 +49,14 @@ class Topic(
|
||||
"""The type of the update received by the channel."""
|
||||
return Union[self.typ, list[self.typ]] # type: ignore[name-defined]
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.accumulate)
|
||||
empty.key = self.key
|
||||
empty.values = self.values.copy()
|
||||
return empty
|
||||
|
||||
def checkpoint(self) -> list[Value]:
|
||||
def checkpoint(self) -> tuple[set[Value], list[Value]]:
|
||||
return self.values
|
||||
|
||||
def from_checkpoint(self, checkpoint: list[Value]) -> Self:
|
||||
def from_checkpoint(self, checkpoint: Optional[list[Value]]) -> Self:
|
||||
empty = self.__class__(self.typ, self.accumulate)
|
||||
empty.key = self.key
|
||||
if checkpoint is not MISSING:
|
||||
if checkpoint is not None:
|
||||
if isinstance(checkpoint, tuple):
|
||||
# backwards compatibility
|
||||
empty.values = checkpoint[1]
|
||||
else:
|
||||
empty.values = checkpoint
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Generic, Sequence, Type
|
||||
from typing import Generic, Optional, Sequence, Type
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
@@ -30,17 +30,10 @@ class UntrackedValue(Generic[Value], BaseChannel[Value, Value, Value]):
|
||||
"""The type of the update received by the channel."""
|
||||
return self.typ
|
||||
|
||||
def copy(self) -> Self:
|
||||
"""Return a copy of the channel."""
|
||||
empty = self.__class__(self.typ, self.guard)
|
||||
empty.key = self.key
|
||||
empty.value = self.value
|
||||
return empty
|
||||
|
||||
def checkpoint(self) -> Value:
|
||||
return MISSING
|
||||
raise EmptyChannelError()
|
||||
|
||||
def from_checkpoint(self, checkpoint: Value) -> Self:
|
||||
def from_checkpoint(self, checkpoint: Optional[Value]) -> Self:
|
||||
empty = self.__class__(self.typ, self.guard)
|
||||
empty.key = self.key
|
||||
return empty
|
||||
|
||||
@@ -845,7 +845,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
if end != END:
|
||||
self.nodes[starts].writers.append(
|
||||
ChannelWrite(
|
||||
(ChannelWriteEntry(CHANNEL_BRANCH_TO.format(end), None),)
|
||||
(ChannelWriteEntry(CHANNEL_BRANCH_TO.format(end), starts),)
|
||||
)
|
||||
)
|
||||
elif end != END:
|
||||
@@ -871,7 +871,7 @@ class CompiledStateGraph(CompiledGraph):
|
||||
if filtered := [p for p in packets if p != END]:
|
||||
writes = [
|
||||
(
|
||||
ChannelWriteEntry(CHANNEL_BRANCH_TO.format(p), None)
|
||||
ChannelWriteEntry(CHANNEL_BRANCH_TO.format(p), start)
|
||||
if not isinstance(p, Send)
|
||||
else p
|
||||
)
|
||||
|
||||
@@ -50,6 +50,7 @@ from langgraph.checkpoint.base import (
|
||||
BaseCheckpointSaver,
|
||||
CheckpointTuple,
|
||||
copy_checkpoint,
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
from langgraph.constants import (
|
||||
@@ -90,7 +91,6 @@ from langgraph.pregel.algo import (
|
||||
local_write,
|
||||
prepare_next_tasks,
|
||||
)
|
||||
from langgraph.pregel.checkpoint import create_checkpoint
|
||||
from langgraph.pregel.debug import tasks_w_writes
|
||||
from langgraph.pregel.io import map_input, read_channels
|
||||
from langgraph.pregel.loop import AsyncPregelLoop, StreamProtocol, SyncPregelLoop
|
||||
@@ -1535,9 +1535,12 @@ class Pregel(PregelProtocol):
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step + 1,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
task,
|
||||
config,
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -1941,9 +1944,12 @@ class Pregel(PregelProtocol):
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step + 1,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
task,
|
||||
config,
|
||||
),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import binascii
|
||||
import itertools
|
||||
import sys
|
||||
import threading
|
||||
from collections import defaultdict, deque
|
||||
from functools import partial
|
||||
from hashlib import sha1
|
||||
@@ -33,6 +32,7 @@ from langgraph.checkpoint.base import (
|
||||
Checkpoint,
|
||||
PendingWrite,
|
||||
V,
|
||||
copy_checkpoint,
|
||||
)
|
||||
from langgraph.constants import (
|
||||
CONF,
|
||||
@@ -68,10 +68,12 @@ from langgraph.managed.base import ManagedValueMapping
|
||||
from langgraph.pregel.call import get_runnable_for_task
|
||||
from langgraph.pregel.io import read_channel, read_channels
|
||||
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,
|
||||
LoopProtocol,
|
||||
PregelExecutableTask,
|
||||
PregelScratchpad,
|
||||
PregelTask,
|
||||
@@ -166,39 +168,39 @@ def should_interrupt(
|
||||
|
||||
|
||||
def local_read(
|
||||
step: int,
|
||||
checkpoint: Checkpoint,
|
||||
channels: Mapping[str, BaseChannel],
|
||||
managed: ManagedValueMapping,
|
||||
task: WritesProtocol,
|
||||
config: RunnableConfig,
|
||||
select: Union[list[str], str],
|
||||
fresh: bool = False,
|
||||
) -> Union[dict[str, Any], Any]:
|
||||
"""Function injected under CONFIG_KEY_READ in task config, to read current state.
|
||||
Used by conditional edges to read a copy of the state with reflecting the writes
|
||||
from that node only."""
|
||||
updated: dict[str, list[Any]] = defaultdict(list)
|
||||
if isinstance(select, str):
|
||||
managed_keys = []
|
||||
for c, v in task.writes:
|
||||
for c, _ in task.writes:
|
||||
if c == select:
|
||||
updated[c].append(v)
|
||||
updated = {c}
|
||||
break
|
||||
else:
|
||||
updated = set()
|
||||
else:
|
||||
managed_keys = [k for k in select if k in managed]
|
||||
select = [k for k in select if k not in managed]
|
||||
for c, v in task.writes:
|
||||
if c in select:
|
||||
updated[c].append(v)
|
||||
updated = set(select).intersection(c for c, _ in task.writes)
|
||||
if fresh and updated:
|
||||
# apply writes
|
||||
local_channels: dict[str, BaseChannel] = {}
|
||||
for k in channels:
|
||||
if k in updated:
|
||||
cc = channels[k].copy()
|
||||
cc.update(updated[k])
|
||||
else:
|
||||
cc = channels[k]
|
||||
local_channels[k] = cc
|
||||
# read fresh values
|
||||
values = read_channels(local_channels, select)
|
||||
with ChannelsManager(
|
||||
{k: v for k, v in channels.items() if k in updated},
|
||||
checkpoint,
|
||||
LoopProtocol(config=config, step=step, stop=step + 1),
|
||||
skip_context=True,
|
||||
) as (local_channels, _):
|
||||
apply_writes(copy_checkpoint(checkpoint), local_channels, [task], None)
|
||||
values = read_channels({**channels, **local_channels}, select)
|
||||
else:
|
||||
values = read_channels(channels, select)
|
||||
if managed_keys:
|
||||
@@ -332,17 +334,6 @@ def apply_writes(
|
||||
return pending_writes_by_managed, updated_channels
|
||||
|
||||
|
||||
def has_next_tasks(
|
||||
trigger_to_nodes: Mapping[str, Sequence[str]],
|
||||
updated_channels: set[str],
|
||||
checkpoint: Checkpoint,
|
||||
) -> bool:
|
||||
"""Check if there are any tasks that should be run in the next step."""
|
||||
return bool(checkpoint["pending_sends"]) or not updated_channels.isdisjoint(
|
||||
trigger_to_nodes
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
def prepare_next_tasks(
|
||||
checkpoint: Checkpoint,
|
||||
@@ -570,9 +561,12 @@ def prepare_single_task(
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(task_path[:3], name, writes, triggers),
|
||||
config,
|
||||
),
|
||||
CONFIG_KEY_STORE: (store or configurable.get(CONFIG_KEY_STORE)),
|
||||
CONFIG_KEY_CHECKPOINTER: (
|
||||
@@ -672,11 +666,14 @@ def prepare_single_task(
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(
|
||||
task_path[:3], packet.node, writes, triggers
|
||||
),
|
||||
config,
|
||||
),
|
||||
CONFIG_KEY_STORE: (
|
||||
store or configurable.get(CONFIG_KEY_STORE)
|
||||
@@ -791,6 +788,8 @@ def prepare_single_task(
|
||||
),
|
||||
CONFIG_KEY_READ: partial(
|
||||
local_read,
|
||||
step,
|
||||
checkpoint,
|
||||
channels,
|
||||
managed,
|
||||
PregelTaskWrites(
|
||||
@@ -799,6 +798,7 @@ def prepare_single_task(
|
||||
writes,
|
||||
triggers,
|
||||
),
|
||||
config,
|
||||
),
|
||||
CONFIG_KEY_STORE: (
|
||||
store or configurable.get(CONFIG_KEY_STORE)
|
||||
@@ -869,30 +869,11 @@ def _scratchpad(
|
||||
pending_writes: list[PendingWrite],
|
||||
task_id: str,
|
||||
) -> PregelScratchpad:
|
||||
if len(pending_writes) > 0:
|
||||
# find global resume value
|
||||
for w in pending_writes:
|
||||
if w[0] == NULL_TASK_ID and w[1] == RESUME:
|
||||
null_resume_write = w
|
||||
break
|
||||
else:
|
||||
# None cannot be used as a resume value, because it would be difficult to
|
||||
# distinguish from missing when used over http
|
||||
null_resume_write = None
|
||||
# find task-specific resume value
|
||||
for w in pending_writes:
|
||||
if w[0] == task_id and w[1] == RESUME:
|
||||
task_resume_write = w[2]
|
||||
if not isinstance(task_resume_write, list):
|
||||
task_resume_write = [task_resume_write]
|
||||
break
|
||||
else:
|
||||
task_resume_write = []
|
||||
# clear var
|
||||
del w
|
||||
else:
|
||||
null_resume_write = None
|
||||
task_resume_write = []
|
||||
# None cannot be used as a resume value, because it would be difficult to
|
||||
# distinguish from missing when used over http
|
||||
null_resume_write = next(
|
||||
(w for w in pending_writes if w[0] == NULL_TASK_ID and w[1] == RESUME), None
|
||||
)
|
||||
|
||||
def get_null_resume(consume: bool = False) -> Any:
|
||||
if null_resume_write is None:
|
||||
@@ -910,13 +891,15 @@ def _scratchpad(
|
||||
# using itertools.count as an atomic counter (+= 1 is not thread-safe)
|
||||
return PregelScratchpad(
|
||||
# call
|
||||
call_counter=LazyAtomicCounter(),
|
||||
call_counter=itertools.count(0).__next__,
|
||||
# interrupt
|
||||
interrupt_counter=LazyAtomicCounter(),
|
||||
resume=task_resume_write,
|
||||
interrupt_counter=itertools.count(0).__next__,
|
||||
resume=next(
|
||||
(w[2] for w in pending_writes if w[0] == task_id and w[1] == RESUME), []
|
||||
),
|
||||
get_null_resume=get_null_resume,
|
||||
# subgraph
|
||||
subgraph_counter=LazyAtomicCounter(),
|
||||
subgraph_counter=itertools.count(0).__next__,
|
||||
)
|
||||
|
||||
|
||||
@@ -990,22 +973,3 @@ def task_path_str(tup: Union[str, int, tuple]) -> str:
|
||||
if isinstance(tup, int)
|
||||
else str(tup)
|
||||
)
|
||||
|
||||
|
||||
LAZY_ATOMIC_COUNTER_LOCK = threading.Lock()
|
||||
|
||||
|
||||
class LazyAtomicCounter:
|
||||
__slots__ = ("_counter",)
|
||||
|
||||
_counter: Optional[Callable[[], int]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._counter = None
|
||||
|
||||
def __call__(self) -> int:
|
||||
if self._counter is None:
|
||||
with LAZY_ATOMIC_COUNTER_LOCK:
|
||||
if self._counter is None:
|
||||
self._counter = itertools.count(0).__next__
|
||||
return self._counter()
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Mapping, Optional
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import LATEST_VERSION, Checkpoint
|
||||
from langgraph.checkpoint.base.id import uuid6
|
||||
from langgraph.constants import MISSING
|
||||
|
||||
|
||||
def create_checkpoint(
|
||||
checkpoint: Checkpoint,
|
||||
channels: Optional[Mapping[str, BaseChannel]],
|
||||
step: int,
|
||||
*,
|
||||
id: Optional[str] = None,
|
||||
) -> Checkpoint:
|
||||
"""Create a checkpoint for the given channels."""
|
||||
ts = datetime.now(timezone.utc).isoformat()
|
||||
if channels is None:
|
||||
values = checkpoint["channel_values"]
|
||||
else:
|
||||
values = {}
|
||||
for k in channels:
|
||||
if k not in checkpoint["channel_versions"]:
|
||||
continue
|
||||
v = channels[k].checkpoint()
|
||||
if v is not MISSING:
|
||||
values[k] = v
|
||||
return Checkpoint(
|
||||
v=LATEST_VERSION,
|
||||
ts=ts,
|
||||
id=id or str(uuid6(clock_seq=step)),
|
||||
channel_values=values,
|
||||
channel_versions=checkpoint["channel_versions"],
|
||||
versions_seen=checkpoint["versions_seen"],
|
||||
pending_sends=checkpoint.get("pending_sends", []),
|
||||
)
|
||||
@@ -38,6 +38,7 @@ from langgraph.checkpoint.base import (
|
||||
CheckpointTuple,
|
||||
PendingWrite,
|
||||
copy_checkpoint,
|
||||
create_checkpoint,
|
||||
empty_checkpoint,
|
||||
)
|
||||
from langgraph.constants import (
|
||||
@@ -87,7 +88,6 @@ from langgraph.pregel.algo import (
|
||||
should_interrupt,
|
||||
task_path_str,
|
||||
)
|
||||
from langgraph.pregel.checkpoint import create_checkpoint
|
||||
from langgraph.pregel.debug import (
|
||||
map_debug_checkpoint,
|
||||
map_debug_task_results,
|
||||
@@ -155,8 +155,6 @@ class PregelLoop(LoopProtocol):
|
||||
manager: Union[None, AsyncParentRunManager, ParentRunManager]
|
||||
interrupt_after: Union[All, Sequence[str]]
|
||||
interrupt_before: Union[All, Sequence[str]]
|
||||
checkpoint_every_step: bool
|
||||
debug: bool
|
||||
|
||||
checkpointer_get_next_version: GetNextVersion
|
||||
checkpointer_put_writes: Optional[
|
||||
@@ -213,7 +211,6 @@ class PregelLoop(LoopProtocol):
|
||||
input_model: Optional[Type[BaseModel]] = None,
|
||||
debug: bool = False,
|
||||
trigger_to_nodes: Optional[Mapping[str, Sequence[str]]] = None,
|
||||
checkpoint_every_step: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
step=0,
|
||||
@@ -238,7 +235,6 @@ class PregelLoop(LoopProtocol):
|
||||
or CONFIG_KEY_DEDUPE_TASKS in config[CONF]
|
||||
)
|
||||
self.trigger_to_nodes = trigger_to_nodes
|
||||
self.checkpoint_every_step = checkpoint_every_step
|
||||
self.debug = debug
|
||||
if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]:
|
||||
self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM])
|
||||
@@ -707,6 +703,8 @@ class PregelLoop(LoopProtocol):
|
||||
return updated_channels
|
||||
|
||||
def _put_checkpoint(self, metadata: CheckpointMetadata) -> None:
|
||||
for k, v in self.config["metadata"].items():
|
||||
metadata.setdefault(k, v) # type: ignore
|
||||
# assign step and parents
|
||||
metadata["step"] = self.step
|
||||
metadata["parents"] = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {})
|
||||
@@ -721,15 +719,10 @@ class PregelLoop(LoopProtocol):
|
||||
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:
|
||||
for k, v in self.config["metadata"].items():
|
||||
metadata.setdefault(k, v) # type: ignore
|
||||
|
||||
# create new checkpoint
|
||||
self.checkpoint = create_checkpoint(
|
||||
self.checkpoint, self.channels, self.step
|
||||
)
|
||||
self.checkpoint_metadata = metadata
|
||||
|
||||
self.prev_checkpoint_config = (
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import AsyncIterator, Iterator, Mapping, Union
|
||||
|
||||
from langgraph.channels.base import BaseChannel
|
||||
from langgraph.checkpoint.base import Checkpoint
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.managed.base import (
|
||||
ConfiguredManagedValue,
|
||||
ManagedValueMapping,
|
||||
@@ -37,7 +36,7 @@ def ChannelsManager(
|
||||
with ExitStack() as stack:
|
||||
yield (
|
||||
{
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k))
|
||||
for k, v in channel_specs.items()
|
||||
},
|
||||
ManagedValueMapping(
|
||||
@@ -91,7 +90,7 @@ async def AsyncChannelsManager(
|
||||
yield (
|
||||
# channels: enter each channel with checkpoint
|
||||
{
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k, MISSING))
|
||||
k: v.from_checkpoint(checkpoint["channel_values"].get(k))
|
||||
for k, v in channel_specs.items()
|
||||
},
|
||||
# managed: build mapping from spec to result
|
||||
|
||||
@@ -369,7 +369,7 @@ class LoopProtocol:
|
||||
self.stop = stop
|
||||
|
||||
|
||||
@dataclasses.dataclass(**_DC_KWARGS)
|
||||
@dataclasses.dataclass(**{**_DC_KWARGS, "frozen": False})
|
||||
class PregelScratchpad:
|
||||
# call
|
||||
call_counter: Callable[[], int]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.3.22"
|
||||
version = "0.3.21"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -6,14 +6,13 @@ import pytest
|
||||
from langgraph.channels.binop import BinaryOperatorAggregate
|
||||
from langgraph.channels.last_value import LastValue
|
||||
from langgraph.channels.topic import Topic
|
||||
from langgraph.constants import MISSING
|
||||
from langgraph.errors import EmptyChannelError, InvalidUpdateError
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
def test_last_value() -> None:
|
||||
channel = LastValue(int).from_checkpoint(MISSING)
|
||||
channel = LastValue(int).from_checkpoint(None)
|
||||
assert channel.ValueType is int
|
||||
assert channel.UpdateType is int
|
||||
|
||||
@@ -32,7 +31,7 @@ def test_last_value() -> None:
|
||||
|
||||
|
||||
def test_topic() -> None:
|
||||
channel = Topic(str).from_checkpoint(MISSING)
|
||||
channel = Topic(str).from_checkpoint(None)
|
||||
assert channel.ValueType is Sequence[str]
|
||||
assert channel.UpdateType is Union[str, list[str]]
|
||||
|
||||
@@ -56,7 +55,7 @@ def test_topic() -> None:
|
||||
|
||||
|
||||
def test_topic_accumulate() -> None:
|
||||
channel = Topic(str, accumulate=True).from_checkpoint(MISSING)
|
||||
channel = Topic(str, accumulate=True).from_checkpoint(None)
|
||||
assert channel.ValueType is Sequence[str]
|
||||
assert channel.UpdateType is Union[str, list[str]]
|
||||
|
||||
@@ -74,7 +73,7 @@ def test_topic_accumulate() -> None:
|
||||
|
||||
|
||||
def test_binop() -> None:
|
||||
channel = BinaryOperatorAggregate(int, operator.add).from_checkpoint(MISSING)
|
||||
channel = BinaryOperatorAggregate(int, operator.add).from_checkpoint(None)
|
||||
assert channel.ValueType is int
|
||||
assert channel.UpdateType is int
|
||||
|
||||
|
||||
@@ -1310,8 +1310,8 @@ def test_pending_writes_resume(
|
||||
},
|
||||
"channel_values": {
|
||||
"value": 1,
|
||||
"branch:to:one": None,
|
||||
"branch:to:two": None,
|
||||
"branch:to:one": "__start__",
|
||||
"branch:to:two": "__start__",
|
||||
},
|
||||
},
|
||||
metadata={
|
||||
@@ -1363,8 +1363,8 @@ def test_pending_writes_resume(
|
||||
parent_config=None,
|
||||
pending_writes=UnsortedSequence(
|
||||
(AnyStr(), "value", 1),
|
||||
(AnyStr(), "branch:to:one", None),
|
||||
(AnyStr(), "branch:to:two", None),
|
||||
(AnyStr(), "branch:to:one", "__start__"),
|
||||
(AnyStr(), "branch:to:two", "__start__"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -2146,8 +2146,8 @@ async def test_pending_writes_resume(
|
||||
},
|
||||
"channel_values": {
|
||||
"value": 1,
|
||||
"branch:to:one": None,
|
||||
"branch:to:two": None,
|
||||
"branch:to:one": "__start__",
|
||||
"branch:to:two": "__start__",
|
||||
},
|
||||
},
|
||||
metadata={
|
||||
@@ -2201,8 +2201,8 @@ async def test_pending_writes_resume(
|
||||
parent_config=None,
|
||||
pending_writes=UnsortedSequence(
|
||||
(AnyStr(), "value", 1),
|
||||
(AnyStr(), "branch:to:one", None),
|
||||
(AnyStr(), "branch:to:two", None),
|
||||
(AnyStr(), "branch:to:one", "__start__"),
|
||||
(AnyStr(), "branch:to:two", "__start__"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user