This commit is contained in:
Sydney Runkle
2025-04-22 10:34:24 -07:00
parent 9a7b1fa12a
commit 35523f4081
10 changed files with 45 additions and 40 deletions
+1 -1
View File
@@ -182,7 +182,7 @@ class Graph:
# validate the condition
if name in self.branches[source]:
raise ValueError(
f"Branch with name `{path.name}` already exists for node " f"`{source}`"
f"Branch with name `{path.name}` already exists for node `{source}`"
)
# save it
self.branches[source][name] = Branch.from_path(path, path_map, then, False)
+1 -1
View File
@@ -527,7 +527,7 @@ class StateGraph(Graph):
# validate the condition
if name in self.branches[source]:
raise ValueError(
f"Branch with name `{path.name}` already exists for node " f"`{source}`"
f"Branch with name `{path.name}` already exists for node `{source}`"
)
# save it
self.branches[source][name] = Branch.from_path(path, path_map, then, True)
+16 -12
View File
@@ -108,6 +108,7 @@ from langgraph.store.base import BaseStore
from langgraph.types import (
All,
Checkpointer,
Interrupt,
LoopProtocol,
StateSnapshot,
StateUpdate,
@@ -2749,17 +2750,15 @@ class Pregel(PregelProtocol):
**kwargs,
):
if stream_mode == "values":
if isinstance(chunk, dict) and (ints := chunk.get(INTERRUPT)) is not None:
interrupts.extend(ints)
if isinstance(chunk, dict):
if (ints := chunk.get(INTERRUPT)) is not None:
interrupts.extend(ints)
latest = chunk
else:
chunks.append(chunk)
if stream_mode == "values":
if len(interrupts) > 0:
return {
INTERRUPT: interrupts
}
return latest
return {INTERRUPT: interrupts} if interrupts else latest
else:
return chunks
@@ -2794,10 +2793,11 @@ class Pregel(PregelProtocol):
"""
output_keys = output_keys if output_keys is not None else self.output_channels
if stream_mode == "values":
latest: Union[dict[str, Any], Any] = None
else:
chunks = []
latest: Union[dict[str, Any], Any] = None
chunks: list[Union[dict[str, Any], Any]] = []
interrupts: list[Interrupt] = []
async for chunk in self.astream(
input,
config,
@@ -2810,11 +2810,15 @@ class Pregel(PregelProtocol):
**kwargs,
):
if stream_mode == "values":
if isinstance(chunk, dict):
if (ints := chunk.get(INTERRUPT)) is not None:
interrupts.extend(ints)
latest = chunk
else:
chunks.append(chunk)
if stream_mode == "values":
return latest
return {INTERRUPT: interrupts} if interrupts else latest
else:
return chunks
+1 -3
View File
@@ -916,9 +916,7 @@ class PregelLoop(LoopProtocol):
v
for w in writes
if w[0] == INTERRUPT
for v in (
w[1] if isinstance(w[1], Sequence) else (w[1],)
)
for v in (w[1] if isinstance(w[1], Sequence) else (w[1],))
)
}
]
+6 -6
View File
@@ -245,12 +245,12 @@ class PregelNode(Runnable):
)
def join(self, channels: Sequence[str]) -> PregelNode:
assert isinstance(channels, list) or isinstance(
channels, tuple
), "channels must be a list or tuple"
assert isinstance(
self.channels, dict
), "all channels must be named when using .join()"
assert isinstance(channels, list) or isinstance(channels, tuple), (
"channels must be a list or tuple"
)
assert isinstance(self.channels, dict), (
"all channels must be named when using .join()"
)
return self.copy(
update=dict(
channels={
+6 -5
View File
@@ -1,5 +1,7 @@
import dataclasses
import hashlib
import sys
import uuid
from collections import deque
from typing import (
TYPE_CHECKING,
@@ -24,8 +26,6 @@ from typing_extensions import Self
from langgraph.checkpoint.base import BaseCheckpointSaver, CheckpointMetadata
from langgraph.utils.fields import get_update_as_tuples
import hashlib
import uuid
if TYPE_CHECKING:
from langgraph.pregel.protocol import PregelProtocol
@@ -145,11 +145,12 @@ class Interrupt:
ns: Optional[Sequence[str]] = None
when: Literal["during"] = dataclasses.field(default="during", repr=False)
@property
def interrupt_id(self) -> str:
"""Generate a unique ID for the interrupt based on its namespace."""
identifier = uuid.uuid4().bytes if self.ns is None else ''.join(self.ns).encode()
identifier = (
uuid.uuid4().bytes if self.ns is None else "".join(self.ns).encode()
)
return hashlib.sha256(identifier).hexdigest()
@@ -487,12 +488,12 @@ def interrupt(value: Any) -> Any:
GraphInterrupt: On the first invocation within the node, halts execution and surfaces the provided value to the client.
"""
from langgraph.constants import (
CONF,
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_SCRATCHPAD,
CONFIG_KEY_SEND,
NS_SEP,
RESUME,
CONF,
)
from langgraph.errors import GraphInterrupt
from langgraph.utils.config import get_config
@@ -1573,9 +1573,9 @@ def test_migrate_checkpoints(source: str, target: str) -> None:
migrated["versions_seen"][c][v].split(".")[0]
)
# check that the migrated checkpoint matches the target checkpoint
assert (
migrated == target_checkpoint.checkpoint
), "Checkpoint mismatch at index {}".format(idx)
assert migrated == target_checkpoint.checkpoint, (
"Checkpoint mismatch at index {}".format(idx)
)
@NEEDS_CONTEXTVARS
+3 -3
View File
@@ -2829,9 +2829,9 @@ def test_state_graph_packets(
# Define decision-making logic
def should_continue(data: dict) -> str:
assert isinstance(data["session"], httpx.Client)
assert (
data["something_extra"] == "hi there"
), "nodes can pass extra data to their cond edges, which isn't saved in state"
assert data["something_extra"] == "hi there", (
"nodes can pass extra data to their cond edges, which isn't saved in state"
)
# Logic to decide whether to continue in the loop or exit
if tool_calls := data["messages"][-1].tool_calls:
return [Send("tools", tool_call) for tool_call in tool_calls]
@@ -3805,7 +3805,7 @@ async def test_in_one_fan_out_out_one_graph_state() -> None:
docs: Annotated[list[str], operator.add]
async def rewrite_query(data: State) -> State:
return {"query": f'query: {data["query"]}'}
return {"query": f"query: {data['query']}"}
async def retriever_one(data: State) -> State:
await asyncio.sleep(0.1)
+7 -5
View File
@@ -226,9 +226,10 @@ def test_graph_with_jitter_retry_policy():
)
# Test graph execution with mocked random and sleep
with patch("random.uniform", return_value=0.05) as mock_random, patch(
"time.sleep"
) as mock_sleep:
with (
patch("random.uniform", return_value=0.05) as mock_random,
patch("time.sleep") as mock_sleep,
):
result = graph.invoke({"foo": ""})
# Verify retry behavior
@@ -334,8 +335,9 @@ def test_graph_with_max_attempts_exceeded():
)
# Test graph execution
with patch("time.sleep") as mock_sleep, pytest.raises(
ValueError, match="Always fails"
with (
patch("time.sleep") as mock_sleep,
pytest.raises(ValueError, match="Always fails"),
):
graph.invoke({"foo": ""})