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.
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.
User submits email address and password.
After successful authentication, the inbox and email sending tool will be opened.
Agent reads recent emails.
Wait for approval before sending email.
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.
@dataclass
class EmailContext:
email_address: str = "julie@example.com"
password: str = "password123"
class AuthenticatedState(AgentState):
authenticated: bool| Data | Where it goes | Reason |
|---|---|---|
| Correct email/password | Runtime Context | Provided by an external system and does not require long-term storage of the model |
| authenticated | Agent State | Authentication results must be retained across the same thread and subsequent steps. |
authenticate will compare the User input and runtime context. If successful, return Command(update=...) Put authenticated and write state.
@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 does not just return a sentence, but changes the status of Agent's subsequent use of tools and prompts.
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.
@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)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.
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.
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_promptTools control "what can be done", prompts control "how should be done". When the authentication status changes, both change simultaneously.
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.
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],
)Authentication controls tool visibility, and HITL controls high-risk tool execution. The two address different risks.
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.
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,
)This Agent also uses runtime context, state, dynamic tools, dynamic prompt, HITL and checkpointer, which is a complete closed loop of Module 3.