LangChainLangChain 1.4 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
43 small wins to finish your pathNext lesson

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.

Exampletools.py, below lookup_order
@tool
def refund_order(order_id: str) -> str:
    """Refund an order in full. This cannot be undone."""
    return f"Refunded {order_id}."
Exampleagent.py
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.

Where a person decides
Please refund A17the customerThe model asksrefund_orderThe run pausessaved by the checkpointerA person decidesapprove, edit, rejectThe refund runsor never does
Hover or tap a piece to see what it is and which lesson built it.
Follow a refund

Pick one to watch it run, step by step.

The pause

Example
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

Example
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

Example
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.

Try it yourself
  • 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).next to see where the run is waiting.

Slow is fine. Stopping is the only problem.