Asking a human first
Some tools should not run on a model's word alone, such as a refund. HumanInTheLoopMiddleware pauses the agent before the call and waits for a person to decide.
The shop adds a second tool to tools.py. The stand-in from lesson 6 already asks for it when a message mentions a refund.
@tool
def refund_order(order_id: str) -> str:
"""Refund an order in full. This cannot be undone."""
return f"Refunded {order_id}."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())interrupt_on names the tools that need a decision; True means every call to that tool pauses. lookup_order is not listed, so it runs as before. The checkpointer is required: a paused agent is saved, and the decision may come minutes or days later.
Pick one to watch it run, step by step.
The pause
thread = {"configurable": {"thread_id": "ravi-refund"}}
ask = {"messages": [{"role": "user", "content": "Please refund A17"}]}
result = agent.invoke(ask, thread, version="v2")
print(result.interrupts[0].value["action_requests"])
print(result.value["messages"][-1].tool_calls[0]["name"])Invoking with version="v2" returns an object with two parts. value is the state so far, ending with the model's request to refund A17. interrupts holds what is waiting for a decision: the tool, its arguments and a description a reviewer can read. The refund has not run.
Approving
decision = Command(resume={"decisions": [{"type": "approve"}]})
result = agent.invoke(decision, thread, version="v2")
for message in result.value["messages"][2:]:
print(f"{message.type:<4} {message.text}")A Command with resume continues the same thread, and decisions holds one decision per paused call. After the approval the refund ran and the model answered.
Rejecting
decision = Command(resume={"decisions": [{"type": "reject", "message": "Refunds need a manager."}]})
result = agent.invoke(decision, thread, version="v2")
for message in result.value["messages"][2:]:
print(f"{message.type:<4} {message.text}")A rejection never runs the tool. The model gets a tool message with the reason instead, so it can tell the customer why. Without a message, the middleware writes a default one telling the model not to try the same call again.
- Ask about A17 without a refund and check that the agent does not pause.
- Print
result.interrupts[0].value["review_configs"]to see which decisions are allowed. - After the pause, print
agent.get_state(thread).nextto see where the run is waiting.
Slow is fine. Stopping is the only problem.