Compare commits

..
Author SHA1 Message Date
Rafid SaadandGitHub 9accc21f0f Merge branch 'main' into fix/add-assistant-id-to-auth-types 2026-02-05 11:49:57 -08:00
rafid saad ad75ffdd8d Add missing fields 2026-02-03 13:30:01 -08:00
rafid saad 85a311087d Add assistant ID 2026-02-03 13:25:56 -08:00
rafid saad cf5c64590b Merge branch 'main' of github.com:langchain-ai/langgraph into rafid/lsd-1115-add-patch-method 2026-02-03 10:53:04 -08:00
rafid saad fa1cf5bc0f Replace patch with update 2026-02-03 10:51:45 -08:00
rafid saad 36f94ba038 Merge branch 'rafid/lsd-1115-add-patch-method' of github.com:langchain-ai/langgraph into rafid/lsd-1115-add-patch-method 2026-02-03 10:50:40 -08:00
rafid saad eaf0d0cf10 Replace patch with update 2026-02-03 10:50:25 -08:00
William Fu-Hinthorn 4b19e2f51c merge 2026-02-03 10:39:50 -08:00
rafid saad a49d999ef9 add patch method 2026-02-03 09:10:55 -08:00
rafid saad e6f66c5949 Address linting issue 2026-02-02 17:59:15 -08:00
rafid saad a4a28e908d Address comments 2026-02-02 17:50:52 -08:00
rafid saad 7ff8141438 Merge branch 'rafid/lsd-1115-make-sdk-updates' of github.com:langchain-ai/langgraph into rafid/lsd-1115-make-sdk-updates 2026-02-02 17:43:52 -08:00
rafid saad 5ddcb3e27c Merge branch 'main' of github.com:langchain-ai/langgraph into rafid/lsd-1115-make-sdk-updates 2026-02-02 17:43:24 -08:00
rafid saad a2f5e8c558 feat: adding support for enabling/disabling cron in sdk-py 2026-02-02 17:43:12 -08:00
rafid saad 0a820b908b Fix linting issue 2026-02-02 17:42:08 -08:00
rafid saad 49299465c7 add test case 2026-02-02 17:34:38 -08:00
rafid saad ca9b3c6ff6 Add enabled to the client 2026-02-02 17:29:58 -08:00
rafid saad 65648e2019 Change is_enabled to enabled 2026-02-02 16:59:44 -08:00
rafid saad cfc595ac62 Update schema 2026-02-02 16:34:02 -08:00
19 changed files with 67 additions and 478 deletions
-2
View File
@@ -53,5 +53,3 @@ sdk-js (standalone)
```
Changes to a library may impact all of its dependents shown above.
- Do NOT use Sphinx-style double backtick formatting (` ``code`` `). Use single backticks (`` `code` ``) for inline code references in docstrings and comments.
-2
View File
@@ -53,5 +53,3 @@ sdk-js (standalone)
```
Changes to a library may impact all of its dependents shown above.
- Do NOT use Sphinx-style double backtick formatting (` ``code`` `). Use single backticks (`` `code` ``) for inline code references in docstrings and comments.
+1 -7
View File
@@ -1,10 +1,5 @@
from langgraph.constants import END, START
from langgraph.graph.message import (
MessageGraph,
MessagesState,
add_messages,
validate_messages_append_only,
)
from langgraph.graph.message import MessageGraph, MessagesState, add_messages
from langgraph.graph.state import StateGraph
__all__ = (
@@ -14,5 +9,4 @@ __all__ = (
"add_messages",
"MessagesState",
"MessageGraph",
"validate_messages_append_only",
)
-84
View File
@@ -31,7 +31,6 @@ __all__ = (
"MessagesState",
"MessageGraph",
"REMOVE_ALL_MESSAGES",
"validate_messages_append_only",
)
Messages = list[MessageLikeRepresentation] | MessageLikeRepresentation
@@ -245,89 +244,6 @@ def add_messages(
return merged
def validate_messages_append_only(
input: dict[str, Any], current_state: dict[str, Any]
) -> None:
"""Validates that incoming messages are append-only (no updates or removals).
This validator can be passed to `compile(validate_input=...)` to enforce that
external inputs only append new messages and never update or remove existing ones.
Internal node updates bypass this validation, allowing nodes to perform operations
like message compaction or cleanup without restriction.
Args:
input: The raw input dictionary being provided externally
current_state: The current state loaded from the checkpoint
Raises:
ValueError: If the input attempts to update or remove existing messages
Example:
```python
from langgraph.graph import StateGraph, MessagesState
from langgraph.graph.message import validate_messages_append_only
builder = StateGraph(MessagesState)
builder.add_node("chatbot", my_chatbot_node)
builder.set_entry_point("chatbot")
builder.set_finish_point("chatbot")
# Compile with validator to enforce append-only at the boundary
graph = builder.compile(validate_input=validate_messages_append_only)
# This works - adding new messages
graph.invoke({"messages": [("user", "Hello")]})
# This raises ValueError - trying to update existing message
graph.invoke({"messages": [HumanMessage(content="Modified", id="existing-id")]})
```
"""
# Only validate if input contains messages
if "messages" not in input:
return
# Get current messages from state
current_messages = current_state.get("messages", [])
# Build a set of existing message IDs
existing_ids: set[str] = set()
for msg in current_messages:
if hasattr(msg, "id") and msg.id is not None:
existing_ids.add(msg.id)
# Coerce input messages to list
input_messages = input["messages"]
if not isinstance(input_messages, list):
input_messages = [input_messages]
# Convert to messages to get proper IDs (let conversion errors propagate)
converted_messages = convert_to_messages(input_messages)
# Check each input message
for msg in converted_messages:
# Check for REMOVE_ALL_MESSAGES
if isinstance(msg, RemoveMessage) and msg.id == REMOVE_ALL_MESSAGES:
raise ValueError(
"Cannot remove all messages in append_only mode. "
"External inputs must only append new messages."
)
# Check for removals of existing messages
if isinstance(msg, RemoveMessage) and msg.id in existing_ids:
raise ValueError(
f"Cannot remove existing message with ID '{msg.id}' in append_only mode. "
"External inputs must only append new messages."
)
# Check for updates of existing messages
if not isinstance(msg, RemoveMessage) and msg.id and msg.id in existing_ids:
raise ValueError(
f"Cannot update existing message with ID '{msg.id}' in append_only mode. "
"External inputs must only append new messages."
)
@deprecated(
"MessageGraph is deprecated in langgraph 1.0.0, to be removed in 2.0.0. Please use StateGraph with a `messages` key instead.",
category=None,
-30
View File
@@ -1042,7 +1042,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
interrupt_after: All | list[str] | None = None,
debug: bool = False,
name: str | None = None,
validate_input: Callable[[Any, dict[str, Any]], None] | None = None,
) -> CompiledStateGraph[StateT, ContextT, InputT, OutputT]:
"""Compiles the `StateGraph` into a `CompiledStateGraph` object.
@@ -1075,34 +1074,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
interrupt_after: An optional list of node names to interrupt after.
debug: A flag indicating whether to enable debug mode.
name: The name to use for the compiled graph.
validate_input: Optional validation function for external inputs.
The function receives `(input, current_state)` and should raise an
exception if the input is invalid, or return normally if valid.
This validation only applies to external updates (from `invoke()`, `stream()`,
`update_state()`, or `Command.update`) - internal node updates bypass this
validation, allowing trusted nodes to perform privileged operations.
Example:
```python
from langgraph.graph import StateGraph
from langgraph.graph.message import validate_messages_append_only
builder = StateGraph(MessagesState)
# ... add nodes ...
# Use built-in validator
graph = builder.compile(
validate_input=validate_messages_append_only
)
# Or custom validator
def my_validator(input: dict, current_state: dict) -> None:
if "forbidden_key" in input:
raise ValueError("Forbidden key detected")
graph = builder.compile(validate_input=my_validator)
```
Returns:
CompiledStateGraph: The compiled `StateGraph`.
@@ -1163,7 +1134,6 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
store=store,
cache=cache,
name=name or "LangGraph",
validate_input=validate_input,
)
compiled.attach_node(START, None)
-21
View File
@@ -226,7 +226,6 @@ class PregelLoop:
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
retry_policy: Sequence[RetryPolicy] = (),
cache_policy: CachePolicy | None = None,
validate_input: Callable[[Any, dict[str, Any]], None] | None = None,
) -> None:
self.stream = stream
self.config = config
@@ -251,7 +250,6 @@ class PregelLoop:
self.retry_policy = retry_policy
self.cache_policy = cache_policy
self.durability = durability
self.validate_input = validate_input
if self.stream is not None and CONFIG_KEY_STREAM in config[CONF]:
self.stream = DuplexStream(self.stream, config[CONF][CONFIG_KEY_STREAM])
scratchpad: PregelScratchpad | None = config[CONF].get(CONFIG_KEY_SCRATCHPAD)
@@ -639,14 +637,6 @@ class PregelLoop:
# map command to writes
if isinstance(self.input, Command):
# Validate command update if validator is provided
if self.validate_input is not None and self.input.update:
current_state = read_channels(self.channels, self.output_keys)
try:
self.validate_input(self.input.update, current_state)
except Exception as e:
raise ValueError(f"Input validation failed: {str(e)}") from e
if (resume := self.input.resume) is not None:
if not self.checkpointer:
raise RuntimeError(
@@ -701,13 +691,6 @@ class PregelLoop:
)
# map inputs to channel updates
elif input_writes := deque(map_input(input_keys, self.input)):
# Validate input if validator is provided
if self.validate_input is not None:
current_state = read_channels(self.channels, self.output_keys)
try:
self.validate_input(self.input, current_state)
except Exception as e:
raise ValueError(f"Input validation failed: {str(e)}") from e
# discard any unfinished tasks from previous checkpoint
discard_tasks = prepare_next_tasks(
self.checkpoint,
@@ -1002,7 +985,6 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
retry_policy: Sequence[RetryPolicy] = (),
cache_policy: CachePolicy | None = None,
validate_input: Callable[[Any, dict[str, Any]], None] | None = None,
) -> None:
super().__init__(
input,
@@ -1024,7 +1006,6 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
retry_policy=retry_policy,
cache_policy=cache_policy,
durability=durability,
validate_input=validate_input,
)
self.stack = ExitStack()
if checkpointer:
@@ -1180,7 +1161,6 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
migrate_checkpoint: Callable[[Checkpoint], None] | None = None,
retry_policy: Sequence[RetryPolicy] = (),
cache_policy: CachePolicy | None = None,
validate_input: Callable[[Any, dict[str, Any]], None] | None = None,
) -> None:
super().__init__(
input,
@@ -1202,7 +1182,6 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
retry_policy=retry_policy,
cache_policy=cache_policy,
durability=durability,
validate_input=validate_input,
)
self.stack = AsyncExitStack()
if checkpointer:
+24 -24
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
from collections.abc import AsyncIterator, Callable, Iterator, Sequence
from dataclasses import fields, is_dataclass
from typing import (
Any,
TypeVar,
@@ -12,7 +11,6 @@ from uuid import UUID, uuid4
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.messages import BaseMessage
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, LLMResult
from pydantic import BaseModel
from langgraph._internal._constants import NS_SEP
from langgraph.constants import TAG_HIDDEN, TAG_NOSTREAM
@@ -28,17 +26,6 @@ T = TypeVar("T")
Meta = tuple[tuple[str, ...], dict[str, Any]]
def _state_values(obj: Any) -> Sequence[Any]:
"""Extract top-level field values from a state object (dict, BaseModel, or dataclass)."""
if isinstance(obj, dict):
return list(obj.values())
elif isinstance(obj, BaseModel):
return [getattr(obj, k) for k in type(obj).model_fields]
elif is_dataclass(obj) and not isinstance(obj, type):
return [getattr(obj, f.name) for f in fields(obj)]
return ()
class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
"""A callback handler that implements stream_mode=messages.
@@ -103,14 +90,26 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
for value in response:
if isinstance(value, BaseMessage):
self._emit(meta, value, dedupe=True)
else:
for value in _state_values(response):
elif isinstance(response, dict):
for value in response.values():
if isinstance(value, BaseMessage):
self._emit(meta, value, dedupe=True)
elif isinstance(value, Sequence):
for item in value:
if isinstance(item, BaseMessage):
self._emit(meta, item, dedupe=True)
elif hasattr(response, "__dir__") and callable(response.__dir__):
for key in dir(response):
try:
value = getattr(response, key)
if isinstance(value, BaseMessage):
self._emit(meta, value, dedupe=True)
elif isinstance(value, Sequence):
for item in value:
if isinstance(item, BaseMessage):
self._emit(meta, item, dedupe=True)
except AttributeError:
pass
def tap_output_aiter(
self, run_id: UUID, output: AsyncIterator[T]
@@ -204,15 +203,16 @@ class StreamMessagesHandler(BaseCallbackHandler, _StreamingCallbackHandler):
if not self.subgraphs and len(ns) > 0:
return
self.metadata[run_id] = (ns, metadata)
for value in _state_values(inputs):
if isinstance(value, BaseMessage):
if value.id is not None:
self.seen.add(value.id)
elif isinstance(value, Sequence) and not isinstance(value, str):
for item in value:
if isinstance(item, BaseMessage):
if item.id is not None:
self.seen.add(item.id)
if isinstance(inputs, dict):
for key, value in inputs.items():
if isinstance(value, BaseMessage):
if value.id is not None:
self.seen.add(value.id)
elif isinstance(value, Sequence) and not isinstance(value, str):
for item in value:
if isinstance(item, BaseMessage):
if item.id is not None:
self.seen.add(item.id)
def on_chain_end(
self,
+2 -2
View File
@@ -565,7 +565,7 @@ def _call(
if fut := next(
(
f
for f, t in list(futures().items()) # type: ignore[union-attr]
for f, t in futures().items() # type: ignore[union-attr]
if t is not None and t == next_task.id
),
None,
@@ -708,7 +708,7 @@ async def _acall_impl(
if fut := next(
(
f
for f, t in list(futures().items()) # type: ignore[union-attr]
for f, t in futures().items() # type: ignore[union-attr]
if t is not None and t == next_task.id
),
None,
-4
View File
@@ -652,7 +652,6 @@ class Pregel(
config: RunnableConfig | None = None,
trigger_to_nodes: Mapping[str, Sequence[str]] | None = None,
name: str = "LangGraph",
validate_input: Callable[[Any, dict[str, Any]], None] | None = None,
**deprecated_kwargs: Unpack[DeprecatedKwargs],
) -> None:
if (
@@ -699,7 +698,6 @@ class Pregel(
self.config = config
self.trigger_to_nodes = trigger_to_nodes or {}
self.name = name
self.validate_input = validate_input
if auto_validate:
self.validate()
@@ -2601,7 +2599,6 @@ class Pregel(
migrate_checkpoint=self._migrate_checkpoint,
retry_policy=self.retry_policy,
cache_policy=self.cache_policy,
validate_input=self.validate_input,
) as loop:
# create runner
runner = PregelRunner(
@@ -2911,7 +2908,6 @@ class Pregel(
migrate_checkpoint=self._migrate_checkpoint,
retry_policy=self.retry_policy,
cache_policy=self.cache_policy,
validate_input=self.validate_input,
) as loop:
# create runner
runner = PregelRunner(
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph"
version = "1.0.8"
version = "1.0.7"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
requires-python = ">=3.10"
+2 -6
View File
@@ -20,7 +20,7 @@ from langgraph.graph.message import REMOVE_ALL_MESSAGES, MessagesState, push_mes
from langgraph.graph.state import StateGraph
from tests.messages import _AnyIdHumanMessage
CORE_MAJOR, CORE_MINOR, CORE_PATCH = (
_, CORE_MINOR, CORE_PATCH = (
int("".join(c for c in v if c.isdigit()))
for v in langchain_core.__version__.split(".")
)
@@ -205,11 +205,7 @@ def test_messages_state(state_schema):
@pytest.mark.skipif(
condition=not (
(CORE_MAJOR == 0 and CORE_MINOR == 3 and CORE_PATCH >= 11)
or (CORE_MAJOR == 0 and CORE_MINOR > 3)
or CORE_MAJOR > 0
),
condition=not ((CORE_MINOR == 3 and CORE_PATCH >= 11) or CORE_MINOR > 3),
reason="Requires langchain_core>=0.3.11.",
)
def test_messages_state_format_openai():
+10 -99
View File
@@ -16,7 +16,7 @@ from typing import Annotated, Any, Literal, get_type_hints
import pytest
from langchain_core.language_models import GenericFakeChatModel
from langchain_core.messages import AIMessage, AnyMessage, HumanMessage
from langchain_core.messages import AnyMessage
from langchain_core.runnables import (
RunnableConfig,
RunnableLambda,
@@ -1264,20 +1264,18 @@ def test_imp_task(
}
thread1 = {"configurable": {"thread_id": "1"}}
result = [*graph.stream([0, 1], thread1, durability=durability)]
# mapper tasks run concurrently so output order is non-deterministic
assert sorted(result[:-1], key=lambda d: str(d)) == [
assert [*graph.stream([0, 1], thread1, durability=durability)] == [
{"mapper": "00"},
{"mapper": "11"},
{
"__interrupt__": (
Interrupt(
value="question",
id=AnyStr(),
),
)
},
]
assert result[-1] == {
"__interrupt__": (
Interrupt(
value="question",
id=AnyStr(),
),
)
}
assert mapper_calls == 2
assert graph.invoke(Command(resume="answer"), thread1, durability=durability) == [
@@ -6980,93 +6978,6 @@ def test_stream_messages_dedupe_state(sync_checkpointer: BaseCheckpointSaver) ->
assert chunks[0][1]["langgraph_node"] == "call_model"
def test_stream_messages_dedupe_pydantic_subgraph_interrupt(
sync_checkpointer: BaseCheckpointSaver,
) -> None:
"""Pydantic BaseModel state should not cause duplicate messages when
streaming from subgraphs that use interrupts. Regression test for a bug
where ``on_chain_start`` only populated the ``seen`` set for dict inputs,
skipping Pydantic model inputs entirely."""
class PydanticState(BaseModel):
messages: Annotated[list[AnyMessage], add_messages] = Field(
default_factory=list
)
def subgraph_proposal(state) -> Command[Literal["subgraph_approval"]]:
return Command(
goto="subgraph_approval",
update={"messages": [AIMessage(content="Proposal", id="proposal_msg")]},
)
def subgraph_approval(state) -> Command[Literal["__end__"]]:
resume_value = interrupt({"message": "Waiting for approval"})
user_msg = resume_value.get("user_message", "")
msgs = [HumanMessage(content=user_msg)] if user_msg else []
return Command(goto="__end__", update={"messages": msgs})
subgraph = (
StateGraph(PydanticState)
.add_node("proposal", subgraph_proposal)
.add_node("subgraph_approval", subgraph_approval)
.add_edge(START, "proposal")
.compile(checkpointer=sync_checkpointer)
)
def finalize(state) -> Command[Literal["__end__"]]:
return Command(
goto="__end__",
update={"messages": [AIMessage(content="Finalized", id="finalize_msg")]},
)
graph = (
StateGraph(PydanticState)
.add_node("subgraph", subgraph)
.add_node("finalize", finalize)
.add_edge(START, "subgraph")
.add_edge("subgraph", "finalize")
.compile(checkpointer=sync_checkpointer)
)
thread1 = {"configurable": {"thread_id": "1"}}
# First stream: should hit interrupt after proposal
chunks_req0 = [
(ns, chunk)
for ns, chunk in graph.stream(
{"messages": [HumanMessage(content="Create a proposal")]},
thread1,
stream_mode="messages",
subgraphs=True,
)
]
msg_ids_req0 = {chunk[0].id for _, chunk in chunks_req0}
assert "proposal_msg" in msg_ids_req0
# Verify interrupted
state = graph.get_state(thread1)
assert state.next
# Second stream: resume — should NOT duplicate messages from first stream
chunks_req1 = [
(ns, chunk)
for ns, chunk in graph.stream(
Command(resume={"user_message": "Yes"}),
thread1,
stream_mode="messages",
subgraphs=True,
)
]
msg_ids_req1 = {chunk[0].id for _, chunk in chunks_req1}
assert "finalize_msg" in msg_ids_req1
# The key assertion: no message IDs from request 0 should appear in request 1
duplicates = msg_ids_req0 & msg_ids_req1
assert not duplicates, f"Duplicate message IDs across requests: {duplicates}"
def test_interrupt_subgraph_reenter_checkpointer_true(
sync_checkpointer: BaseCheckpointSaver,
) -> None:
+9 -11
View File
@@ -2262,20 +2262,18 @@ async def test_imp_task(
tracer = FakeTracer()
thread1 = {"configurable": {"thread_id": "1"}, "callbacks": [tracer]}
result = [c async for c in graph.astream([0, 1], thread1, durability=durability)]
# mapper tasks run concurrently so output order is non-deterministic
assert sorted(result[:-1], key=lambda d: str(d)) == [
assert [c async for c in graph.astream([0, 1], thread1, durability=durability)] == [
{"mapper": "00"},
{"mapper": "11"},
{
"__interrupt__": (
Interrupt(
value="question",
id=AnyStr(),
),
)
},
]
assert result[-1] == {
"__interrupt__": (
Interrupt(
value="question",
id=AnyStr(),
),
)
}
assert mapper_calls == 2
assert len(tracer.runs) == 1
assert len(tracer.runs[0].child_runs) == 1
@@ -1,173 +0,0 @@
"""Tests for validate_input with callable approach."""
import pytest
from langchain_core.messages import AIMessage, HumanMessage, RemoveMessage
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph
from langgraph.graph.message import MessagesState, validate_messages_append_only
from langgraph.types import Command
def test_validate_messages_append_only_allows_new():
"""Test that validate_messages_append_only allows adding new messages."""
def chatbot(state: MessagesState) -> MessagesState:
return {"messages": [AIMessage(content="Response")]}
builder = StateGraph(MessagesState)
builder.add_node("chatbot", chatbot)
builder.set_entry_point("chatbot")
builder.set_finish_point("chatbot")
graph = builder.compile(validate_input=validate_messages_append_only)
result = graph.invoke({"messages": [HumanMessage(content="Hello", id="1")]})
assert len(result["messages"]) == 2
def test_validate_messages_append_only_blocks_updates():
"""Test that validate_messages_append_only blocks message updates."""
def chatbot(state: MessagesState) -> MessagesState:
return {"messages": [AIMessage(content="Response")]}
builder = StateGraph(MessagesState)
builder.add_node("chatbot", chatbot)
builder.set_entry_point("chatbot")
builder.set_finish_point("chatbot")
checkpointer = MemorySaver()
graph = builder.compile(
checkpointer=checkpointer, validate_input=validate_messages_append_only
)
config = {"configurable": {"thread_id": "test"}}
graph.invoke({"messages": [HumanMessage(content="Hello", id="1")]}, config)
# Try to update - should fail
with pytest.raises(ValueError, match="Cannot update existing message"):
graph.invoke({"messages": [HumanMessage(content="Modified", id="1")]}, config)
def test_validate_messages_append_only_blocks_removals():
"""Test that validate_messages_append_only blocks removals."""
def chatbot(state: MessagesState) -> MessagesState:
return {"messages": []}
builder = StateGraph(MessagesState)
builder.add_node("chatbot", chatbot)
builder.set_entry_point("chatbot")
builder.set_finish_point("chatbot")
checkpointer = MemorySaver()
graph = builder.compile(
checkpointer=checkpointer, validate_input=validate_messages_append_only
)
config = {"configurable": {"thread_id": "test"}}
graph.invoke({"messages": [HumanMessage(content="Hello", id="1")]}, config)
# Try to remove - should fail
with pytest.raises(ValueError, match="Cannot remove existing message"):
graph.invoke({"messages": [RemoveMessage(id="1")]}, config)
def test_validate_with_command_update():
"""Test that Command.update is also validated."""
def chatbot(state: MessagesState) -> MessagesState:
return {"messages": [AIMessage(content="Response")]}
builder = StateGraph(MessagesState)
builder.add_node("chatbot", chatbot)
builder.set_entry_point("chatbot")
builder.set_finish_point("chatbot")
checkpointer = MemorySaver()
graph = builder.compile(
checkpointer=checkpointer, validate_input=validate_messages_append_only
)
config = {"configurable": {"thread_id": "test"}}
graph.invoke({"messages": [HumanMessage(content="Hello", id="1")]}, config)
# Try to update via Command - should fail
with pytest.raises(ValueError, match="Cannot update existing message"):
graph.invoke(
Command(update={"messages": [HumanMessage(content="Modified", id="1")]}),
config,
)
def test_custom_validator():
"""Test with custom validation function."""
def my_validator(input: dict, current_state: dict) -> None:
if "forbidden" in str(input):
raise ValueError("Forbidden content")
def node(state: MessagesState) -> MessagesState:
return {"messages": [AIMessage(content="OK")]}
builder = StateGraph(MessagesState)
builder.add_node("node", node)
builder.set_entry_point("node")
builder.set_finish_point("node")
graph = builder.compile(validate_input=my_validator)
# Should work
graph.invoke({"messages": [("user", "Hello")]})
# Should fail
with pytest.raises(ValueError, match="Forbidden content"):
graph.invoke({"messages": [("user", "forbidden word")]})
def test_node_bypasses_validation():
"""Test that internal node operations bypass validation."""
def node_that_modifies(state: MessagesState) -> MessagesState:
# Node modifies existing message - should work even with strict validator
messages = state["messages"]
if messages:
updated = messages[0].model_copy(update={"content": "Modified by node"})
return {"messages": [updated]}
return {"messages": []}
builder = StateGraph(MessagesState)
builder.add_node("modifier", node_that_modifies)
builder.set_entry_point("modifier")
builder.set_finish_point("modifier")
graph = builder.compile(validate_input=validate_messages_append_only)
# Node can modify - validation only applies to external input
result = graph.invoke({"messages": [HumanMessage(content="Original", id="1")]})
assert any("Modified by node" in m.content for m in result["messages"])
def test_without_validator_allows_everything():
"""Test backwards compatibility - without validator, everything is allowed."""
def chatbot(state: MessagesState) -> MessagesState:
return {"messages": []}
builder = StateGraph(MessagesState)
builder.add_node("chatbot", chatbot)
builder.set_entry_point("chatbot")
builder.set_finish_point("chatbot")
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer) # No validator
config = {"configurable": {"thread_id": "test"}}
graph.invoke({"messages": [HumanMessage(content="Hello", id="1")]}, config)
# Should allow update (old behavior)
result = graph.invoke(
{"messages": [HumanMessage(content="Modified", id="1")]}, config
)
assert result["messages"][0].content == "Modified"
+1 -1
View File
@@ -1372,7 +1372,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.0.8"
version = "1.0.7"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -308,13 +308,7 @@ def create_react_agent(
) -> CompiledStateGraph:
"""Creates an agent graph that calls tools in a loop until a stopping condition is met.
!!! warning
This function is deprecated in favor of
[`create_agent`][langchain.agents.create_agent] from the `langchain`
package, which provides an equivalent agent factory with a flexible
middleware system. For migration guidance, see
[Migrating from LangGraph v0](https://docs.langchain.com/oss/python/migrate/langgraph-v1).
For more details on using `create_react_agent`, visit [Agents](https://langchain-ai.github.io/langgraph/agents/overview/) documentation.
Args:
model: The language model for the agent. Supports static and dynamic
+1 -1
View File
@@ -268,7 +268,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.0.8"
version = "1.0.7"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
+1 -1
View File
@@ -3,6 +3,6 @@ from langgraph_sdk.client import get_client, get_sync_client
from langgraph_sdk.encryption import Encryption
from langgraph_sdk.encryption.types import EncryptionContext
__version__ = "0.3.4"
__version__ = "0.3.3"
__all__ = ["Auth", "Encryption", "EncryptionContext", "get_client", "get_sync_client"]
+14 -2
View File
@@ -737,16 +737,21 @@ class CronsCreate(typing.TypedDict, total=False):
```python
create_params = {
"assistant_id": UUID("123e4567-e89b-12d3-a456-426614173999")
"payload": {"key": "value"},
"schedule": "0 0 * * *",
"cron_id": UUID("123e4567-e89b-12d3-a456-426614174000"),
"thread_id": UUID("123e4567-e89b-12d3-a456-426614174001"),
"user_id": "user123",
"end_time": datetime(2024, 3, 16, 10, 0, 0)
"end_time": datetime(2024, 3, 16, 10, 0, 0),
"enabled": true,
}
```
"""
assistant_id: UUID
"""Unique identifier for the assistant."""
payload: dict[str, typing.Any]
"""Payload for the cron job."""
@@ -765,6 +770,9 @@ class CronsCreate(typing.TypedDict, total=False):
end_time: datetime | None
"""typing.Optional end time for the cron job."""
enabled: bool | None
"""typing.Optional enabled status of the cron job."""
class CronsDelete(typing.TypedDict):
"""Payload for deleting a cron job.
@@ -807,7 +815,8 @@ class CronsUpdate(typing.TypedDict, total=False):
update_params = {
"cron_id": UUID("123e4567-e89b-12d3-a456-426614174000"),
"payload": {"key": "value"},
"schedule": "0 0 * * *"
"schedule": "0 0 * * *",
"enabled": true
}
```
"""
@@ -821,6 +830,9 @@ class CronsUpdate(typing.TypedDict, total=False):
schedule: str | None
"""typing.Optional schedule to update."""
enabled: bool | None
"""typing.Optional enabled status of the cron job."""
class CronsSearch(typing.TypedDict, total=False):
"""Payload for searching cron jobs.