mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
192 lines
5.6 KiB
Python
192 lines
5.6 KiB
Python
import pytest
|
|
from langchain_core.messages import ToolMessage
|
|
from langchain_core.runnables import RunnableConfig
|
|
|
|
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
from langgraph.prebuilt import create_react_agent
|
|
from langgraph.prebuilt.interrupt import HumanInterruptConfig, InterruptToolNode
|
|
from langgraph.types import Command
|
|
from tests.model import FakeToolCallingModel
|
|
|
|
|
|
def hello_tool(name: str) -> str:
|
|
"""Return a greeting for the provided person."""
|
|
return f"Hello, {name}!"
|
|
|
|
|
|
post_model_hook = InterruptToolNode(
|
|
hello_tool=HumanInterruptConfig(
|
|
allow_accept=True,
|
|
allow_edit=True,
|
|
allow_ignore=True,
|
|
allow_respond=True,
|
|
)
|
|
)
|
|
|
|
default_model = FakeToolCallingModel(
|
|
tool_calls=[
|
|
[
|
|
{
|
|
"name": "hello_tool",
|
|
"args": {"name": "lady gaga"},
|
|
"id": "some-random-id",
|
|
}
|
|
]
|
|
]
|
|
)
|
|
|
|
|
|
def test_interrupt_surfaced(
|
|
request: pytest.FixtureRequest,
|
|
sync_checkpointer: BaseCheckpointSaver,
|
|
) -> None:
|
|
agent = create_react_agent(
|
|
default_model,
|
|
[hello_tool],
|
|
checkpointer=sync_checkpointer,
|
|
post_model_hook=post_model_hook,
|
|
)
|
|
config: RunnableConfig = {"configurable": {"thread_id": "1"}}
|
|
result = agent.invoke({"messages": [("user", "Say hi to lady gaga!")]}, config)
|
|
|
|
interrupt_data = result["__interrupt__"]
|
|
assert interrupt_data[0].value == [
|
|
{
|
|
"action_request": {"action": "hello_tool", "args": {"name": "lady gaga"}},
|
|
"config": {
|
|
"allow_accept": True,
|
|
"allow_edit": True,
|
|
"allow_ignore": True,
|
|
"allow_respond": True,
|
|
},
|
|
"description": "Please review tool call for `hello_tool` before execution.",
|
|
}
|
|
]
|
|
|
|
response = agent.invoke(Command(resume={"type": "accept"}), config=config)
|
|
tool_message: ToolMessage = response["messages"][-2]
|
|
assert tool_message.content == "Hello, lady gaga!"
|
|
assert tool_message.name == "hello_tool"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"resume, expected_content",
|
|
[
|
|
({"type": "accept"}, "Hello, lady gaga!"),
|
|
(
|
|
{"type": "ignore"},
|
|
"User ignored the tool call for `hello_tool` with id some-random-id",
|
|
),
|
|
(
|
|
{
|
|
"type": "edit",
|
|
"args": {"action": "hello_tool", "args": {"name": "bruno mars"}},
|
|
},
|
|
"Hello, bruno mars!",
|
|
),
|
|
],
|
|
)
|
|
def test_interrupt_resume_variants(
|
|
request: pytest.FixtureRequest,
|
|
sync_checkpointer: BaseCheckpointSaver,
|
|
resume: dict,
|
|
expected_content: str,
|
|
) -> None:
|
|
agent = create_react_agent(
|
|
default_model,
|
|
[hello_tool],
|
|
checkpointer=sync_checkpointer,
|
|
post_model_hook=post_model_hook,
|
|
)
|
|
|
|
config: RunnableConfig = {"configurable": {"thread_id": "1"}}
|
|
agent.invoke({"messages": [("user", "Say hi to lady gaga!")]}, config)
|
|
|
|
response = agent.invoke(Command(resume=resume), config=config)
|
|
tool_message: ToolMessage = response["messages"][-2]
|
|
assert tool_message.name == "hello_tool"
|
|
assert tool_message.content == expected_content
|
|
|
|
if resume["type"] == "edit":
|
|
ai_msg = response["messages"][-1]
|
|
assert ai_msg.tool_calls == [
|
|
{
|
|
"name": "hello_tool",
|
|
"args": {"name": "lady gaga"},
|
|
"id": "some-random-id",
|
|
"type": "tool_call",
|
|
}
|
|
]
|
|
|
|
|
|
def test_resume_with_response(
|
|
request: pytest.FixtureRequest,
|
|
sync_checkpointer: BaseCheckpointSaver,
|
|
) -> None:
|
|
model = FakeToolCallingModel(
|
|
tool_calls=[
|
|
[
|
|
{
|
|
"name": "hello_tool",
|
|
"args": {"name": "lady gaga"},
|
|
"id": "some-random-id",
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"name": "hello_tool",
|
|
"args": {"name": "bruno mars"},
|
|
"id": "some-random-id-2",
|
|
}
|
|
],
|
|
]
|
|
)
|
|
|
|
agent = create_react_agent(
|
|
model,
|
|
[hello_tool],
|
|
checkpointer=sync_checkpointer,
|
|
post_model_hook=post_model_hook,
|
|
)
|
|
|
|
config: RunnableConfig = {"configurable": {"thread_id": "1"}}
|
|
agent.invoke({"messages": [("user", "Say hi to lady gaga!")]}, config)
|
|
|
|
# Provide user response
|
|
agent.invoke(
|
|
Command(
|
|
resume={
|
|
"type": "response",
|
|
"args": "actually, please say hello to bruno mars",
|
|
}
|
|
),
|
|
config=config,
|
|
)
|
|
|
|
# Accept the updated call
|
|
response = agent.invoke(Command(resume={"type": "accept"}), config=config)
|
|
|
|
assert len(response["messages"]) == 6
|
|
tool_message: ToolMessage = response["messages"][-2]
|
|
assert tool_message.name == "hello_tool"
|
|
assert tool_message.content == "Hello, bruno mars!"
|
|
|
|
|
|
def test_resume_with_type_not_allowed(sync_checkpointer: BaseCheckpointSaver) -> None:
|
|
agent = create_react_agent(
|
|
default_model,
|
|
[hello_tool],
|
|
checkpointer=sync_checkpointer,
|
|
post_model_hook=post_model_hook,
|
|
)
|
|
config: RunnableConfig = {"configurable": {"thread_id": "1"}}
|
|
agent.invoke({"messages": [("user", "Say hi to lady gaga!")]}, config)
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
agent.invoke(Command(resume={"type": "not-allowed"}), config=config)
|
|
|
|
assert (
|
|
str(exc_info.value)
|
|
== "Unexpected human response: {'type': 'not-allowed'}. Expected one with `'type'` in ['accept', 'edit', 'response', 'ignore'] based on hello_tool's interrupt configuration."
|
|
)
|