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

The rules around it

The tools are the easy part. What makes it a desk you could put in front of customers is the four pieces of middleware around the model.

The guardrail ends the run before a model call when someone types the word password.

Exampleguard.py
from langchain.agents.middleware import before_agent
from langchain.messages import AIMessage


@before_agent(can_jump_to=["end"])
def no_passwords(state, runtime):
    if "password" in state["messages"][-1].text.lower():
        answer = AIMessage("I cannot help with passwords. Please use the reset link.")
        return {"messages": [answer], "jump_to": "end"}

The rest are LangChain's own. The list reads top to bottom as the order things happen in.

Exampledesk.py
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware, ModelCallLimitMiddleware, PIIMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from desk_model import DeskModel
from guard import no_passwords
from search import search_policies
from tools import Customer, lookup_order, refund_order

agent = create_agent(
    DeskModel(),
    tools=[lookup_order, refund_order, search_policies],
    context_schema=Customer,
    middleware=[
        no_passwords,
        PIIMiddleware("credit_card", strategy="mask"),
        ModelCallLimitMiddleware(run_limit=6),
        HumanInTheLoopMiddleware(interrupt_on={"refund_order": True}),
    ],
    checkpointer=InMemorySaver(),
)
  • no_passwords answers and ends the run, so a password question never reaches the model.
  • PIIMiddleware masks a card number in the message before the model or the checkpointer sees it.
  • ModelCallLimitMiddleware caps one run at six model calls, so a loop stops on its own.
  • HumanInTheLoopMiddleware pauses on refund_order and waits to be resumed.
  • InMemorySaver keeps each thread, so a customer's next message continues the last one.

Sending one message

A refund pauses the run, so the caller has to be able to answer. say sends one message as one customer, and approves anything the desk holds.

Examplechat.py
from langgraph.types import Command
from desk import Customer, agent


def say(who, text, thread):
    config = {"configurable": {"thread_id": thread}}
    result = agent.invoke({"messages": [{"role": "user", "content": text}]}, config,
                          context=Customer(who), version="v2")
    if result.interrupts:
        print(f"{who}: {text}\n  paused for approval: {result.interrupts[0].value['action_requests'][0]['args']}")
        result = agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config,
                              context=Customer(who), version="v2")
        text = "(approved)"
    print(f"{who}: {text}\n  desk: {result.value['messages'][-1].text}")
Example
from chat import say

say("ravi", "What is my password?", "ravi-0")
say("ravi", "My card 4111 1111 1111 1111 was charged. Where is A17?", "ravi-1")

The password question was answered by the guardrail with no model call at all. The card question reached the model with the number already masked, and the answer is about the order.

Try it yourself
  • Move no_passwords below the PII middleware and ask both questions again.
  • Set run_limit=1 and ask about an order.
  • Add "lookup_order": True to interrupt_on and see what pauses.

This is what real progress feels like.