LangChain LangGraph
Module 1 Module 2 Module 3
Module 3 · Middleware and HITL
1 3.2 Managing Msgs
2 3.3 HITL
3 3.4 Dyn Models
4 3.4 Dyn Prompts
5 3.4 Dyn Tools
6 3.5 Email Agent
SUM Module Summary
HomeLangChainModule 3 · Middleware and HITL3.5 Email Agent
📓 Notebook 📖 Explained
LANGCHAIN MODULE 3 · LESSON 3.5

Email Agent Integrated Project
Auth · Dynamic Tools · HITL

The capabilities of Put Module 3 in this section are combined: you can only log in when you are not authenticated, you can read the inbox and draft a reply after authentication, and you need manual approval before actually sending the email.

1 Project goal and flow

This notebook is a comprehensive project of Module 3. The Agent must first authenticate the User account, check the inbox after successful authentication, then draft the email according to the User account's instructions, and finally wait for human approval before sending.

1. Auth

User submits email address and password.

2. Tools unlock

After successful authentication, the inbox and email sending tool will be opened.

3. Inbox

Agent reads recent emails.

4. HITL

Wait for approval before sending email.

2 Context stores valid credentials; State stores authentication status

The correct email address and password are passed in through the runtime context; whether it has been authenticated is written into the Agent state. In this way, subsequent calls to Use by the same thread can know that the User user has logged in.

Python
@dataclass
class EmailContext:
    email_address: str = "julie@example.com"
    password: str = "password123"

class AuthenticatedState(AgentState):
    authenticated: bool
DataWhere it goesReason
Correct email/passwordRuntime ContextProvided by an external system and does not require long-term storage of the model
authenticatedAgent StateAuthentication results must be retained across the same thread and subsequent steps.

3 Define authentication, inbox lookup and send-email tools

authenticate will compare the User input and runtime context. If successful, return Command(update=...) Put authenticated and write state.

Python
@tool
def check_inbox() -> str:
    """Check the inbox for recent emails"""
    return """
    Hi Julie,
    I'm going to be in town next week and was wondering if we could grab a coffee?
    - best, Jane (jane@example.com)
    """

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an response email"""
    return f"Email sent to {to} with subject {subject} and body {body}"

@tool
def authenticate(email: str, password: str, runtime: ToolRuntime) -> Command:
    """Authenticate the user with the given email and password"""
    if email == runtime.context.email_address and password == runtime.context.password:
        return Command(update={
            "authenticated": True,
            "messages": [ToolMessage("Successfully authenticated", tool_call_id=runtime.tool_call_id)],
        })
    else:
        return Command(update={
            "authenticated": False,
            "messages": [ToolMessage("Authentication failed", tool_call_id=runtime.tool_call_id)],
        })
Authentication result writes state

Authentication does not just return a sentence, but changes the status of Agent's subsequent use of tools and prompts.

4 Dynamic tools: expose different tools before and after authentication

Only Use authenticate is allowed to be adjusted before authentication. Open check_inbox and send_email after authentication. This is the practical version of Dynamic Tool Permissions.

Python
@wrap_model_call
def dynamic_tool_call(
    request: ModelRequest,
    handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
    """Allow read inbox and send email tools only if user provides correct email and password"""
    authenticated = request.state.get("authenticated")

    if authenticated:
        tools = [check_inbox, send_email]
    else:
        tools = [authenticate]

    request = request.override(tools=tools)
    return handler(request)
Do not rely only on the prompt

If unauthenticated Use users can still see the send_email tool, there is a risk of misadjusting Use. Capabilities are limited here from the tool list level.

5 Dynamic prompt: switch role instructions before and after authentication

Prompt also follows the change of authentication status: when not authenticated, it only acts as an authentication assistant; after authentication, it becomes an assistant that can check the inbox and send emails.

Python
from langchain.agents.middleware import dynamic_prompt

authenticated_prompt = """You are a helpful assistant that can check the inbox and send emails.
Your first step after authentication is to check the inbox."""
unauthenticated_prompt = "You are a helpful assistant that can authenticate users."

@dynamic_prompt
def dynamic_prompt(request: ModelRequest) -> str:
    """Generate system prompt based on authentication status"""
    authenticated = request.state.get("authenticated")

    if authenticated:
        return authenticated_prompt
    else:
        return unauthenticated_prompt
Tools and prompts work together

Tools control "what can be done", prompts control "how should be done". When the authentication status changes, both change simultaneously.

6 Add human approval before sending email

Even if the Use account has been authenticated, sending emails still has an external side effect of Use, sosend_emailContinue to be under HITL control.

Python
humanInTheLoopMiddleware = HumanInTheLoopMiddleware(
    interrupt_on={
        "authenticate": False,
        "check_inbox": False,
        "send_email": True,
    },
    description_prefix="Tool execution requires approval",
)

agent = create_agent(
    deepseek_model(),
    tools=[authenticate, check_inbox, send_email],
    checkpointer=InMemorySaver(),
    state_schema=AuthenticatedState,
    context_schema=EmailContext,
    middleware=[dynamic_tool_call, dynamic_prompt, humanInTheLoopMiddleware],
)
Layered protection

Authentication controls tool visibility, and HITL controls high-risk tool execution. The two address different risks.

7 Run: log in, draft and approve

The first time it calls Use to submit the credentials; the second time it lets the Agent draft a reply; when it Prepares to send the email, it returns__interrupt__. After approval through, Use the same thread to resume execution.

Python
config = {"configurable": {"thread_id": "1"}}

response = agent.invoke(
    {"messages": [HumanMessage(content="julie@example.com, password123")]},
    context=EmailContext(),
    config=config,
)

response = agent.invoke(
    {"messages": [HumanMessage(content="any draft is fine. don't check back.")]},
    context=EmailContext(),
    config=config,
)

print(response["__interrupt__"][0].value["action_requests"][0]["args"]["body"])

response = agent.invoke(
    Command(resume={"decisions": [{"type": "approve"}]}),
    config=config,
)
Lesson summary

This Agent also uses runtime context, state, dynamic tools, dynamic prompt, HITL and checkpointer, which is a complete closed loop of Module 3.