mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-29 11:19:54 +02:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
582fb11dd4 | ||
|
|
39d9cdbef0 | ||
|
|
6dacd1aabe |
Generated
+3
-3
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
+2
-2
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user