Compare commits

..
Author SHA1 Message Date
William Fu-Hinthorn 1af6b812c4 Cycles 2025-03-28 14:29:07 -07:00
30 changed files with 118 additions and 311 deletions
+1 -1
View File
@@ -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:
-26
View File
@@ -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_)
-8
View File
@@ -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,
)
-3
View File
@@ -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 -1
View File
@@ -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"
-4
View File
@@ -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"""
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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}
+5 -13
View File
@@ -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
+4 -14
View File
@@ -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."""
+9 -13
View File
@@ -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
+7 -14
View File
@@ -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
+2 -2
View File
@@ -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
)
+7 -1
View File
@@ -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,
),
},
),
+40 -76
View File
@@ -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", []),
)
+5 -12
View File
@@ -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 = (
+2 -3
View File
@@ -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
+1 -1
View File
@@ -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 -1
View File
@@ -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"
+4 -5
View File
@@ -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
+4 -4
View File
@@ -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__"),
),
)
+4 -4
View File
@@ -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__"),
),
)