Compare commits

..
6 changed files with 108 additions and 9 deletions
+3 -3
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "annotated-types"
@@ -324,7 +324,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.0"
version = "2.0.2"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1116,4 +1116,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0,<4.0"
content-hash = "8f763cd1727287f8c8b5ad2b4d8df00fb446e68d0cd4e88c278e4007969b83fd"
content-hash = "6bd85ce8ee1192995c1ff03d5fa65af8ee7872214d71b84559a6192cadf82be6"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-postgres"
version = "2.0.1"
version = "2.0.2"
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0,<4.0"
langgraph-checkpoint = "^2.0.0"
langgraph-checkpoint = "^2.0.2"
orjson = ">=3.10.1"
psycopg = "^3.0.0"
psycopg-pool = "^3.0.0"
+2 -2
View File
@@ -332,7 +332,7 @@ typing-extensions = ">=4.7"
[[package]]
name = "langgraph-checkpoint"
version = "2.0.0"
version = "2.0.2"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "^3.9.0,<4.0"
@@ -1001,4 +1001,4 @@ watchmedo = ["PyYAML (>=3.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.9.0"
content-hash = "e0091cc2deab4de99a6bc4eb262b0040b771a9659dd3638ac1c4a225a1f11dc2"
content-hash = "927b49b9ba72a301980237d7adc2e73cdacfbe127a174c7488136a9af9372796"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-checkpoint-sqlite"
version = "2.0.0"
version = "2.0.1"
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
authors = []
license = "MIT"
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
[tool.poetry.dependencies]
python = "^3.9.0"
langgraph-checkpoint = "^2.0.0"
langgraph-checkpoint = "^2.0.2"
aiosqlite = "^0.20.0"
[tool.poetry.group.dev.dependencies]
@@ -36,6 +36,7 @@ from langchain_core.tools import BaseTool, InjectedToolArg
from langchain_core.tools import tool as create_tool
from typing_extensions import Annotated, get_args, get_origin
from langgraph.errors import GraphInterrupt
from langgraph.store.base import BaseStore
from langgraph.utils.runnable import RunnableCallable
@@ -267,6 +268,14 @@ class ToolNode(RunnableCallable):
Union[str, list], msg_content_output(tool_message.content)
)
return tool_message
# GraphInterrupt is a special exception that will always be raised.
# It can be triggered in the following scenarios:
# (1) a NodeInterrupt is raised inside a tool
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
except GraphInterrupt as e:
raise e
except Exception as e:
if isinstance(self.handle_tool_errors, tuple):
handled_types: tuple = self.handle_tool_errors
@@ -300,6 +309,14 @@ class ToolNode(RunnableCallable):
Union[str, list], msg_content_output(tool_message.content)
)
return tool_message
# GraphInterrupt is a special exception that will always be raised.
# It can be triggered in the following scenarios:
# (1) a NodeInterrupt is raised inside a tool
# (2) a NodeInterrupt is raised inside a graph node for a graph called as a tool
# (3) a GraphInterrupt is raised when a subgraph is interrupted inside a graph called as a tool
# (2 and 3 can happen in a "supervisor w/ tools" multi-agent architecture)
except GraphInterrupt as e:
raise e
except Exception as e:
if isinstance(self.handle_tool_errors, tuple):
handled_types: tuple = self.handle_tool_errors
+82
View File
@@ -37,6 +37,8 @@ from pydantic.v1 import ValidationError as ValidationErrorV1
from typing_extensions import TypedDict
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.errors import NodeInterrupt
from langgraph.graph import START, MessagesState, StateGraph, add_messages
from langgraph.prebuilt import (
ToolNode,
@@ -52,6 +54,7 @@ from langgraph.prebuilt.tool_node import (
)
from langgraph.store.base import BaseStore
from langgraph.store.memory import InMemoryStore
from langgraph.types import Interrupt
from tests.conftest import (
ALL_CHECKPOINTERS_ASYNC,
ALL_CHECKPOINTERS_SYNC,
@@ -834,6 +837,85 @@ def test_tool_node_incorrect_tool_name():
assert tool_message.tool_call_id == "some 0"
def test_tool_node_node_interrupt():
def tool_normal(some_val: int) -> str:
"""Tool docstring."""
return "normal"
def tool_interrupt(some_val: int) -> str:
"""Tool docstring."""
raise NodeInterrupt("foo")
def handle(e: NodeInterrupt):
return "handled"
for handle_tool_errors in (True, (NodeInterrupt,), "handled", handle, False):
node = ToolNode([tool_interrupt], handle_tool_errors=handle_tool_errors)
with pytest.raises(NodeInterrupt) as exc_info:
node.invoke(
{
"messages": [
AIMessage(
"hi?",
tool_calls=[
{
"name": "tool_interrupt",
"args": {"some_val": 0},
"id": "some 0",
}
],
)
]
}
)
assert exc_info.value == "foo"
# test inside react agent
model = FakeToolCallingModel(
tool_calls=[
[
ToolCall(name="tool_interrupt", args={"some_val": 0}, id="1"),
ToolCall(name="tool_normal", args={"some_val": 1}, id="2"),
],
[],
]
)
checkpointer = MemorySaver()
config = {"configurable": {"thread_id": "1"}}
agent = create_react_agent(
model, [tool_interrupt, tool_normal], checkpointer=checkpointer
)
result = agent.invoke({"messages": [HumanMessage("hi?")]}, config)
assert result["messages"] == [
_AnyIdHumanMessage(
content="hi?",
),
AIMessage(
content="hi?",
id="0",
tool_calls=[
{
"name": "tool_interrupt",
"args": {"some_val": 0},
"id": "1",
"type": "tool_call",
},
{
"name": "tool_normal",
"args": {"some_val": 1},
"id": "2",
"type": "tool_call",
},
],
),
]
state = agent.get_state(config)
assert state.next == ("tools",)
task = state.tasks[0]
assert task.name == "tools"
assert task.interrupts == (Interrupt(value="foo", when="during"),)
def my_function(some_val: int, some_other_val: str) -> str:
return f"{some_val} - {some_other_val}"