Edit and respond
Approve and reject are two of four decisions. A reviewer can also change a call before it runs, or answer in the tool's place.
Lesson 23's agent, asked for two refunds in one message, pauses once with two actions.
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
from shop_model import ShopModel
from tools import lookup_order, refund_order
approval = HumanInTheLoopMiddleware(interrupt_on={"refund_order": True})
agent = create_agent(ShopModel(), tools=[lookup_order, refund_order],
middleware=[approval], checkpointer=InMemorySaver())thread = {"configurable": {"thread_id": "two-refunds"}}
ask = {"messages": [{"role": "user", "content": "Please refund A17 and C40"}]}
result = agent.invoke(ask, thread, version="v2")
for action in result.interrupts[0].value["action_requests"]:
print(action["name"], action["args"])One pause, two actions waiting: a reviewer answers both in one go, in the order they are listed.
One decision per call, in order
decision = Command(resume={"decisions": [{"type": "approve"}]})
agent.invoke(decision, thread, version="v2")Two actions were waiting and one decision was sent, so the invoke raises. The list has to be as long as the list of actions, and in the same order.
Two calls are waiting, so one decision is refused. Decisions are matched to actions by position: the first decision is for the first action.
Editing a call
The customer meant C41, not C40. An edit decision replaces the call's arguments before it runs.
fixed = {"name": "refund_order", "args": {"order_id": "C41"}}
decisions = [{"type": "approve"}, {"type": "edit", "edited_action": fixed}]
result = agent.invoke(Command(resume={"decisions": decisions}), thread, version="v2")
for message in result.value["messages"]:
if message.type == "tool":
print(message.text.splitlines()[-1])A17 was refunded as asked and C41 instead of C40. The tool message for the edited call also tells the model that a person replaced its call, so it does not try the original again. Edit conservatively: a large change can make the model rethink its plan.
Answering in the tool's place
respond skips the tool and returns the reviewer's text as its result. It is for tools whose real answer is a person, such as a warehouse check. Configured with a dictionary, interrupt_on also limits which decisions are allowed.
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
from shop_model import ShopModel
from tools import lookup_order
ask_warehouse = HumanInTheLoopMiddleware(interrupt_on={"lookup_order": {"allowed_decisions": ["respond"]}})
agent = create_agent(ShopModel(), tools=[lookup_order], middleware=[ask_warehouse],
checkpointer=InMemorySaver())thread = {"configurable": {"thread_id": "warehouse"}}
agent.invoke({"messages": [{"role": "user", "content": "Where is A17?"}]}, thread, version="v2")
reply = {"type": "respond", "message": "A17 is on the van, arriving today."}
result = agent.invoke(Command(resume={"decisions": [reply]}), thread, version="v2")
print(result.value["messages"][-1].text)The model treats a respond message as a successful tool result. That is why the documentation warns against using it to turn down a refund: the model would believe the refund happened. Use reject for that.
- Send a
rejectdecision to the warehouse agent and read the error. - Edit the A17 refund to C41 and approve the C40 one, and check the order of the results.
- Allow both
respondandapprovefor the lookup and approve it instead.
This is what real progress feels like.