Files
2025-08-12 21:15:07 -07:00

5274 lines
382 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"cells": [
{
"cell_type": "markdown",
"id": "d4b42c9b-4c82-4e14-a83e-f05019cad09c",
"metadata": {},
"source": [
"# LangGraph 101: Building An Ambient Email Assistant"
]
},
{
"cell_type": "markdown",
"id": "8af264ca-490b-4d02-9fa1-7b8d4b24d1e0",
"metadata": {},
"source": [
"In this notebook, we're going to build an ambient email assistant from scratch. We will start from a simple router agent and add additional steps into the workflow, showcasing concepts including human-in-the-loop and memory. \n",
"\n",
"![overview-img](img/overview.png)\n",
"\n",
"For a deeper dive into LangGraph primitives and learning our framework, check out our [LangChain Academy](https://academy.langchain.com/courses/intro-to-langgraph)!\n",
"\n",
"\n",
"\n",
"\n",
"For this notebook, we will be using [agent-inbox](https://github.com/langchain-ai/agent-inbox) to visualize our email interactions, which characterize emails based on their thread status. \n",
"\n",
"![overview-inbox](img/inbox_1.png)"
]
},
{
"cell_type": "markdown",
"id": "e42c9eab-56e0-4805-861d-6e12453fce34",
"metadata": {},
"source": [
"## Pre-work: Setup"
]
},
{
"cell_type": "markdown",
"id": "66219ab6-5649-457c-8c29-70f4484ecb3e",
"metadata": {},
"source": [
"To start, let's load our environment variables from our .env file. Make sure all of the keys necessary in .env.example are included!\n",
"We use OpenAI in this example, but feel free to swap ChatOpenAI with other model providers that you prefer. "
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "c387c33f-4361-43c1-8397-b1168c567145",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from dotenv import load_dotenv\n",
"\n",
"load_dotenv(dotenv_path=\"../.env\", override=True)"
]
},
{
"cell_type": "markdown",
"id": "8d327b61-90ab-462a-b99c-4467e3330024",
"metadata": {},
"source": [
"### Setting up LLM"
]
},
{
"cell_type": "markdown",
"id": "2596d311-2046-4d11-ac10-629f8f512bbc",
"metadata": {},
"source": [
"We have added options to use several LLMs below, including OpenAI, AzureOpenAI, and Bedrock. You can run the cell corresponding to your model to set up."
]
},
{
"cell_type": "markdown",
"id": "b834e282-e946-483d-8b38-fcb87438ab21",
"metadata": {},
"source": [
"#### OpenAI"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "3fc9da45-ab5a-4f70-beb6-1f4bc4978bd3",
"metadata": {},
"outputs": [],
"source": [
"from langchain_openai import ChatOpenAI\n",
"\n",
"llm = ChatOpenAI(model=\"gpt-4.1\") # or your model"
]
},
{
"cell_type": "markdown",
"id": "5de0038c-5886-474c-8fb5-1b1bd2fcba01",
"metadata": {},
"source": [
"#### Anthropic "
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "db83573b-553b-42e9-8912-137f27c61f58",
"metadata": {},
"outputs": [],
"source": [
"# from langchain_anthropic import ChatAnthropic\n",
"\n",
"# llm = ChatAnthropic(model=\"claude-3-7-sonnet-20250219\") # or your model"
]
},
{
"cell_type": "markdown",
"id": "444e4045-c066-4820-b89f-1f78b29fb681",
"metadata": {},
"source": [
"#### AWS Bedrock"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "bf9fafe8-7b12-4bae-9a46-e40a044f36a2",
"metadata": {},
"outputs": [],
"source": [
"# import os \n",
"# from langchain_aws import ChatBedrock\n",
"\n",
"# llm = ChatBedrock(model_id=\"us.anthropic.claude-3-7-sonnet-20250219-v1:0\") # or your model_id "
]
},
{
"cell_type": "markdown",
"id": "99e62fc0-7a84-404e-9c19-bd767e28c8a7",
"metadata": {},
"source": [
"#### Azure OpenAI"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "d1ee20f1-fcce-4286-a873-a3e952dc9619",
"metadata": {},
"outputs": [],
"source": [
"# from langchain_openai import AzureChatOpenAI\n",
"\n",
"# llm = AzureChatOpenAI(\n",
"# azure_deployment=\"gpt-4o\", # or your deployment\n",
"# api_version=\"2025-04-01-preview\", # or your api version\n",
"# )"
]
},
{
"cell_type": "markdown",
"id": "08f1e994-a927-4327-86c9-f56afbceefe1",
"metadata": {},
"source": [
"### Setting up Short Term Memory"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "ade571e1-63ae-46bf-b351-e8c210fbca52",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.checkpoint.memory import InMemorySaver\n",
"\n",
"checkpointer = InMemorySaver()"
]
},
{
"cell_type": "markdown",
"id": "1a857926-7608-4001-86f2-9e62207f3d8b",
"metadata": {},
"source": [
"## Part I: Tool-calling React-Agent"
]
},
{
"cell_type": "markdown",
"id": "76ff2075-d05d-4533-9f38-c0438ea0ef32",
"metadata": {},
"source": [
"Now that we are set up, we are ready to build out our **tool-calling agent**. This is a ReAct-style agent that based on the incoming email, invokes relevant tools to perform the task. \n",
"\n",
"![agent_workflow_img](img/react_email.png)"
]
},
{
"cell_type": "markdown",
"id": "6d965852-61f8-4955-81ee-ddb05648c2c7",
"metadata": {},
"source": [
"### State"
]
},
{
"cell_type": "markdown",
"id": "776d280f-4545-4ec5-aff3-07bb4a32038d",
"metadata": {},
"source": [
"How does information flow through the steps? \n",
"\n",
"State is the first LangGraph concept we'll cover. When defining a Graph, you must pass in a schema for State. The State schema serves as the input schema for all Nodes and Edges in the graph. \n",
"\n",
"**State can be thought of as the memory of the agent - its a shared data structure thats passed on between the nodes of your graph and the information that you want to track over time**, representing the current snapshot of your application. \n",
"\n",
"For our email agent, we'll use LangGraph's pre-built [`MessagesState` object](https://langchain-ai.github.io/langgraph/concepts/low_level/#messagesstate), which is a just dictionary with a `messages` key that appends messages returned by nodes [as its update logic](https://langchain-ai.github.io/langgraph/concepts/low_level/#reducers). LangGraph also gives you flexibility to track other information. We'll define a custom `State` object that extends `MessagesState` and adds a `classification_decision` key: "
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "c6cd3371-fe0b-431a-a120-f4205ab07bd3",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import MessagesState\n",
"from typing import Literal\n",
"\n",
"class State(MessagesState):\n",
" # We can add a specific key to our state for the email input\n",
" email_input: dict\n",
" classification_decision: Literal[\"ignore\", \"respond\", \"notify\"]"
]
},
{
"cell_type": "markdown",
"id": "7534abcc-647a-4a3e-a9f5-71f956372a76",
"metadata": {},
"source": [
"### Tools\n",
"\n",
"[Tools](https://python.langchain.com/docs/concepts/tools/) are **utilities that can be called by a chat model and can act as extension of the LLM's capabilities**. In LangChain, creating tools can be done using the `@tool` decorator, which transforms Python functions into callable tools. It will automatically infer the tool's name, description, and expected arguments from the function definition. You can also use [Model Context Protocol (MCP) servers](https://github.com/langchain-ai/langchain-mcp-adapters) as LangChain-compatible tools. \n",
"\n",
"\n",
"In our case, we have defined a number of placeholder tools that have capabilities incl. writing emails, scheduling meeting, checking calendar availabilities, and sending emails. We have also added a Question tool in preparation for our later HITL step, allowing the assistant to ask the user a question."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "b287d846-8fad-4064-bda4-544e8b6d5585",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"/Users/qiaocatherine/git/ambient-agent-101\n"
]
}
],
"source": [
"%cd ..\n",
"%load_ext autoreload\n",
"%autoreload 2\n",
"\n",
"from datetime import datetime\n",
"from pydantic import BaseModel, Field\n",
"from langchain_core.tools import tool\n",
"\n",
"from langgraph.types import interrupt, Command\n",
"from email_assistant.utils import parse_email, format_for_display, format_email_markdown\n",
"\n",
"# Agent tools \n",
"@tool\n",
"def write_email(to: str, subject: str, content: str) -> str:\n",
" \"\"\"Write and send an email.\"\"\"\n",
" # Placeholder response - in real app would send email\n",
" return f\"Email sent to {to} with subject '{subject}' and content: {content}\"\n",
"\n",
"@tool\n",
"def schedule_meeting(\n",
" attendees: list[str], subject: str, duration_minutes: int, preferred_day: datetime, start_time: int\n",
") -> str:\n",
" \"\"\"Schedule a calendar meeting.\"\"\"\n",
" # Placeholder response - in real app would check calendar and schedule\n",
" date_str = preferred_day.strftime(\"%A, %B %d, %Y\")\n",
" return f\"Meeting '{subject}' scheduled on {date_str} at {start_time} for {duration_minutes} minutes with {len(attendees)} attendees\"\n",
"\n",
"@tool\n",
"def check_calendar_availability(day: str) -> str:\n",
" \"\"\"Check calendar availability for a given day.\"\"\"\n",
" # Placeholder response - in real app would check actual calendar\n",
" return f\"Available times on {day}: 9:00 AM, 2:00 PM, 4:00 PM\"\n",
"\n",
"@tool\n",
"# This is new! \n",
"class Question(BaseModel):\n",
" \"\"\"Question to ask user.\"\"\"\n",
" content: str\n",
" \n",
"@tool\n",
"class Done(BaseModel):\n",
" \"\"\"E-mail has been sent.\"\"\"\n",
" done: bool\n",
"\n",
"# All tools available to the agent\n",
"tools = [\n",
" write_email, \n",
" schedule_meeting, \n",
" check_calendar_availability, \n",
" Question, \n",
" Done,\n",
"]\n",
"\n",
"tools_by_name = {tool.name: tool for tool in tools}"
]
},
{
"cell_type": "markdown",
"id": "b7cb4bcc-a388-4dcc-86d7-5ab7b8857c7b",
"metadata": {},
"source": [
"### Nodes"
]
},
{
"cell_type": "markdown",
"id": "6e294c32-7ba1-40c6-b207-6f3a2274509b",
"metadata": {},
"source": [
"Now that we have a list of tools, we are ready to build nodes that interact with them. \n",
"\n",
"Nodes are just python (or JS/TS!) functions. Nodes take in your graph's State as input, execute some logic, and return a new State. \n",
"\n",
"Here, we're just going to set up 2 nodes for our ReAct agent:\n",
"1. **LLM Node**: Reasoning node that decides which tool to invoke by calling the LLM. \n",
"2. **Tool Node**: Node that contains all the available tools and executes the function. "
]
},
{
"cell_type": "markdown",
"id": "0cf4fe24-2ee5-4b97-b6f8-5931cdc5a212",
"metadata": {},
"source": [
"#### Tool Node"
]
},
{
"cell_type": "markdown",
"id": "f7a5c22c-af3c-4f26-aaf5-16d135243830",
"metadata": {},
"source": [
"LangGraph has a pre-built [ToolNode](https://langchain-ai.github.io/langgraph/how-tos/tool-calling/#toolnode) that you can leverage. You can also build your own tool handler node as below - this is helpful in scenarios when you want to add additional custom logic, such as implementing interrupt before certain tool calls. "
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "a6f882ac-e6e3-4a71-81e0-b9fbf4dec771",
"metadata": {},
"outputs": [],
"source": [
"def tool_handler(state: State):\n",
" \"\"\"Performs the tool call.\"\"\"\n",
"\n",
" # List for tool messages\n",
" result = []\n",
" \n",
" # Iterate through tool calls\n",
" for tool_call in state[\"messages\"][-1].tool_calls:\n",
" # Get the tool\n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" # Run it\n",
" observation = tool.invoke(tool_call[\"args\"])\n",
" # Create a tool message\n",
" result.append({\"role\": \"tool\", \"content\" : observation, \"tool_call_id\": tool_call[\"id\"]})\n",
" \n",
" # Add it to our messages\n",
" return {\"messages\": result}"
]
},
{
"cell_type": "markdown",
"id": "2cb76c99-b95e-47f5-9cd4-02376cbb4d28",
"metadata": {},
"source": [
"#### LLM Node"
]
},
{
"cell_type": "markdown",
"id": "e0d7c595-a128-4b91-9729-0ea66fc8e30f",
"metadata": {},
"source": [
"Here, we define the LLM decision-making node. This node takes in the current state, calls the LLM, and updates `messages` with the LLM output. \n",
"\n",
"We [enforce tool use](https://python.langchain.com/docs/how_to/tool_choice/) by setting `tool_choice=\"any\"`.\n",
"\n",
"First, let's import a set of prompt utilities. \n",
"\n",
"> **Note:** here we change to the parent directory (`%cd ..`) to access our project's module structure, which contains reusable prompts and components. The autoreload extensions ensure any changes to these modules are automatically reflected in the notebook without requiring kernel restarts. This allows us to organize our prompts in a dedicated module rather than defining them inline, making them easier to maintain and reuse across the notebooks! You can see all these files in: `src/email_assistant`\n"
]
},
{
"cell_type": "markdown",
"id": "9c1184c5-dd31-4551-bc2b-ad807f4dc108",
"metadata": {},
"source": [
"We call `bind_tools` to our llm, which connects tools to models. The method is used to specify which tools are available for a model to call and ensures the model has access to all the tool metadata "
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "4adfc419-e95c-407d-bd80-468ee485222e",
"metadata": {},
"outputs": [],
"source": [
"# Initialize the LLM, enforcing tool use\n",
"llm_with_tools = llm.bind_tools(tools, tool_choice=\"any\")"
]
},
{
"cell_type": "markdown",
"id": "9da2187d-7446-4182-a46d-8aa9fa492626",
"metadata": {},
"source": [
"We then create our system prompt, passing in default components of our background, calendar, and response preferences"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "98ac30b5-c71d-46fb-8419-b1f37b4a2724",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"< Role >\n",
"You are a top-notch executive assistant. \n",
"</ Role >\n",
"\n",
"< Tools >\n",
"You have access to the following tools to help manage communications and schedule:\n",
"\n",
"1. write_email(to, subject, content) - Send emails to specified recipients\n",
"2. schedule_meeting(attendees, subject, duration_minutes, preferred_day, start_time) - Schedule calendar meetings where preferred_day is a datetime object\n",
"3. check_calendar_availability(day) - Check available time slots for a given day\n",
"4. Question(content) - Ask the user any follow-up questions\n",
"5. Done - E-mail has been sent\n",
"\n",
"</ Tools >\n",
"\n",
"< Instructions >\n",
"When handling emails, follow these steps:\n",
"1. Carefully analyze the email content and purpose\n",
"2. IMPORTANT --- always call a tool and call one tool at a time until the task is complete: \n",
"3. If the incoming email asks the user a direct question and you do not have context to answer the question, use the Question tool to ask the user for the answer\n",
"4. For responding to the email, draft a response email with the write_email tool\n",
"5. For meeting requests, use the check_calendar_availability tool to find open time slots\n",
"6. To schedule a meeting, use the schedule_meeting tool with a datetime object for the preferred_day parameter\n",
" - Today's date is 2025-08-12 - use this for scheduling meetings accurately\n",
"7. If you scheduled a meeting, then draft a short response email using the write_email tool\n",
"8. After using the write_email tool, the task is complete\n",
"9. If you have sent the email, then use the Done tool to indicate that the task is complete\n",
"</ Instructions >\n",
"\n",
"< Background >\n",
" \n",
"I'm Catherine, a deployed engineer at LangChain.\n",
"\n",
"</ Background >\n",
"\n",
"< Response Preferences >\n",
"\n",
"Use professional and concise language. If the e-mail mentions a deadline, make sure to explicitly acknowledge and reference the deadline in your response.\n",
"\n",
"When responding to technical questions that require investigation:\n",
"- Clearly state whether you will investigate or who you will ask\n",
"- Provide an estimated timeline for when you'll have more information or complete the task\n",
"\n",
"When responding to event or conference invitations:\n",
"- Always acknowledge any mentioned deadlines (particularly registration deadlines)\n",
"- If workshops or specific topics are mentioned, ask for more specific details about them\n",
"- If discounts (group or early bird) are mentioned, explicitly request information about them\n",
"- Don't commit \n",
"\n",
"When responding to collaboration or project-related requests:\n",
"- Acknowledge any existing work or materials mentioned (drafts, slides, documents, etc.)\n",
"- Explicitly mention reviewing these materials before or during the meeting\n",
"- When scheduling meetings, clearly state the specific day, date, and time proposed\n",
"\n",
"When responding to meeting scheduling requests:\n",
"- If times are proposed, verify calendar availability for all time slots mentioned in the original email and then commit to one of the proposed times based on your availability by scheduling the meeting. Or, say you can't make it at the time proposed.\n",
"- If no times are proposed, then check your calendar for availability and propose multiple time options when available instead of selecting just one.\n",
"- Mention the meeting duration in your response to confirm you've noted it correctly.\n",
"- Reference the meeting's purpose in your response.\n",
"\n",
"</ Response Preferences >\n",
"\n",
"< Calendar Preferences >\n",
"\n",
"30 minute meetings are preferred, but 15 minute meetings are also acceptable.\n",
"\n",
"</ Calendar Preferences >\n",
"\n"
]
}
],
"source": [
"from email_assistant.prompts import triage_system_prompt, triage_user_prompt, agent_system_prompt_hitl_memory, default_triage_instructions, default_background, default_response_preferences, default_cal_preferences\n",
"from email_assistant.tools.default.prompt_templates import HITL_MEMORY_TOOLS_PROMPT\n",
"\n",
"react_system_prompt = agent_system_prompt_hitl_memory.format(\n",
" tools_prompt=HITL_MEMORY_TOOLS_PROMPT,\n",
" background=default_background,\n",
" response_preferences=default_response_preferences,\n",
" cal_preferences=default_cal_preferences, \n",
")\n",
"\n",
"print(react_system_prompt)"
]
},
{
"cell_type": "markdown",
"id": "369f6fe9-76ea-4450-9432-7f371ac4b3b6",
"metadata": {},
"source": [
"Now that we have a system prompt, we can pass in as part of a system message to the LLM, along with current message list."
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "6a7c06e9-65d8-4e3c-bbea-a3e143030d49",
"metadata": {},
"outputs": [],
"source": [
"def llm_call(state: State):\n",
" \"\"\"LLM decides whether to call a tool or not\"\"\"\n",
"\n",
" return {\n",
" \"messages\": [\n",
" # Invoke the LLM\n",
" llm_with_tools.invoke(\n",
" # Add the system prompt\n",
" [ \n",
" {\"role\": \"system\", \"content\": react_system_prompt}\n",
" ]\n",
" # Add the current messages to the prompt\n",
" + state[\"messages\"]\n",
" )\n",
" ]\n",
" }"
]
},
{
"cell_type": "markdown",
"id": "9eeb5b63-d716-4744-a028-4b930dc44b3c",
"metadata": {},
"source": [
"### Edges"
]
},
{
"cell_type": "markdown",
"id": "5b025c3b-c267-4c0a-8a4a-e2abe5077b84",
"metadata": {},
"source": [
"Now, we need to define a control flow that connects between our defined nodes, and that's where the concept of edges come in.\n",
"\n",
"**Edges are connections between nodes. They define the flow of the graph.**\n",
"* **Normal edges** are deterministic and always go from one node to its defined target\n",
"* **Conditional edges** are used to dynamically route between nodes, implemented as functions that return the next node to visit based upon some logic. \n",
"\n",
"In this case, we want a **conditional edge** from our LLM node that determines whether to: \n",
"- Invoke tools, or,\n",
"- Route to the end if no more tool calls are needed"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "94c5da7a-d031-4acf-842e-08b768d0becb",
"metadata": {},
"outputs": [],
"source": [
"def should_continue(state: State) -> Literal[\"tool_handler\", \"__end__\"]:\n",
" \"\"\"Route to tool handler, or end if Done tool called.\"\"\"\n",
" \n",
" # Get the last message\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" \n",
" # Check if it's a Done tool call\n",
" if last_message.tool_calls:\n",
" for tool_call in last_message.tool_calls: \n",
" if tool_call[\"name\"] == \"Done\":\n",
" return END\n",
" else:\n",
" return \"tool_handler\""
]
},
{
"cell_type": "markdown",
"id": "c9f63542-8172-49bd-8b52-526a2a9e26ec",
"metadata": {},
"source": [
"### Compile Graph!\n",
"\n",
"Now that we've defined our State and Nodes, let's put assemble the components together and construct our react agent!"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "3db347fe-5459-4daf-840f-78b1c894897f",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.graph import StateGraph, START, END\n",
"from email_assistant.utils import show_graph\n",
"\n",
"# Build workflow\n",
"overall_workflow = StateGraph(State)\n",
"\n",
"# Add nodes\n",
"overall_workflow.add_node(\"llm_call\", llm_call)\n",
"overall_workflow.add_node(\"tool_handler\", tool_handler)\n",
"\n",
"# Add edges\n",
"overall_workflow.add_edge(START, \"llm_call\")\n",
"overall_workflow.add_conditional_edges(\n",
" \"llm_call\",\n",
" should_continue,\n",
" {\n",
" \"tool_handler\": \"tool_handler\",\n",
" END: END,\n",
" },\n",
")\n",
"overall_workflow.add_edge(\"tool_handler\", \"llm_call\")\n",
"\n",
"# Compile the agent\n",
"agent = overall_workflow.compile()"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "d83544a1-694f-4f2c-bf2e-2ca1c69a2153",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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",
"text/plain": [
"<IPython.core.display.Image object>"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Show graph \n",
"show_graph(agent)"
]
},
{
"cell_type": "markdown",
"id": "2b0fa142-992e-4997-8011-8fda86415164",
"metadata": {},
"source": [
"### Testing"
]
},
{
"cell_type": "markdown",
"id": "d23e9a06-86be-4672-800a-93062bd7611f",
"metadata": {},
"source": [
"Let's see how it works!"
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "e17b458b-2fa6-4550-90d8-9cb6f00a3da0",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Hi John,\n",
"I was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\n",
"Specifically, I'm looking at:\n",
"- /auth/refresh\n",
"- /auth/validate\n",
"Thanks!\n",
"Alice\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Question (call_8YqPy5Rv5S8Igmds6KKvIZHv)\n",
" Call ID: call_8YqPy5Rv5S8Igmds6KKvIZHv\n",
" Args:\n",
" content: Hi Catherine, were the /auth/refresh and /auth/validate endpoints intentionally left out of the authentication service API documentation, or should we plan to update the specs to include them?\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"content='Hi Catherine, were the /auth/refresh and /auth/validate endpoints intentionally left out of the authentication service API documentation, or should we plan to update the specs to include them?'\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" write_email (call_Ndzl1lZjUXfANW8P66vTx4Wa)\n",
" Call ID: call_Ndzl1lZjUXfANW8P66vTx4Wa\n",
" Args:\n",
" to: Alice\n",
" subject: Re: API Documentation Missing Endpoints Clarification\n",
" content: Hi Alice,\n",
"\n",
"Thank you for your message and for reviewing the API documentation. I've reached out to our team to clarify whether the /auth/refresh and /auth/validate endpoints were intentionally omitted or if we should update the documentation accordingly. \n",
"\n",
"I'll follow up with you as soon as I have more information—expect an update shortly.\n",
"\n",
"Best regards,\n",
"Catherine\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Email sent to Alice with subject 'Re: API Documentation Missing Endpoints Clarification' and content: Hi Alice,\n",
"\n",
"Thank you for your message and for reviewing the API documentation. I've reached out to our team to clarify whether the /auth/refresh and /auth/validate endpoints were intentionally omitted or if we should update the documentation accordingly. \n",
"\n",
"I'll follow up with you as soon as I have more information—expect an update shortly.\n",
"\n",
"Best regards,\n",
"Catherine\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_08kLdlbJpdBgnGtqSOUtyznk)\n",
" Call ID: call_08kLdlbJpdBgnGtqSOUtyznk\n",
" Args:\n",
" done: True\n"
]
}
],
"source": [
"from langchain_core.messages import HumanMessage\n",
"\n",
"email_input = \"Hi John,\\nI was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\\nSpecifically, I'm looking at:\\n- /auth/refresh\\n- /auth/validate\\nThanks!\\nAlice\"\n",
"\n",
"# Run the agent\n",
"response = agent.invoke({\"messages\": [HumanMessage(content=email_input)]})\n",
"for m in response[\"messages\"]:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "43f0f4e4-e66d-445a-850a-4277b93e6211",
"metadata": {
"jp-MarkdownHeadingCollapsed": true
},
"source": [
"### Optional: Using ReAct Agent Pre-Built"
]
},
{
"cell_type": "markdown",
"id": "4f3ef6d4-a0b8-4832-ab4a-8343c620edb1",
"metadata": {},
"source": [
"LangGraph offers pre-built libraries for common architectures, allowing us to quickly create architectures like ReAct or multi-agent architacture. A full list of pre-built libraries can be found here: https://langchain-ai.github.io/langgraph/prebuilt/#available-libraries \n",
"\n",
"In the last workflow, we have seen how we can build a ReAct agent from scratch. Now, we will show how we can leverage the LangGraph pre-built libraries to achieve similar results. "
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "bea5f387-4aaf-4968-bc1c-c53e233ec028",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.managed.is_last_step import RemainingSteps\n",
"\n",
"class PrebuiltState(MessagesState):\n",
" email_input: dict\n",
" classification_decision: Literal[\"ignore\", \"respond\", \"notify\"]\n",
"\n",
" # Using pre-built requires RemainingSteps\n",
" remaining_steps: RemainingSteps "
]
},
{
"cell_type": "code",
"execution_count": 31,
"id": "9e8a3faf-6328-441b-9106-55b8ad32af2b",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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",
"text/plain": [
"<langgraph.graph.state.CompiledStateGraph object at 0x11b09b550>"
]
},
"execution_count": 31,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from langgraph.prebuilt import create_react_agent\n",
"\n",
"# Define the agent \n",
"agent_prebuilt = create_react_agent(llm, tools=tools, prompt=react_system_prompt, state_schema=PrebuiltState)\n",
"\n",
"# Visualize the graph\n",
"agent_prebuilt"
]
},
{
"cell_type": "code",
"execution_count": 32,
"id": "908b7068-705e-415f-854e-2e68d595f672",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Hi John,\n",
"I was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\n",
"Specifically, I'm looking at:\n",
"- /auth/refresh\n",
"- /auth/validate\n",
"Thanks!\n",
"Alice\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Question (call_dUEwgUTWcTiD9aDxy6162AHD)\n",
" Call ID: call_dUEwgUTWcTiD9aDxy6162AHD\n",
" Args:\n",
" content: Hi Catherine, can you clarify whether the /auth/refresh and /auth/validate endpoints were intentionally omitted from the API documentation for the new authentication service, or should the documentation be updated to include them? Once I have your input, I can respond to Alice accordingly.\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"Name: Question\n",
"\n",
"content='Hi Catherine, can you clarify whether the /auth/refresh and /auth/validate endpoints were intentionally omitted from the API documentation for the new authentication service, or should the documentation be updated to include them? Once I have your input, I can respond to Alice accordingly.'\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"\n",
"Alice has asked if the /auth/refresh and /auth/validate endpoints were intentionally omitted from the API documentation, or if the documentation needs to be updated. Were these endpoints intentionally left out, or should I coordinate an update to include them? Once you clarify, I can respond to Alice accordingly.\n"
]
}
],
"source": [
"# Run the agent\n",
"response = agent_prebuilt.invoke({\"messages\": [HumanMessage(content=email_input)]})\n",
"for m in response[\"messages\"]:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "85ffc1d6-1742-4cd5-8515-f75f01f312e9",
"metadata": {},
"source": [
"## Part I.2: Adding Router Logic in Tool-Calling Agent"
]
},
{
"cell_type": "markdown",
"id": "c79f7917-3083-4453-b5bc-40691f398e07",
"metadata": {},
"source": [
"Now, we have a ReAct-style agent that can respond to incoming emails. However, often times, not all emails are worth responding to. \n",
"\n",
"Here, we'll add a triage logic with our tool-calling agent to build a [workflow](https://langchain-ai.github.io/langgraph/tutorials/workflows/) for our email assistant, where we will first decide the importance of the email, before triggering the ReAct email agent that we just built. \n",
"\n",
"![agent_workflow_img](img/email_workflow.png)\n"
]
},
{
"cell_type": "markdown",
"id": "1ad7d84e-5ab8-445b-85c4-ec8d8ca8bab8",
"metadata": {},
"source": [
"#### Router Node"
]
},
{
"cell_type": "markdown",
"id": "152a3ee5-ba82-4103-837a-2b1950916058",
"metadata": {},
"source": [
"##### Step 1. Specifying router output schema "
]
},
{
"cell_type": "markdown",
"id": "9abd66a8-7f90-4b17-8022-cc3400b1d8e7",
"metadata": {},
"source": [
"The routing step handles the triage decision, and we define a python function with our triage routing logic.\n",
"\n",
"For this, we use [structured outputs](https://python.langchain.com/docs/concepts/structured_outputs/) with a Pydantic model. **ChatModels support attaching a structured data schema to adhere response to**. This is useful in scenarios like extracting information or categorizing. \n",
"\n",
"Pydantic models are particularly useful for defining structured output schemas because it offers type hints and validation. The descriptions in the pydantic model are important because they get passed as part JSON schema to the LLM to inform the output coercion."
]
},
{
"cell_type": "code",
"execution_count": 33,
"id": "e3357227-da0f-4c93-a2fd-a064a16af71e",
"metadata": {},
"outputs": [],
"source": [
"class RouterSchema(BaseModel):\n",
" \"\"\"Analyze the unread email and route it according to its content.\"\"\"\n",
"\n",
" reasoning: str = Field(\n",
" description=\"Step-by-step reasoning behind the classification.\"\n",
" )\n",
" classification: Literal[\"ignore\", \"respond\", \"notify\"] = Field(\n",
" description=\"The classification of an email: 'ignore' for irrelevant emails, \"\n",
" \"'notify' for important information that doesn't need a response, \"\n",
" \"'respond' for emails that need a reply\",\n",
" )\n",
"\n",
"llm_router = llm.with_structured_output(RouterSchema) "
]
},
{
"cell_type": "markdown",
"id": "d7dee75d-9492-44d4-8b4e-847a99006bda",
"metadata": {},
"source": [
"##### Step 2. Prompt creation"
]
},
{
"cell_type": "markdown",
"id": "7d3113f9-95bd-4c6d-aecc-43d612c083da",
"metadata": {},
"source": [
"To get at our triaging decision, we will create two prompts: \n",
"- First, a **system prompt** that includes instructions and rules on categorization\n",
"- Second, a **user prompt template** that includes the email content, which we will pass as a user message"
]
},
{
"cell_type": "code",
"execution_count": 34,
"id": "8f176b15-f8fa-4cf7-b582-724ea99db9bc",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\n",
"< Role >\n",
"Your role is to triage incoming emails based upon instructs and background information below.\n",
"</ Role >\n",
"\n",
"< Background >\n",
" \n",
"I'm Catherine, a deployed engineer at LangChain.\n",
". \n",
"</ Background >\n",
"\n",
"< Instructions >\n",
"Categorize each email into one of three categories:\n",
"1. IGNORE - Emails that are not worth responding to or tracking\n",
"2. NOTIFY - Important information that worth notification but doesn't require a response\n",
"3. RESPOND - Emails that need a direct response\n",
"Classify the below email into one of these categories.\n",
"</ Instructions >\n",
"\n",
"< Rules >\n",
"\n",
"Emails that are not worth responding to:\n",
"- Marketing newsletters and promotional emails\n",
"- Spam or suspicious emails\n",
"- CC'd on FYI threads with no direct questions\n",
"\n",
"There are also other things that should be known about, but don't require an email response. For these, you should notify (using the `notify` response). Examples of this include:\n",
"- Team member out sick or on vacation\n",
"- Build system notifications or deployments\n",
"- Project status updates without action items\n",
"- Important company announcements\n",
"- FYI emails that contain relevant information for current projects\n",
"- HR Department deadline reminders\n",
"- Subscription status / renewal reminders\n",
"- GitHub notifications\n",
"\n",
"Emails that are worth responding to:\n",
"- Direct questions from team members requiring expertise\n",
"- Meeting requests requiring confirmation\n",
"- Critical bug reports related to team's projects\n",
"- Requests from management requiring acknowledgment\n",
"- Client inquiries about project status or features\n",
"- Technical questions about documentation, code, or APIs (especially questions about missing endpoints or features)\n",
"- Personal reminders related to family (wife / daughter)\n",
"- Personal reminder related to self-care (doctor appointments, etc)\n",
"\n",
"</ Rules >\n",
"\n"
]
}
],
"source": [
"router_system_prompt = triage_system_prompt.format(\n",
" background=default_background,\n",
" triage_instructions=default_triage_instructions\n",
")\n",
"print(router_system_prompt)"
]
},
{
"cell_type": "code",
"execution_count": 35,
"id": "753f3a0c-57cc-4531-abcb-0dc5a09b8995",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Please determine how to handle the below email thread:\n",
"\n",
"From: {author}\n",
"To: {to}\n",
"Subject: {subject}\n",
"{email_thread}\n"
]
}
],
"source": [
"print(triage_user_prompt)"
]
},
{
"cell_type": "markdown",
"id": "9b41d135-b1bf-4cd4-8d69-8c835055d65a",
"metadata": {},
"source": [
"##### Step 3. Implementing the node "
]
},
{
"cell_type": "markdown",
"id": "31e6c872-6a9c-4136-b4af-b4201f6961bf",
"metadata": {},
"source": [
"Once we have these prompts, we are ready set up our `triage_router` node, where we will \n",
"- First, **invoke the structured LLM to achieve triage decision**, passing in human instructions and email content\n",
"- Next, based on the router's classification results, we will **map to different paths**"
]
},
{
"cell_type": "code",
"execution_count": 37,
"id": "27ef143f-7cd6-4a71-9ef6-3a9762c8ce19",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.types import Command\n",
"def triage_router(state: State) -> Command[Literal[\"response_agent\", \"__end__\"]]:\n",
" \"\"\"Analyze email content to decide if we should respond, notify, or ignore.\"\"\"\n",
" \n",
" author, to, subject, email_thread = parse_email(state[\"email_input\"])\n",
"\n",
" user_prompt = triage_user_prompt.format(\n",
" author=author, to=to, subject=subject, email_thread=email_thread\n",
" )\n",
"\n",
" # Invoke structured LLM to classify Email \n",
" result = llm_router.invoke(\n",
" [\n",
" {\"role\": \"system\", \"content\": router_system_prompt},\n",
" {\"role\": \"user\", \"content\": user_prompt},\n",
" ]\n",
" )\n",
" \n",
" if result.classification == \"respond\":\n",
" print(\"📧 Classification: RESPOND - This email requires a response\")\n",
" goto = \"response_agent\"\n",
" update = {\n",
" \"messages\": [\n",
" {\n",
" \"role\": \"user\",\n",
" \"content\": f\"Respond to the email: \\n\\n{format_email_markdown(subject, author, to, email_thread)}\",\n",
" }\n",
" ],\n",
" \"classification_decision\": result.classification,\n",
" }\n",
" \n",
" elif result.classification == \"ignore\":\n",
" print(\"🚫 Classification: IGNORE - This email can be safely ignored\")\n",
" goto = END\n",
" update = {\n",
" \"classification_decision\": result.classification,\n",
" }\n",
" \n",
" elif result.classification == \"notify\":\n",
" print(\"🔔 Classification: NOTIFY - This email contains important information\")\n",
" # For now, we go to END. But we will add to this later!\n",
" goto = END\n",
" update = {\n",
" \"classification_decision\": result.classification,\n",
" }\n",
" \n",
" else:\n",
" raise ValueError(f\"Invalid classification: {result.classification}\")\n",
" return Command(goto=goto, update=update)"
]
},
{
"cell_type": "markdown",
"id": "6ce69e28-b646-4055-9b5d-f0e046120971",
"metadata": {},
"source": [
"We use [Command](https://langchain-ai.github.io/langgraph/how-tos/command/) objects in LangGraph to both update the state and select the next node to visit. This is a useful alternative to edges."
]
},
{
"cell_type": "markdown",
"id": "61e66c21-00a5-4d82-a887-32ff7508119a",
"metadata": {},
"source": [
"### Combine workflow \n",
"\n",
"We can combine the router with our tool-calling ReAct agent from Part 1.1"
]
},
{
"cell_type": "code",
"execution_count": 38,
"id": "b62cd12e-f7db-47d7-a91f-b555f67f4ca7",
"metadata": {},
"outputs": [],
"source": [
"triage_workflow = (\n",
" StateGraph(State)\n",
" .add_node(triage_router)\n",
" .add_node(\"response_agent\", agent)\n",
" .add_edge(START, \"triage_router\")\n",
").compile()"
]
},
{
"cell_type": "code",
"execution_count": 39,
"id": "713a54c7-50e0-4911-a75b-5978d30f54fb",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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",
"text/plain": [
"<IPython.core.display.Image object>"
]
},
"execution_count": 39,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"show_graph(triage_workflow, xray=True)"
]
},
{
"cell_type": "markdown",
"id": "987724f2-fc21-4571-968d-269339cada65",
"metadata": {},
"source": [
"This is a higher-level composition where:\n",
"1. First, the triage router analyzes the email\n",
"2. If needed, the response agent handles crafting a response\n",
"3. The workflow ends when either the triage decides no response is needed or the response agent completes"
]
},
{
"cell_type": "markdown",
"id": "afea1cc1-d7cf-4fd4-8d48-2bc94c5d9eba",
"metadata": {},
"source": [
"### Let's test it out!"
]
},
{
"cell_type": "markdown",
"id": "20054d0d",
"metadata": {},
"source": [
"First, lets give our system an email that will be classified as 'NOTIFY' but wont require any action"
]
},
{
"cell_type": "code",
"execution_count": 42,
"id": "8d7966dd-3e64-4421-99da-45889d7c9a69",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"🔔 Classification: NOTIFY - This email contains important information\n"
]
}
],
"source": [
"email_input = {\n",
" \"author\": \"System Admin <sysadmin@company.com>\",\n",
" \"to\": \"Development Team <dev@company.com>\",\n",
" \"subject\": \"Scheduled maintenance - database downtime\",\n",
" \"email_thread\": \"Hi team,\\n\\nThis is a reminder that we'll be performing scheduled maintenance on the production database tonight from 2AM to 4AM EST. During this time, all database services will be unavailable.\\n\\nPlease plan your work accordingly and ensure no critical deployments are scheduled during this window.\\n\\nThanks,\\nSystem Admin Team\"\n",
"}\n",
"\n",
"# Run the agent\n",
"response = triage_workflow.invoke({\"email_input\": email_input})\n",
"for m in response[\"messages\"]:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "09336dd0",
"metadata": {},
"source": [
"Now, lets pass an email that will require triggering our response agent"
]
},
{
"cell_type": "code",
"execution_count": 43,
"id": "1147bb5e-66ee-41a1-bb22-e35553e1edcb",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"📧 Classification: RESPOND - This email requires a response\n",
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Respond to the email: \n",
"\n",
"\n",
"\n",
"**Subject**: Quick question about API documentation\n",
"**From**: Alice Smith <alice.smith@company.com>\n",
"**To**: John Doe <john.doe@company.com>\n",
"\n",
"Hi John,\n",
"I was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\n",
"Specifically, I'm looking at:\n",
"- /auth/refresh\n",
"- /auth/validate\n",
"Thanks!\n",
"Alice\n",
"\n",
"---\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Question (call_bCH8APLccNst1iZqoJ86odE4)\n",
" Call ID: call_bCH8APLccNst1iZqoJ86odE4\n",
" Args:\n",
" content: Hi Catherine, Alice asked whether the endpoints /auth/refresh and /auth/validate are intentionally missing from the new authentication service API documentation, or if the docs should be updated. Can you clarify whether this is by design, or if we need to update the documentation? If you're not sure, should I check with someone from the auth service team? Please advise. Thank you!\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"content=\"Hi Catherine, Alice asked whether the endpoints /auth/refresh and /auth/validate are intentionally missing from the new authentication service API documentation, or if the docs should be updated. Can you clarify whether this is by design, or if we need to update the documentation? If you're not sure, should I check with someone from the auth service team? Please advise. Thank you!\"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" write_email (call_KJxeQrlIHkin7d5qEDP2RphM)\n",
" Call ID: call_KJxeQrlIHkin7d5qEDP2RphM\n",
" Args:\n",
" to: alice.smith@company.com\n",
" subject: Re: Quick question about API documentation\n",
" content: Hi Alice,\n",
"\n",
"Thank you for reaching out and highlighting the missing endpoints in the API documentation for the authentication service. Im currently checking with the relevant team to clarify whether the omission of /auth/refresh and /auth/validate was intentional or if the documentation needs to be updated. \n",
"\n",
"Ill follow up with you as soon as I have more information, likely within the next 1-2 business days. Please let me know if you have any additional questions or require further details in the meantime.\n",
"\n",
"Best regards,\n",
"Catherine\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Email sent to alice.smith@company.com with subject 'Re: Quick question about API documentation' and content: Hi Alice,\n",
"\n",
"Thank you for reaching out and highlighting the missing endpoints in the API documentation for the authentication service. Im currently checking with the relevant team to clarify whether the omission of /auth/refresh and /auth/validate was intentional or if the documentation needs to be updated. \n",
"\n",
"Ill follow up with you as soon as I have more information, likely within the next 1-2 business days. Please let me know if you have any additional questions or require further details in the meantime.\n",
"\n",
"Best regards,\n",
"Catherine\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_Qj7XxJjFA3XVFyuRdowCBs20)\n",
" Call ID: call_Qj7XxJjFA3XVFyuRdowCBs20\n",
" Args:\n",
" done: True\n"
]
}
],
"source": [
"email_input = {\n",
" \"author\": \"Alice Smith <alice.smith@company.com>\",\n",
" \"to\": \"John Doe <john.doe@company.com>\",\n",
" \"subject\": \"Quick question about API documentation\",\n",
" \"email_thread\": \"Hi John,\\nI was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\\nSpecifically, I'm looking at:\\n- /auth/refresh\\n- /auth/validate\\nThanks!\\nAlice\"\n",
"}\n",
"\n",
"# Run the agent\n",
"response = triage_workflow.invoke({\"email_input\": email_input})\n",
"for m in response[\"messages\"]:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "e6b3290b",
"metadata": {},
"source": [
"<div align=\"center\">\n",
"<h1 style=\"color: blue; padding-top: 30px; padding-bottom: 30px\"><b>10 minute break</b></h1>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "1b2cf91b-c5ba-46d3-8c54-08a7e39bdc44",
"metadata": {},
"source": [
"## Part II. Human-in-the-loop"
]
},
{
"cell_type": "markdown",
"id": "a138bbd6-6edd-41be-8fe3-a1fcd9461b1b",
"metadata": {},
"source": [
"We have an email assistant that uses a router to triage emails and then passes the email to the agent for response generation. We've also evaluated it. But do we fully *trust* it to manage our inbox autonomously? For such a sensitive task, **human-in-the-loop (HITL)** is important! \n",
"\n",
"In this step, we'll show how to add a human-in-the-loop to our email assistant so that we can review specific tool calls. \n",
"\n",
"![overview-img](img/overview_hitl.png)\n"
]
},
{
"cell_type": "markdown",
"id": "200e52ee-37fb-4a48-adf0-17b1c7d7c02a",
"metadata": {},
"source": [
"We're going to show how to make the graph *pause* at specific points and await human input.\n",
"\n",
"![overview-img](img/hitl_schematic.png)\n",
"\n",
"Specifically, there are two steps that we want to pause: \n",
"- **triager_interrupt_handler**: Interrupting for \"Notify\" type emails, allowing user to review\n",
"- Adding interrupt to our **tool_handler**, maintaining a list of \"HITL tools\" that require human approval"
]
},
{
"cell_type": "markdown",
"id": "dbe47c79-079e-45ae-a675-a59e2d732c44",
"metadata": {},
"source": [
"### Node 1. Triage Interrupt Handler\n",
"\n",
"![overview-img](img/HITL_flow_triage.png)\n",
"\n",
"If the decision is to `notify` the user, we interrupt the graph! \n",
"\n",
"For this, we add a new node, `triage_interrupt_handler`, that will do two steps: \n",
"1. If an email is classified as \"notify\", we interrupt the execution\n",
"2. We allow the user to respond to interrupt and appropriate handle the flow \n"
]
},
{
"cell_type": "markdown",
"id": "6fa464f7-0c81-490e-8dd1-b4e9ebc4ced8",
"metadata": {},
"source": [
"**For the first step, with the `notify` decision, we want to interrupt the graph execution, and pass a `dict` to the end user** that contains our classification decision and email content\n",
"\n",
"In Agent Inbox specifically, we allow four interaction types, as shown below, which include: \n",
"- `accept`: Accept the interrupt's arguments, or action. \n",
"- `edit`: Edit the interrupt's arguments. For our email agent, we will use it to directly edit the content email responses or meeting schedules proposed by our agent. \n",
"- `ignore`: Ignore the interrupt's arguments, or action. \n",
"- `response`: Send a response to the interrupt. Does not require any arguments. For our email agent, we will use it to give feedback back to regenerate (rather than directly editing email response or meeting schedule). \n",
"\n",
"\n",
"![interrupt-inbox](img/agent-inbox-edit.png)\n",
"\n",
"To render the content appropriately in Agent Inbox, we format our interrupt with specific fields shown [here](https://github.com/langchain-ai/agent-inbox?tab=readme-ov-file#what-do-the-fields-mean):\n",
"\n",
"1. `action_request`: The action and arguments for the interrupt with `action` (the action name) and `args` (the tool call arguments). This is rendered in the Agent Inbox as the main header for the interrupt event, which we will **show the classification decision** \n",
"2. `config`: Configures which interaction types are allowed, which allows Agent Inbox to render UI elements for each allowed option.\n",
" - In our case of reviewing triage results, we want to **allow for two actions: `ignore` and `respond`**\n",
"4. `description`: Should be detailed, and may be markdown. This will be rendered in the Agent Inbox as the description.\n",
" - In this case, we will be passing our **formatted email content in markdown**. \n"
]
},
{
"cell_type": "markdown",
"id": "a6e4908c-b02c-40ad-8005-8f8e9d848f3a",
"metadata": {},
"source": [
"**In the second step, allow the user to respond to the decision**: We'll design the code to handle what we will get back from Agent Inbox.\n",
"- If the user chooses to respond to the agent, we will route to the `response_agent` with instructions\n",
"- If the user decides to ignore, route to `END`\n"
]
},
{
"cell_type": "code",
"execution_count": 47,
"id": "a147d95a-cf38-4281-b917-50f400a0988d",
"metadata": {},
"outputs": [],
"source": [
"def triage_interrupt_handler(state: State) -> Command[Literal[\"response_agent\", \"__end__\"]]:\n",
" \"\"\"Handles interrupts from the triage step.\"\"\"\n",
"\n",
" # Step 1. Formatting the classification and showing the user triage decision through interrupt \n",
" \n",
" # Parse the email input\n",
" author, to, subject, email_thread = parse_email(state[\"email_input\"])\n",
"\n",
" # Create email markdown for Agent Inbox in case of notification \n",
" email_markdown = format_email_markdown(subject, author, to, email_thread)\n",
"\n",
" # Create messages\n",
" messages = [{\"role\": \"user\",\n",
" \"content\": f\"Email to notify user about: {email_markdown}\"\n",
" }]\n",
"\n",
" # Create interrupt that is shown to the user\n",
" request = {\n",
" \"action_request\": {\n",
" \"action\": f\"Email Assistant: {state['classification_decision']}\",\n",
" \"args\": {}\n",
" },\n",
" \"config\": {\n",
" \"allow_ignore\": True, \n",
" \"allow_respond\": True, \n",
" \"allow_edit\": False, \n",
" \"allow_accept\": False, \n",
" },\n",
" # Email to show in Agent Inbox\n",
" \"description\": email_markdown,\n",
" }\n",
"\n",
" \n",
"\n",
" # Step 2. Allow user to respond decision. \n",
"\n",
" # Agent Inbox responds with a list of dicts with a single key `type` that can be `accept`, `edit`, `ignore`, or `response`. \n",
" # In this case, we only allowed for `respond` and `ignore` \n",
" response = interrupt([request])[0]\n",
"\n",
" # If user provides feedback, go to response agent and use feedback to respond to email \n",
" if response[\"type\"] == \"response\":\n",
" # Add feedback to messages \n",
" user_input = response[\"args\"]\n",
" # Used by the response agent\n",
" messages.append({\"role\": \"user\",\n",
" \"content\": f\"User wants to reply to the email. Use this feedback to respond: {user_input}\"\n",
" })\n",
" # Go to response agent\n",
" goto = \"response_agent\"\n",
"\n",
" # If user ignores email, go to END\n",
" elif response[\"type\"] == \"ignore\":\n",
" goto = END\n",
"\n",
" # Catch all other responses\n",
" else:\n",
" raise ValueError(f\"Invalid response: {response}\")\n",
"\n",
" # Update the state \n",
" update = {\n",
" \"messages\": messages,\n",
" }\n",
"\n",
" return Command(goto=goto, update=update)"
]
},
{
"cell_type": "markdown",
"id": "b2b264c3-2ced-4d79-8c31-a3ad93981526",
"metadata": {},
"source": [
"#### Updating triage logic\n",
"\n",
"Now that we have a new node, we would want to update our triage routing logic to reflect it.\n",
"\n",
"In our earlier implementation, if the classification is `notify`, we simply route to `END`. \n",
"\n",
"Now, with the new node, we should route instead to `triage_interrupt_handler`, in order to interrupt the graph and allow the user to review the email: "
]
},
{
"cell_type": "code",
"execution_count": 48,
"id": "e2f8b29c-453a-454a-9f5f-2df345e23d83",
"metadata": {},
"outputs": [],
"source": [
"def triage_router(state: State) -> Command[Literal[\"triage_interrupt_handler\", \"response_agent\", \"__end__\"]]:\n",
" \"\"\"Analyze email content to decide if we should respond, notify, or ignore.\"\"\"\n",
"\n",
" # Parse the email input\n",
" author, to, subject, email_thread = parse_email(state[\"email_input\"])\n",
" user_prompt = triage_user_prompt.format(\n",
" author=author, to=to, subject=subject, email_thread=email_thread\n",
" )\n",
"\n",
" # Create email markdown for Agent Inbox in case of notification \n",
" email_markdown = format_email_markdown(subject, author, to, email_thread)\n",
"\n",
" # Format system prompt with background and triage instructions\n",
" system_prompt = triage_system_prompt.format(\n",
" background=default_background,\n",
" triage_instructions=default_triage_instructions\n",
" )\n",
"\n",
" # Run the router LLM\n",
" result = llm_router.invoke(\n",
" [\n",
" {\"role\": \"system\", \"content\": system_prompt},\n",
" {\"role\": \"user\", \"content\": user_prompt},\n",
" ]\n",
" )\n",
"\n",
" # Decision\n",
" classification = result.classification\n",
"\n",
" # Process the classification decision\n",
" if classification == \"respond\":\n",
" print(\"📧 Classification: RESPOND - This email requires a response\")\n",
" # Next node\n",
" goto = \"response_agent\"\n",
" # Update the state\n",
" update = {\n",
" \"classification_decision\": result.classification,\n",
" \"messages\": [{\"role\": \"user\",\n",
" \"content\": f\"Respond to the email: {email_markdown}\"\n",
" }],\n",
" }\n",
" elif classification == \"ignore\":\n",
" print(\"🚫 Classification: IGNORE - This email can be safely ignored\")\n",
" # Next node\n",
" goto = END\n",
" # Update the state\n",
" update = {\n",
" \"classification_decision\": classification,\n",
" }\n",
"\n",
" # We changed the routing decision here! \n",
" elif classification == \"notify\":\n",
" print(\"🔔 Classification: NOTIFY - This email contains important information\") \n",
" # This is new! \n",
" goto = \"triage_interrupt_handler\"\n",
" # Update the state\n",
" update = {\n",
" \"classification_decision\": classification,\n",
" }\n",
"\n",
" else:\n",
" raise ValueError(f\"Invalid classification: {classification}\")\n",
" return Command(goto=goto, update=update)"
]
},
{
"cell_type": "markdown",
"id": "17967a74-bf86-43a5-a614-7a7706203834",
"metadata": {},
"source": [
"### Node 2. Tool Handler with Interrupt"
]
},
{
"cell_type": "markdown",
"id": "62732dc3-50a9-42b6-9e30-152f35be9cbd",
"metadata": {},
"source": [
"The `tool_handler` with interrupt is the core HITL component of our response agent. This updates our tool node, and its job is to examine the tool calls that the LLM wants to make and determine which ones need human review before execution. Here's how it works:\n",
"\n",
"Out of all our tools (`write_email`, `schedule_meeting`, `check_calendar_availability`, `Question`, `Done`) The handler maintains a list of **\"HITL tools\"** that require human approval. \n",
"\n",
"* **Tools not in the HITL list** (like `check_calendar_availability`) are executed immediately without interruption. This allows low-risk operations to proceed automatically. \n",
"\n",
"* **Tools in the HITL list** include: \n",
" - `write_email`: Since sending emails has significant external impact; user can `ignore` (cancel tool call),`respond` (give feedback to agent on email draft), `edit` (directly edit content of email), `accept` (accept email draft)\n",
" - `schedule_meeting`: Since scheduling meetings affects calendars; user can `ignore` (cancel tool call),`respond` (give feedback to agent on schedule proposal), `edit` (directly edit meeting time), `accept` (accept scheduled meeting)\n",
" - `Question`: Since asking users questions requires direct interaction; user can `ignore` or `respond`\n",
"\n",
"\n",
"We can visualize the overall flow: \n",
"![overview-img](img/HITL_flow.png)"
]
},
{
"cell_type": "markdown",
"id": "2e6473d9-efc5-405d-b4dc-04aba2ee0972",
"metadata": {},
"source": [
"For tools requiring review, the handler executes the following steps, ensuring humans have oversight of all significant actions while allowing routine operations to proceed automatically. \n",
"\n",
"**Step 1. Send relevant context for interrupt:** \n",
" - Context preparation: Retrieves the original email and formats the tool call details for display\n",
" - Structured interrupt request with:\n",
" - `action_request`: `action` (the action name) and `args` (the tool call arguments)\n",
" - `config` of allowed interaction types for each tool type\n",
" - `description` includes both the original email and the proposed action\n",
"\n",
"**Step 2. Response Processing** after the interrupt: \n",
" - `accept`: Executes the tool with original arguments\n",
" - `edit`: Updates the tool call with edited arguments and then executes - giving users precise control over the assistant's actions\n",
" - `ignore`: Cancels the tool execution\n",
" - `response`: Records feedback without execution"
]
},
{
"cell_type": "code",
"execution_count": 49,
"id": "f01a8346-249e-4764-a762-3770d360db42",
"metadata": {},
"outputs": [],
"source": [
"def tool_handler(state: State) -> Command[Literal[\"llm_call\", \"__end__\"]]:\n",
" \"\"\"Creates an interrupt for human review of tool calls\"\"\"\n",
" \n",
" # Store messages\n",
" result = []\n",
"\n",
" # Go to the LLM call node next after tool execution \n",
" goto = \"llm_call\"\n",
"\n",
" # Iterate over the tool calls in the last message\n",
" for tool_call in state[\"messages\"][-1].tool_calls:\n",
" \n",
" # Allowed tools for HITL\n",
" hitl_tools = [\"write_email\", \"schedule_meeting\", \"Question\"]\n",
" \n",
" # NOT HITL TOOLS: execute directly without interruption\n",
" if tool_call[\"name\"] not in hitl_tools:\n",
"\n",
" # Execute search_memory and other tools without interruption - same logic as before \n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" observation = tool.invoke(tool_call[\"args\"])\n",
" result.append({\"role\": \"tool\", \"content\": observation, \"tool_call_id\": tool_call[\"id\"]})\n",
" continue\n",
" \n",
" # HITL TOOLS: format relevant context and call interrupt \n",
" \n",
" # STEP 1. Format context for interrupt \n",
" \n",
" # Get original email from email_input in state\n",
" email_input = state[\"email_input\"]\n",
" author, to, subject, email_thread = parse_email(email_input)\n",
" original_email_markdown = format_email_markdown(subject, author, to, email_thread)\n",
" \n",
" # Format tool call for display and prepend the original email\n",
" tool_display = format_for_display(tool_call)\n",
" description = original_email_markdown + tool_display\n",
"\n",
" # Configure what actions are allowed for each tool call in Agent Inbox \n",
" if tool_call[\"name\"] == \"write_email\":\n",
" config = {\n",
" \"allow_ignore\": True,\n",
" \"allow_respond\": True,\n",
" \"allow_edit\": True,\n",
" \"allow_accept\": True,\n",
" }\n",
" elif tool_call[\"name\"] == \"schedule_meeting\":\n",
" config = {\n",
" \"allow_ignore\": True,\n",
" \"allow_respond\": True,\n",
" \"allow_edit\": True,\n",
" \"allow_accept\": True,\n",
" }\n",
" elif tool_call[\"name\"] == \"Question\":\n",
" config = {\n",
" \"allow_ignore\": True,\n",
" \"allow_respond\": True,\n",
" \"allow_edit\": False,\n",
" \"allow_accept\": False,\n",
" }\n",
" else:\n",
" raise ValueError(f\"Invalid tool call: {tool_call['name']}\")\n",
"\n",
" # Create the interrupt request\n",
" request = {\n",
" \"action_request\": {\n",
" \"action\": tool_call[\"name\"],\n",
" \"args\": tool_call[\"args\"]\n",
" },\n",
" \"config\": config,\n",
" \"description\": description,\n",
" }\n",
"\n",
" # Send to Agent Inbox and wait for response\n",
" response = interrupt([request])[0]\n",
"\n",
"\n",
"\n",
" # STEP 2. Handle responses from user \n",
"\n",
" # For \"accept\", we execute the tool call, route back to agent \n",
" if response[\"type\"] == \"accept\":\n",
"\n",
" # Execute the tool with original args\n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" observation = tool.invoke(tool_call[\"args\"])\n",
" result.append({\"role\": \"tool\", \"content\": observation, \"tool_call_id\": tool_call[\"id\"]})\n",
" \n",
" \n",
" # For \"EDIT\", we always execute the tool call after edited params, route back to agent \n",
" elif response[\"type\"] == \"edit\":\n",
"\n",
" # Tool selection \n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" \n",
" # Get edited args from Agent Inbox\n",
" edited_args = response[\"args\"][\"args\"]\n",
"\n",
" # Update the AI message's tool call with edited content (reference to the message in the state)\n",
" ai_message = state[\"messages\"][-1] # Get the most recent message from the state\n",
" current_id = tool_call[\"id\"] # Store the ID of the tool call being edited\n",
" \n",
" # Create a new list of tool calls by filtering out the one being edited and adding the updated version\n",
" # This avoids modifying the original list directly (immutable approach)\n",
" updated_tool_calls = [tc for tc in ai_message.tool_calls if tc[\"id\"] != current_id] + [\n",
" {\"type\": \"tool_call\", \"name\": tool_call[\"name\"], \"args\": edited_args, \"id\": current_id}\n",
" ]\n",
"\n",
" # Create a new copy of the message with updated tool calls rather than modifying the original\n",
" # This ensures state immutability and prevents side effects in other parts of the code\n",
" result.append(ai_message.model_copy(update={\"tool_calls\": updated_tool_calls}))\n",
"\n",
" # Update the write_email tool call with the edited content from Agent Inbox\n",
" if tool_call[\"name\"] == \"write_email\":\n",
" \n",
" # Execute the tool with edited args\n",
" observation = tool.invoke(edited_args)\n",
" \n",
" # Add only the tool response message\n",
" result.append({\"role\": \"tool\", \"content\": observation, \"tool_call_id\": current_id})\n",
" \n",
" # Update the schedule_meeting tool call with the edited content from Agent Inbox\n",
" elif tool_call[\"name\"] == \"schedule_meeting\":\n",
" \n",
" \n",
" # Execute the tool with edited args\n",
" observation = tool.invoke(edited_args)\n",
" \n",
" # Add only the tool response message\n",
" result.append({\"role\": \"tool\", \"content\": observation, \"tool_call_id\": current_id})\n",
" \n",
" # Catch all other tool calls\n",
" else:\n",
" raise ValueError(f\"Invalid tool call: {tool_call['name']}\")\n",
"\n",
"\n",
" # For \"IGNORE\", we do not execute the tool, and END the workflow \n",
" elif response[\"type\"] == \"ignore\":\n",
" if tool_call[\"name\"] == \"write_email\":\n",
" # Don't execute the tool, and tell the agent how to proceed\n",
" result.append({\"role\": \"tool\", \"content\": \"User ignored this email draft. Ignore this email and end the workflow.\", \"tool_call_id\": tool_call[\"id\"]})\n",
" # Go to END\n",
" goto = END\n",
" elif tool_call[\"name\"] == \"schedule_meeting\":\n",
" # Don't execute the tool, and tell the agent how to proceed\n",
" result.append({\"role\": \"tool\", \"content\": \"User ignored this calendar meeting draft. Ignore this email and end the workflow.\", \"tool_call_id\": tool_call[\"id\"]})\n",
" # Go to END\n",
" goto = END\n",
" elif tool_call[\"name\"] == \"Question\":\n",
" # Don't execute the tool, and tell the agent how to proceed\n",
" result.append({\"role\": \"tool\", \"content\": \"User ignored this question. Ignore this email and end the workflow.\", \"tool_call_id\": tool_call[\"id\"]})\n",
" # Go to END\n",
" goto = END\n",
" else:\n",
" raise ValueError(f\"Invalid tool call: {tool_call['name']}\")\n",
"\n",
"\n",
" # For \"respond\", we route to our agent to incorporate user feedback \n",
" elif response[\"type\"] == \"response\":\n",
" # User provided feedback\n",
" user_feedback = response[\"args\"]\n",
" if tool_call[\"name\"] == \"write_email\":\n",
" # Don't execute the tool, and add a message with the user feedback to incorporate into the email\n",
" result.append({\"role\": \"tool\", \"content\": f\"User gave feedback, which can we incorporate into the email. Feedback: {user_feedback}\", \"tool_call_id\": tool_call[\"id\"]})\n",
" elif tool_call[\"name\"] == \"schedule_meeting\":\n",
" # Don't execute the tool, and add a message with the user feedback to incorporate into the email\n",
" result.append({\"role\": \"tool\", \"content\": f\"User gave feedback, which can we incorporate into the meeting request. Feedback: {user_feedback}\", \"tool_call_id\": tool_call[\"id\"]})\n",
" elif tool_call[\"name\"] == \"Question\": \n",
" # Don't execute the tool, and add a message with the user feedback to incorporate into the email\n",
" result.append({\"role\": \"tool\", \"content\": f\"User answered the question, which can we can use for any follow up actions. Feedback: {user_feedback}\", \"tool_call_id\": tool_call[\"id\"]})\n",
" else:\n",
" raise ValueError(f\"Invalid tool call: {tool_call['name']}\")\n",
"\n",
" # Catch all other responses\n",
" else:\n",
" raise ValueError(f\"Invalid response: {response}\")\n",
" \n",
" # Update the state \n",
" update = {\n",
" \"messages\": result,\n",
" }\n",
"\n",
" return Command(goto=goto, update=update)"
]
},
{
"cell_type": "markdown",
"id": "c8e15e8c-d36b-484c-aad1-cb64e6e5ae90",
"metadata": {},
"source": [
"### Combine Graph\n",
"\n",
"We can combine the graph together"
]
},
{
"cell_type": "code",
"execution_count": 50,
"id": "fadaaa21-72bb-43bc-bf79-a2c7c29bd162",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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",
"text/plain": [
"<IPython.core.display.Image object>"
]
},
"execution_count": 50,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from email_assistant.utils import show_graph\n",
"from email_assistant.schemas import StateInput\n",
"\n",
"\n",
"# Build workflow\n",
"agent_builder = StateGraph(State)\n",
"\n",
"# Add nodes\n",
"agent_builder.add_node(\"llm_call\", llm_call)\n",
"agent_builder.add_node(\"tool_handler\", tool_handler)\n",
"\n",
"# Add edges\n",
"agent_builder.add_edge(START, \"llm_call\")\n",
"agent_builder.add_conditional_edges(\n",
" \"llm_call\",\n",
" should_continue,\n",
" {\n",
" \"tool_handler\": \"tool_handler\",\n",
" END: END,\n",
" },\n",
")\n",
"\n",
"# Compile the agent\n",
"response_agent = agent_builder.compile()\n",
"\n",
"# Build overall workflow\n",
"overall_workflow = (\n",
" StateGraph(State, input=StateInput)\n",
" .add_node(triage_router)\n",
" .add_node(triage_interrupt_handler)\n",
" .add_node(\"response_agent\", response_agent)\n",
" .add_edge(START, \"triage_router\")\n",
" \n",
")\n",
"\n",
"email_assistant = overall_workflow.compile()\n",
"show_graph(email_assistant, xray=True)"
]
},
{
"cell_type": "markdown",
"id": "c320fe59-dab0-4ccf-bee4-646a03bbae3e",
"metadata": {},
"source": [
"### Testing - Let's see this live!"
]
},
{
"cell_type": "markdown",
"id": "0938e212-ccf1-4a0e-a6e6-7fffc719cd1a",
"metadata": {},
"source": [
"#### Review of HITL Patterns\n",
"\n",
"**Triage Interruption** When an email is classified as \"notify\", the system interrupts to show the email to the human user\n",
"- *User Decision*: User can choose to ignore the notification or provide feedback to respond to the email\n",
"- *Flow Control*: If ignored, workflow ends; if user provides feedback, it flows to the Response Agent\n",
"\n",
"**Write Email**: System shows proposed email draft for human review\n",
"- *User Decision and Flow Control*: ignore (end workflow), respond with feedback, accept draft as-is, or edit draft\n",
"\n",
"**Schedule Meeting**: System shows proposed meeting details for human review\n",
"- *User Decision and Flow Control*: ignore (end workflow), respond with feedback, accept meeting details as-is, or edit details\n",
"\n",
"**Question**: System asks user a question to clarify information\n",
"- *User Decision and Flow Control*: ignore (end workflow) or respond with an answer"
]
},
{
"cell_type": "markdown",
"id": "e3bc22c1-baae-473b-b718-46b494115269",
"metadata": {},
"source": [
"#### Test 1. Interrupts Allow Us to Review and Accept Tool Calls"
]
},
{
"cell_type": "code",
"execution_count": 64,
"id": "eb2b7b85-8a09-49c8-9e61-b320a0ea106f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running the graph until the first interrupt...\n",
"📧 Classification: RESPOND - This email requires a response\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'schedule_meeting', 'args': {'attendees': ['pm@client.com', 'catherine@company.com'], 'subject': 'Tax Planning Strategies Discussion', 'duration_minutes': 45, 'preferred_day': '2025-05-06', 'start_time': 14}}\n"
]
}
],
"source": [
"import uuid\n",
"\n",
"# Email to respond to\n",
"email_input_respond = {\n",
" \"to\": \"Catherine Qiao <catherine@company.com>\",\n",
" \"author\": \"Project Manager <pm@client.com>\",\n",
" \"subject\": \"Tax season let's schedule call\",\n",
" \"email_thread\": \"Catherine,\\n\\nIt's tax season again. Before the filing deadline, I would highly recommend scheduling a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\\n\\nAre you available sometime soon? Tuesday or Thursday afternoon on May 6, 2025 or May 8, 2025 would work best for me, for about 45 minutes.\\n\\nRegards,\\nProject Manager\"\n",
"}\n",
"\n",
"# Compile the graph with checkpointer\n",
"graph = overall_workflow.compile(checkpointer=checkpointer)\n",
"thread_id_1 = uuid.uuid4()\n",
"thread_config_1 = {\"configurable\": {\"thread_id\": thread_id_1}}\n",
"\n",
"# Run the graph until a tool call that we choose to interrupt\n",
"print(\"Running the graph until the first interrupt...\")\n",
"for chunk in graph.stream({\"email_input\": email_input_respond}, config=thread_config_1):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "1edb6680-abf7-4077-a06b-7ce722351f91",
"metadata": {},
"source": [
"What happened here? \n",
"\n",
"We hit the [interrupt](https://langchain-ai.github.io/langgraph/concepts/interrupts/), which paused execution at the tool call. You can see the `action` (tool call name) and `args` (tool call arguments) that we interrupted displayed to the user.\n",
"\n",
"Now, how do we handle the interrupt? This is where the `Command` interface comes in. [The `Command` object has several powerful capabilities](https://langchain-ai.github.io/langgraph/how-tos/command/). We used it to direct the flow of the graph in prior notebooks: \n",
"- `goto`: Specifies which node to route to next\n",
"- `update`: Modifies the state before continuing execution\n",
"\n",
"Here, we'll use it to resume the graph from the interrupted state:\n",
"- `resume`: Provides the value to return from the interrupt call\n",
"\n",
"We can return whatever value our graph is designed to handle. In our case, the graph is designed to handle a list of dicts with a single key `type` that can be `accept`, `edit`, `ignore`, or `response`. So, we can simply pass `{\"type\": \"accept\"}` to the `resume` argument in order to tell the graph that we accept the tool call."
]
},
{
"cell_type": "code",
"execution_count": 65,
"id": "803f1d83-ebee-4469-a525-8bdcabdf5035",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user accepting the {'action': 'schedule_meeting', 'args': {'attendees': ['pm@client.com', 'catherine@company.com'], 'subject': 'Tax Planning Strategies Discussion', 'duration_minutes': 45, 'preferred_day': '2025-05-06', 'start_time': 14}} tool call...\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'write_email', 'args': {'to': 'pm@client.com', 'subject': \"Re: Tax season let's schedule call\", 'content': 'Dear Project Manager,\\n\\nThank you for reaching out regarding tax planning strategies ahead of the filing deadline. I appreciate your willingness to share suggestions that could potentially save me money.\\n\\nI am available on Tuesday, May 6, 2025, and have scheduled our 45-minute call for 2:00 PM that afternoon. Please let me know if there are any materials or documents you would like me to review prior to our discussion.\\n\\nLooking forward to our call.\\n\\nBest regards,\\nCatherine'}}\n"
]
}
],
"source": [
"from langgraph.types import Command\n",
"\n",
"print(f\"\\nSimulating user accepting the {Interrupt_Object.value[0]['action_request']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"accept\"}]), config=thread_config_1):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "8ab16472",
"metadata": {},
"source": [
"Now, lets accept the suggested email"
]
},
{
"cell_type": "code",
"execution_count": 66,
"id": "472b6d6b-dcbb-4488-a1cb-4a84f8c231d4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user accepting the {'action': 'write_email', 'args': {'to': 'pm@client.com', 'subject': \"Re: Tax season let's schedule call\", 'content': 'Dear Project Manager,\\n\\nThank you for reaching out regarding tax planning strategies ahead of the filing deadline. I appreciate your willingness to share suggestions that could potentially save me money.\\n\\nI am available on Tuesday, May 6, 2025, and have scheduled our 45-minute call for 2:00 PM that afternoon. Please let me know if there are any materials or documents you would like me to review prior to our discussion.\\n\\nLooking forward to our call.\\n\\nBest regards,\\nCatherine'}} tool call...\n"
]
}
],
"source": [
"print(f\"\\nSimulating user accepting the {Interrupt_Object.value[0]['action_request']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"accept\"}]), config=thread_config_1):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "code",
"execution_count": 67,
"id": "a84e420c-b223-47a0-b20f-85e91cd7be24",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Respond to the email: \n",
"\n",
"**Subject**: Tax season let's schedule call\n",
"**From**: Project Manager <pm@client.com>\n",
"**To**: Catherine Qiao <catherine@company.com>\n",
"\n",
"Catherine,\n",
"\n",
"It's tax season again. Before the filing deadline, I would highly recommend scheduling a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\n",
"\n",
"Are you available sometime soon? Tuesday or Thursday afternoon on May 6, 2025 or May 8, 2025 would work best for me, for about 45 minutes.\n",
"\n",
"Regards,\n",
"Project Manager\n",
"\n",
"---\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" check_calendar_availability (call_2cihDBOZJ4d5a3gftJARXlSa)\n",
" Call ID: call_2cihDBOZJ4d5a3gftJARXlSa\n",
" Args:\n",
" day: 2025-05-06\n",
" check_calendar_availability (call_mEsfut7xRHxqdrDqjecUZy6Z)\n",
" Call ID: call_mEsfut7xRHxqdrDqjecUZy6Z\n",
" Args:\n",
" day: 2025-05-08\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-05-06: 9:00 AM, 2:00 PM, 4:00 PM\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-05-08: 9:00 AM, 2:00 PM, 4:00 PM\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" schedule_meeting (call_kzXhz8j6A5vgeNzJZqJDgzBD)\n",
" Call ID: call_kzXhz8j6A5vgeNzJZqJDgzBD\n",
" Args:\n",
" attendees: ['pm@client.com', 'catherine@company.com']\n",
" subject: Tax Planning Strategies Discussion\n",
" duration_minutes: 45\n",
" preferred_day: 2025-05-06\n",
" start_time: 14\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Meeting 'Tax Planning Strategies Discussion' scheduled on Tuesday, May 06, 2025 at 14 for 45 minutes with 2 attendees\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" write_email (call_M0G8ilGlOzANrMpq1OgRgBQp)\n",
" Call ID: call_M0G8ilGlOzANrMpq1OgRgBQp\n",
" Args:\n",
" to: pm@client.com\n",
" subject: Re: Tax season let's schedule call\n",
" content: Dear Project Manager,\n",
"\n",
"Thank you for reaching out regarding tax planning strategies ahead of the filing deadline. I appreciate your willingness to share suggestions that could potentially save me money.\n",
"\n",
"I am available on Tuesday, May 6, 2025, and have scheduled our 45-minute call for 2:00 PM that afternoon. Please let me know if there are any materials or documents you would like me to review prior to our discussion.\n",
"\n",
"Looking forward to our call.\n",
"\n",
"Best regards,\n",
"Catherine\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Email sent to pm@client.com with subject 'Re: Tax season let's schedule call' and content: Dear Project Manager,\n",
"\n",
"Thank you for reaching out regarding tax planning strategies ahead of the filing deadline. I appreciate your willingness to share suggestions that could potentially save me money.\n",
"\n",
"I am available on Tuesday, May 6, 2025, and have scheduled our 45-minute call for 2:00 PM that afternoon. Please let me know if there are any materials or documents you would like me to review prior to our discussion.\n",
"\n",
"Looking forward to our call.\n",
"\n",
"Best regards,\n",
"Catherine\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_RAcpsY1kgLaXtb01DK6DQYuS)\n",
" Call ID: call_RAcpsY1kgLaXtb01DK6DQYuS\n",
" Args:\n",
" done: True\n"
]
}
],
"source": [
"state = graph.get_state(thread_config_1)\n",
"for m in state.values['messages']:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "626aa399-8cd9-4ac5-b69e-f9e3b66c5509",
"metadata": {},
"source": [
"#### Test 2. Interrupts Allow Us to Edit Tool Calls"
]
},
{
"cell_type": "markdown",
"id": "082c6693-726f-41ce-b6f3-129785db1703",
"metadata": {},
"source": [
"This test demonstrates how human modification works in the HITL flow:\n",
"1. We start with the same tax planning email as before\n",
"2. The agent proposes a meeting with the same parameters\n",
"3. This time, the user EDITS the meeting proposal to change:\n",
" - Duration from 45 to 30 minutes\n",
" - Meeting subject is made more concise\n",
"4. The agent adapts to these changes when drafting the email\n",
"5. The user further EDITS the email to be shorter and less formal\n",
"6. The workflow completes with both modifications incorporated\n",
"\n",
"This scenario showcases one of the most powerful aspects of HITL: \n",
"\n",
"* Users can make precise modifications to agent actions before they are executed, ensuring the final outcome matches their preferences without having to handle all the details themselves."
]
},
{
"cell_type": "code",
"execution_count": 72,
"id": "dfdbb290-0f77-484f-bc4a-ccaaa2ffdcf6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running the graph until the first interrupt...\n",
"📧 Classification: RESPOND - This email requires a response\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'schedule_meeting', 'args': {'attendees': ['pm@client.com', 'catherine@company.com'], 'subject': 'Tax Planning Strategies Discussion', 'duration_minutes': 45, 'preferred_day': '2025-05-06', 'start_time': 14}}\n"
]
}
],
"source": [
"# Same email as before\n",
"email_input_respond = {\n",
" \"to\": \"Catherine Qiao <catherine@company.com>\",\n",
" \"author\": \"Project Manager <pm@client.com>\",\n",
" \"subject\": \"Tax season let's schedule call\",\n",
" \"email_thread\": \"Catherine,\\n\\nIt's tax season again. Before the filing deadline, I would highly recommend scheduling a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\\n\\nAre you available sometime soon? Tuesday or Thursday afternoon on May 6, 2025 or May 8, 2025 would work best for me, for about 45 minutes.\\n\\nRegards,\\nProject Manager\"\n",
"}\n",
"\n",
"# Compile the graph with new thread\n",
"graph = overall_workflow.compile(checkpointer=checkpointer)\n",
"thread_id_2 = uuid.uuid4()\n",
"thread_config_2 = {\"configurable\": {\"thread_id\": thread_id_2}}\n",
"\n",
"# Run the graph until the first interrupt - will be classified as \"respond\" and the agent will create a write_email tool call\n",
"print(\"Running the graph until the first interrupt...\")\n",
"for chunk in graph.stream({\"email_input\": email_input_respond}, config=thread_config_2):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "196a7b2a-42cc-41f3-8ded-135359104cf5",
"metadata": {},
"source": [
"**Edit the `schedule_meeting` tool call**\n",
"\n",
"When the agent proposes the initial meeting schedule, we now simulate the user making modifications through the edit functionality. This demonstrates how the `edit` response type works:\n",
"\n",
"1. The user receives the same meeting proposal as in the previous test\n",
"2. Instead of accepting, they modify the parameters:\n",
" - Reducing duration from 45 to 30 minutes\n",
" - Keeping the same day and time\n",
"3. The `edit` response includes the complete set of modified arguments\n",
"4. The interrupt handler replaces the original tool arguments with these edited ones\n",
"5. The tool is executed with the user's modifications\n",
"\n",
"This shows how edit capability gives users precise control over agent actions while still letting the agent handle the execution details."
]
},
{
"cell_type": "code",
"execution_count": 73,
"id": "d871f459-39e4-4a70-87b4-1b5dc7e00f88",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user editing the schedule_meeting tool call...\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'write_email', 'args': {'to': 'pm@client.com', 'subject': \"Re: Tax season let's schedule call\", 'content': \"Hello,\\n\\nThank you for reaching out and for your tax planning suggestions. I am available on Tuesday, May 6, 2025, and have scheduled a 30-minute call with you at 2:00 PM that afternoon to discuss tax strategies ahead of the filing deadline. If you have specific topics or documents you'd like me to review in advance, please let me know.\\n\\nLooking forward to our discussion.\\n\\nBest regards,\\nCatherine\"}}\n"
]
}
],
"source": [
"# Now simulate user editing the schedule_meeting tool call\n",
"print(\"\\nSimulating user editing the schedule_meeting tool call...\")\n",
"edited_schedule_args = {\n",
" \"attendees\": [\"pm@client.com\", \"catherine@company.com\"],\n",
" \"subject\": \"Tax Planning Discussion\",\n",
" \"duration_minutes\": 30, # Changed from 45 to 30\n",
" \"preferred_day\": \"2025-05-06\",\n",
" \"start_time\": 14 \n",
"}\n",
"\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"edit\", \"args\": {\"args\": edited_schedule_args}}]), config=thread_config_2):\n",
" # Inspect response_agent most recent message\n",
" if 'response_agent' in chunk:\n",
" chunk['response_agent']['messages'][-1].pretty_print()\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "a9374f3b-2077-4b91-8f9f-33e8b8134cf0",
"metadata": {},
"source": [
"**Edit the `write_email` tool call**\n",
"\n",
"After accepting the modified meeting schedule, the agent drafts an email reflecting the 30-minute duration. Now we demonstrate how editing works with email content:\n",
"\n",
"1. The agent has adapted its email to mention the shorter 30-minute duration\n",
"2. We simulate the user wanting an even more significant change to the email:\n",
" - Completely rewriting the content to be shorter and less formal\n",
" - Changing the meeting day mentioned in the email (showing how users can correct agent mistakes)\n",
" - Requesting confirmation rather than stating the meeting as definite\n",
"3. The `edit` response contains the complete new email content\n",
"4. The tool arguments are updated with this edited content\n",
"5. The email is sent with the user's preferred wording\n",
"\n",
"This example shows the power of HITL for complex communication tasks - the agent handles the structure and initial content, while humans can refine tone, style, and substance."
]
},
{
"cell_type": "code",
"execution_count": 74,
"id": "32983d30-8364-4b31-99ad-80f76929c7a8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user editing the write_email tool call...\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_UcubTmeEcWqR73mmlJmNOqWu)\n",
" Call ID: call_UcubTmeEcWqR73mmlJmNOqWu\n",
" Args:\n",
" done: True\n"
]
}
],
"source": [
"# Now simulate user editing the write_email tool call\n",
"print(\"\\nSimulating user editing the write_email tool call...\")\n",
"edited_email_args = {\n",
" \"to\": \"pm@client.com\",\n",
" \"subject\": \"Re: Tax season let's schedule call\",\n",
" \"content\": \"Hello Project Manager,\\n\\nThank you for reaching out about tax planning. I believe half an hour would be sufficient for the discussion. I scheduled a 30-minute call on May 6, 2025 at 3:00 PM. Would that work for you?\\n\\nBest regards,\\nCatherine Qiao\"\n",
"}\n",
"\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"edit\", \"args\": {\"args\": edited_email_args}}]), config=thread_config_2):\n",
" # Inspect response_agent most recent message\n",
" if 'response_agent' in chunk:\n",
" chunk['response_agent']['messages'][-1].pretty_print()\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "831d0b7b-718a-4aed-ba58-0ac9664b4d80",
"metadata": {},
"source": [
"Look at the full message history, and see the trace:\n",
"\n",
"https://smith.langchain.com/public/64a4c2c3-0093-463e-b4e7-bbe7060a2beb/r\n",
"\n",
"We can see that user feedback in incorporated into the tool calls. "
]
},
{
"cell_type": "code",
"execution_count": 75,
"id": "8b6565d8",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Respond to the email: \n",
"\n",
"**Subject**: Tax season let's schedule call\n",
"**From**: Project Manager <pm@client.com>\n",
"**To**: Catherine Qiao <catherine@company.com>\n",
"\n",
"Catherine,\n",
"\n",
"It's tax season again. Before the filing deadline, I would highly recommend scheduling a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\n",
"\n",
"Are you available sometime soon? Tuesday or Thursday afternoon on May 6, 2025 or May 8, 2025 would work best for me, for about 45 minutes.\n",
"\n",
"Regards,\n",
"Project Manager\n",
"\n",
"---\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" check_calendar_availability (call_LkZobcidCWJGabacbhSXTPgj)\n",
" Call ID: call_LkZobcidCWJGabacbhSXTPgj\n",
" Args:\n",
" day: 2025-05-06\n",
" check_calendar_availability (call_E08wRcMXpftTr9vrqpNMl3WE)\n",
" Call ID: call_E08wRcMXpftTr9vrqpNMl3WE\n",
" Args:\n",
" day: 2025-05-08\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-05-06: 9:00 AM, 2:00 PM, 4:00 PM\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-05-08: 9:00 AM, 2:00 PM, 4:00 PM\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" schedule_meeting (call_2ReTnV96Im4nkVCjyEXwEFay)\n",
" Call ID: call_2ReTnV96Im4nkVCjyEXwEFay\n",
" Args:\n",
" attendees: ['pm@client.com', 'catherine@company.com']\n",
" subject: Tax Planning Discussion\n",
" duration_minutes: 30\n",
" preferred_day: 2025-05-06\n",
" start_time: 14\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Meeting 'Tax Planning Discussion' scheduled on Tuesday, May 06, 2025 at 14 for 30 minutes with 2 attendees\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" write_email (call_dfW9Qua5DC1Q12UPn8DaEwlT)\n",
" Call ID: call_dfW9Qua5DC1Q12UPn8DaEwlT\n",
" Args:\n",
" to: pm@client.com\n",
" subject: Re: Tax season let's schedule call\n",
" content: Hello Project Manager,\n",
"\n",
"Thank you for reaching out about tax planning. I believe half an hour would be sufficient for the discussion. I scheduled a 30-minute call on May 6, 2025 at 3:00 PM. Would that work for you?\n",
"\n",
"Best regards,\n",
"Catherine Qiao\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Email sent to pm@client.com with subject 'Re: Tax season let's schedule call' and content: Hello Project Manager,\n",
"\n",
"Thank you for reaching out about tax planning. I believe half an hour would be sufficient for the discussion. I scheduled a 30-minute call on May 6, 2025 at 3:00 PM. Would that work for you?\n",
"\n",
"Best regards,\n",
"Catherine Qiao\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_UcubTmeEcWqR73mmlJmNOqWu)\n",
" Call ID: call_UcubTmeEcWqR73mmlJmNOqWu\n",
" Args:\n",
" done: True\n"
]
}
],
"source": [
"state = graph.get_state(thread_config_2)\n",
"for m in state.values['messages']:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "b83a1a1e",
"metadata": {},
"source": [
"#### Test 3. Interrupts Allow Us to Provide Feedback on Tool Calls\n",
"\n",
"This test set demonstrates the \"response\" capability - providing feedback without editing or accepting:\n",
"\n",
"1. First, we test feedback for meeting scheduling:\n",
" - The user provides specific preferences (30 minutes instead of 45, and afternoon meetings)\n",
" - The agent incorporates this feedback into a revised proposal\n",
" - The user then accepts the revised meeting schedule\n",
"\n",
"2. Second, we test feedback for email drafting:\n",
" - The user requests a shorter, less formal email with a specific closing statement\n",
" - The agent completely rewrites the email according to this guidance\n",
" - The user accepts the new draft\n",
"\n",
"3. Lastly, we test feedback for questions:\n",
" - For the brunch invitation, the user answers the question with additional context\n",
" - The agent uses this information to draft an appropriate email response\n",
" - The workflow proceeds with the user's input integrated\n",
"\n",
"The \"response\" capability bridges the gap between acceptance and editing - users can guide the agent without having to write the full content themselves. This is especially powerful for:\n",
"- Adjusting tone and style\n",
"- Adding context the agent missed\n",
"- Redirecting the agent's approach\n",
"- Answering questions in a way that shapes the next steps"
]
},
{
"cell_type": "code",
"execution_count": 76,
"id": "7460f14a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running the graph until the first interrupt...\n",
"📧 Classification: RESPOND - This email requires a response\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'schedule_meeting', 'args': {'attendees': ['pm@client.com'], 'subject': 'Tax Planning Strategy Discussion', 'duration_minutes': 45, 'preferred_day': '2025-08-19', 'start_time': 14}}\n"
]
}
],
"source": [
"# Respond - Meeting Request Email\n",
"email_input_respond = {\n",
" \"to\": \"Catherine Qiao <catherine@company.com>\",\n",
" \"author\": \"Project Manager <pm@client.com>\",\n",
" \"subject\": \"Tax season let's schedule call\",\n",
" \"email_thread\": \"Catherine,\\n\\nIt's tax season again, and I wanted to schedule a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\\n\\nAre you available sometime next week? Tuesday or Thursday afternoon would work best for me, for about 45 minutes.\\n\\nRegards,\\nProject Manager\"\n",
"}\n",
"\n",
"# Compile the graph\n",
"checkpointer = InMemorySaver()\n",
"graph = overall_workflow.compile(checkpointer=checkpointer)\n",
"thread_id_5 = uuid.uuid4()\n",
"thread_config_5 = {\"configurable\": {\"thread_id\": thread_id_5}}\n",
"\n",
"# Run the graph until the first interrupt \n",
"# Email will be classified as \"respond\" \n",
"# Agent will create a schedule_meeting and write_email tool call\n",
"print(\"Running the graph until the first interrupt...\")\n",
"for chunk in graph.stream({\"email_input\": email_input_respond}, config=thread_config_5):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "d8d57575",
"metadata": {},
"source": [
"Provide feedback for the `schedule_meeting` tool call\n",
"\n",
"Now we explore the feedback capability for meeting scheduling:\n",
"\n",
"1. The agent proposes the standard 45-minute meeting on Tuesday at 2:00 PM\n",
"2. Instead of accepting or editing, we provide feedback in natural language\n",
"3. Our feedback specifies two preferences:\n",
" - Shorter meeting (30 minutes instead of 45)\n",
" - Preference for afternoon meetings (after 2pm)\n",
"4. The agent receives this feedback through the `response` type\n",
"5. The interrupt handler adds this feedback as a message to the state\n",
"6. The agent processes this feedback and generates a new tool call incorporating these preferences\n",
"\n",
"Unlike direct editing, which requires specifying the entire set of parameters, feedback allows users to express their preferences conversationally. The agent must then interpret this feedback and apply it appropriately to create a revised proposal."
]
},
{
"cell_type": "code",
"execution_count": 77,
"id": "fcb583a6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user providing feedback for the schedule_meeting tool call...\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'schedule_meeting', 'args': {'attendees': ['pm@client.com'], 'subject': 'Tax Planning Strategy Discussion', 'duration_minutes': 30, 'preferred_day': '2025-08-19', 'start_time': 14}}\n"
]
}
],
"source": [
"print(f\"\\nSimulating user providing feedback for the {Interrupt_Object.value[0]['action_request']['action']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"response\", \"args\": \"Please schedule this for 30 minutes instead of 45 minutes, and I prefer afternoon meetings after 2pm.\"}]), config=thread_config_5):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "f360f1e7",
"metadata": {},
"source": [
"Accept the `schedule_meeting` tool call after providing feedback"
]
},
{
"cell_type": "code",
"execution_count": 78,
"id": "ae2bdcff",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user accepting the {'action': 'schedule_meeting', 'args': {'attendees': ['pm@client.com'], 'subject': 'Tax Planning Strategy Discussion', 'duration_minutes': 30, 'preferred_day': '2025-08-19', 'start_time': 14}} tool call...\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'write_email', 'args': {'to': 'pm@client.com', 'subject': \"Re: Tax season let's schedule call\", 'content': 'Dear Project Manager,\\n\\nThank you for reaching out. I have scheduled our call to discuss tax planning strategies for Tuesday, August 19, 2025, at 2:00 PM for 30 minutes, as this aligns with your preference for an afternoon meeting.\\n\\nIf you have any specific points or suggestions youd like me to prepare for in advance, please let me know.\\n\\nLooking forward to our discussion.\\n\\nBest regards,\\nCatherine'}}\n"
]
}
],
"source": [
"print(f\"\\nSimulating user accepting the {Interrupt_Object.value[0]['action_request']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"accept\"}]), config=thread_config_5):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "ed6cbd29",
"metadata": {},
"source": [
"Now provide feedback for the `write_email` tool call\n",
"\n",
"After accepting the revised meeting schedule, the agent drafts an email. We now test feedback for email content:\n",
"\n",
"1. The agent's email is relatively formal and detailed\n",
"2. We provide stylistic feedback requesting:\n",
" - A shorter, more concise email\n",
" - A less formal tone\n",
" - A specific closing statement about looking forward to the meeting\n",
"3. The agent processes this feedback to completely rewrite the email\n",
"4. The new draft is much shorter, more casual, and includes the requested closing\n",
"\n",
"This demonstrates the power of natural language feedback for content creation:\n",
"- Users don't need to rewrite the entire email themselves\n",
"- They can provide high-level guidance on style, tone, and content\n",
"- The agent handles the actual writing based on this guidance\n",
"- The result better matches user preferences while preserving the essential information\n",
"\n",
"The message history shows both the original and revised emails, clearly showing how the feedback was incorporated."
]
},
{
"cell_type": "code",
"execution_count": 79,
"id": "35497037",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user providing feedback for the write_email tool call...\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'write_email', 'args': {'to': 'pm@client.com', 'subject': \"Re: Tax season let's schedule call\", 'content': \"Hi there,\\n\\nI've put our tax chat on the calendar for Tuesday, Aug 19 at 2:00 PM (for 30 minutes). Looking forward to hearing your money-saving ideas—after all, who doesn't like a good tax deduction?\\n\\nSee you then!\\n\\nCatherine\"}}\n"
]
}
],
"source": [
"print(f\"\\nSimulating user providing feedback for the {Interrupt_Object.value[0]['action_request']['action']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"response\", \"args\": \"Shorter and less formal. Incorporate a joke.\"}]), config=thread_config_5):\n",
" # Inspect response_agent most recent message\n",
" if 'response_agent' in chunk:\n",
" chunk['response_agent']['messages'][-1].pretty_print()\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "4ecea978",
"metadata": {},
"source": [
"Accept the `write_email` tool call after providing feedback"
]
},
{
"cell_type": "code",
"execution_count": 80,
"id": "c021db1b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user accepting the {'action': 'write_email', 'args': {'to': 'pm@client.com', 'subject': \"Re: Tax season let's schedule call\", 'content': \"Hi there,\\n\\nI've put our tax chat on the calendar for Tuesday, Aug 19 at 2:00 PM (for 30 minutes). Looking forward to hearing your money-saving ideas—after all, who doesn't like a good tax deduction?\\n\\nSee you then!\\n\\nCatherine\"}} tool call...\n"
]
}
],
"source": [
"print(f\"\\nSimulating user accepting the {Interrupt_Object.value[0]['action_request']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"accept\"}]), config=thread_config_5):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "80158c02",
"metadata": {},
"source": [
"Look at the full message history - We can see that user feedback in incorporated into the tool calls. "
]
},
{
"cell_type": "code",
"execution_count": 81,
"id": "5de3f539",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Respond to the email: \n",
"\n",
"**Subject**: Tax season let's schedule call\n",
"**From**: Project Manager <pm@client.com>\n",
"**To**: Catherine Qiao <catherine@company.com>\n",
"\n",
"Catherine,\n",
"\n",
"It's tax season again, and I wanted to schedule a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\n",
"\n",
"Are you available sometime next week? Tuesday or Thursday afternoon would work best for me, for about 45 minutes.\n",
"\n",
"Regards,\n",
"Project Manager\n",
"\n",
"---\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" check_calendar_availability (call_SaamiRimQiSH9Puxy2ygKtOw)\n",
" Call ID: call_SaamiRimQiSH9Puxy2ygKtOw\n",
" Args:\n",
" day: 2025-08-19\n",
" check_calendar_availability (call_3FdoFFJlZNgBOEEXEOsFyXEG)\n",
" Call ID: call_3FdoFFJlZNgBOEEXEOsFyXEG\n",
" Args:\n",
" day: 2025-08-21\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-08-19: 9:00 AM, 2:00 PM, 4:00 PM\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-08-21: 9:00 AM, 2:00 PM, 4:00 PM\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" schedule_meeting (call_JUtFG50HBR5NxFUoaVzp5nk9)\n",
" Call ID: call_JUtFG50HBR5NxFUoaVzp5nk9\n",
" Args:\n",
" attendees: ['pm@client.com']\n",
" subject: Tax Planning Strategy Discussion\n",
" duration_minutes: 45\n",
" preferred_day: 2025-08-19\n",
" start_time: 14\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"User gave feedback, which can we incorporate into the meeting request. Feedback: Please schedule this for 30 minutes instead of 45 minutes, and I prefer afternoon meetings after 2pm.\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" schedule_meeting (call_AkQTBx8AkDkURymAfqwzFlCS)\n",
" Call ID: call_AkQTBx8AkDkURymAfqwzFlCS\n",
" Args:\n",
" attendees: ['pm@client.com']\n",
" subject: Tax Planning Strategy Discussion\n",
" duration_minutes: 30\n",
" preferred_day: 2025-08-19\n",
" start_time: 14\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Meeting 'Tax Planning Strategy Discussion' scheduled on Tuesday, August 19, 2025 at 14 for 30 minutes with 1 attendees\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" write_email (call_edc5dnw3buhTYeyl3GEkNiQJ)\n",
" Call ID: call_edc5dnw3buhTYeyl3GEkNiQJ\n",
" Args:\n",
" to: pm@client.com\n",
" subject: Re: Tax season let's schedule call\n",
" content: Dear Project Manager,\n",
"\n",
"Thank you for reaching out. I have scheduled our call to discuss tax planning strategies for Tuesday, August 19, 2025, at 2:00 PM for 30 minutes, as this aligns with your preference for an afternoon meeting.\n",
"\n",
"If you have any specific points or suggestions youd like me to prepare for in advance, please let me know.\n",
"\n",
"Looking forward to our discussion.\n",
"\n",
"Best regards,\n",
"Catherine\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"User gave feedback, which can we incorporate into the email. Feedback: Shorter and less formal. Incorporate a joke.\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" write_email (call_9oatE0AXex7BY9pqgodV702T)\n",
" Call ID: call_9oatE0AXex7BY9pqgodV702T\n",
" Args:\n",
" to: pm@client.com\n",
" subject: Re: Tax season let's schedule call\n",
" content: Hi there,\n",
"\n",
"I've put our tax chat on the calendar for Tuesday, Aug 19 at 2:00 PM (for 30 minutes). Looking forward to hearing your money-saving ideas—after all, who doesn't like a good tax deduction?\n",
"\n",
"See you then!\n",
"\n",
"Catherine\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Email sent to pm@client.com with subject 'Re: Tax season let's schedule call' and content: Hi there,\n",
"\n",
"I've put our tax chat on the calendar for Tuesday, Aug 19 at 2:00 PM (for 30 minutes). Looking forward to hearing your money-saving ideas—after all, who doesn't like a good tax deduction?\n",
"\n",
"See you then!\n",
"\n",
"Catherine\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_loYqHgcfw8GtsRH24ZLbE9SD)\n",
" Call ID: call_loYqHgcfw8GtsRH24ZLbE9SD\n",
" Args:\n",
" done: True\n"
]
}
],
"source": [
"state = graph.get_state(thread_config_5)\n",
"for m in state.values['messages']:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "7254240c-1737-4733-98f8-f3de35b5f106",
"metadata": {},
"source": [
"#### Test 4. Interrupts Enable New Tools"
]
},
{
"cell_type": "markdown",
"id": "0d4241a9-e886-4e84-a1d1-5364ada8ec28",
"metadata": {},
"source": [
"Now let's try an email that calls the `Question` tool to provide feedback\n",
"\n",
"Finally, we test how feedback works with the `Question` tool:\n",
"\n",
"1. For the brunch invitation email, the agent asks about preferred day and time\n",
"2. Instead of ignoring, we provide a substantive response with additional context:\n",
" - Confirming we want to invite the people mentioned\n",
" - Noting we need to check which weekend works best\n",
" - Adding information about needing a reservation\n",
"3. The agent uses this information to:\n",
" - Draft a comprehensive email response incorporating all our feedback\n",
" - Notice we didn't provide a specific day/time, so it suggests checking the calendar\n",
" - Include the detail about making a reservation\n",
"4. The complete email reflects both the original request and our additional guidance\n",
"\n",
"This demonstrates how question responses can shape the entire workflow:\n",
"- Questions let the agent gather missing information\n",
"- User responses can include both direct answers and additional context\n",
"- The agent integrates all this information into its next actions\n",
"- The final outcome reflects the collaborative intelligence of both human and AI"
]
},
{
"cell_type": "code",
"execution_count": 83,
"id": "c15726bd-a20d-4903-a5af-5514a1c5fb8c",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running the graph until the first interrupt...\n",
"📧 Classification: RESPOND - This email requires a response\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'Question', 'args': {'content': 'Your roommate is asking whether you want Italian or Indian food for dinner tonight. Do you have a preference?'}}\n"
]
}
],
"source": [
"# Respond\n",
"email_input_respond = {\n",
" \"to\": \"Catherine Qiao <catherine@company.com>\",\n",
" \"author\": \"Roommate <roommate@home.com>\",\n",
" \"subject\": \"Dinner?\",\n",
" \"email_thread\": \"Hey, do you want italian or indian tonight?\"}\n",
"\n",
"# Compile the graph\n",
"graph = overall_workflow.compile(checkpointer=checkpointer)\n",
"thread_id_6 = uuid.uuid4()\n",
"thread_config_6 = {\"configurable\": {\"thread_id\": thread_id_6}}\n",
"\n",
"# Run the graph until the first interrupt\n",
"print(\"Running the graph until the first interrupt...\")\n",
"for chunk in graph.stream({\"email_input\": email_input_respond}, config=thread_config_6):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "f00285d4-fe8f-441f-8574-ea253d60762c",
"metadata": {},
"source": [
"Provide feedback for the `Question` tool call"
]
},
{
"cell_type": "code",
"execution_count": 84,
"id": "8d05ee04-746a-4794-bfa7-3579f06ffe63",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user providing feedback for the Question tool call...\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'write_email', 'args': {'to': 'roommate@home.com', 'subject': 'Re: Dinner?', 'content': 'Indian sounds great for tonight!'}}\n"
]
}
],
"source": [
"print(f\"\\nSimulating user providing feedback for the {Interrupt_Object.value[0]['action_request']['action']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"response\", \"args\": \"Let's do indian.\"}]), config=thread_config_6):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "7ac9bd5f-c282-4334-bc2b-ce85f12fb3b4",
"metadata": {},
"source": [
"Accept the `write_email` tool call"
]
},
{
"cell_type": "code",
"execution_count": 85,
"id": "e33d7e3a-47af-42e5-8dad-ad39d1f0adf5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user accepting the write_email tool call...\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_ncbpvnJbwvZIROUfOTPAGTBl)\n",
" Call ID: call_ncbpvnJbwvZIROUfOTPAGTBl\n",
" Args:\n",
" done: True\n"
]
}
],
"source": [
"print(f\"\\nSimulating user accepting the {Interrupt_Object.value[0]['action_request']['action']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"accept\"}]), config=thread_config_6):\n",
" # Inspect response_agent most recent message\n",
" if 'response_agent' in chunk:\n",
" chunk['response_agent']['messages'][-1].pretty_print()\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "b32eca86-5fe0-4c47-a41a-13f5aef13ff0",
"metadata": {},
"source": [
"Look at the full message history, and see the trace:\n",
"\n",
"https://smith.langchain.com/public/3e3df0ea-cb60-4fca-8506-4246809127bd/r\n",
"\n",
"We can see that user feedback in incorporated into the email response."
]
},
{
"cell_type": "code",
"execution_count": 86,
"id": "2ed70be6-5890-4912-8c69-f8a126cc3bc5",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Respond to the email: \n",
"\n",
"**Subject**: Dinner?\n",
"**From**: Roommate <roommate@home.com>\n",
"**To**: Catherine Qiao <catherine@company.com>\n",
"\n",
"Hey, do you want italian or indian tonight?\n",
"\n",
"---\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Question (call_Cl0YfXF8gs9mjxJPUV7qqjJx)\n",
" Call ID: call_Cl0YfXF8gs9mjxJPUV7qqjJx\n",
" Args:\n",
" content: Your roommate is asking whether you want Italian or Indian food for dinner tonight. Do you have a preference?\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"User answered the question, which can we can use for any follow up actions. Feedback: Let's do indian.\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" write_email (call_vgxnGxnl3QrHav0oNNdOQrCr)\n",
" Call ID: call_vgxnGxnl3QrHav0oNNdOQrCr\n",
" Args:\n",
" to: roommate@home.com\n",
" subject: Re: Dinner?\n",
" content: Indian sounds great for tonight!\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Email sent to roommate@home.com with subject 'Re: Dinner?' and content: Indian sounds great for tonight!\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_ncbpvnJbwvZIROUfOTPAGTBl)\n",
" Call ID: call_ncbpvnJbwvZIROUfOTPAGTBl\n",
" Args:\n",
" done: True\n"
]
}
],
"source": [
"state = graph.get_state(thread_config_6)\n",
"for m in state.values['messages']:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "492ce8b1",
"metadata": {},
"source": [
"<div align=\"center\">\n",
"<h1 style=\"color: blue; padding-top: 30px; padding-bottom: 30px\"><b>10 minute break</b></h1>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"id": "99d41db0-10c9-4661-a744-470e4e257e42",
"metadata": {},
"source": [
"## Part III. Memory"
]
},
{
"cell_type": "markdown",
"id": "ef1004e5-6329-443d-8fde-033734526b88",
"metadata": {},
"source": [
"We have an email assistant that uses a router to triage emails and then passes the email to the agent for response generation. We've also evaluated it and added human-in-the-loop (HITL) to review specific tool calls. Now, we add memory, giving our assistant the ability to remember our HITL feedback!\n",
"\n",
"![overview-img](img/overview_memory.png)"
]
},
{
"cell_type": "markdown",
"id": "a3bc7c74-752f-4a78-8d6e-7294073628ae",
"metadata": {},
"source": [
"### Introduction: Thread-Scoped and Across-Thread Memory"
]
},
{
"cell_type": "markdown",
"id": "c7966e8d-f64d-4b6e-b4fa-134c233d4404",
"metadata": {},
"source": [
"First, it's worth explaining how [memory works in LangGraph](https://langchain-ai.github.io/langgraph/concepts/memory/). LangGraph offers two distinct types of memory that serve complementary purposes:\n",
"\n",
"**Thread-Scoped Memory (Short-term)** operates within the boundaries of a single conversation thread. It's automatically managed as part of the graph's state and persisted through thread-scoped checkpoints. This memory type retains conversation history, uploaded files, retrieved documents, and other artifacts generated during the interaction. Think of it as the working memory that maintains context within one specific conversation, allowing the agent to reference earlier messages or actions without starting from scratch each time.\n",
"\n",
"**Across-Thread Memory (Long-term)** extends beyond individual conversations, creating a persistent knowledge base that spans multiple sessions. This memory is stored as JSON documents in a memory store, organized by namespaces (like folders) and distinct keys (like filenames). Unlike thread-scoped memory, this information persists even after conversations end, enabling the system to recall user preferences, past decisions, and accumulated knowledge. This is what allows an agent to truly learn and adapt over time, rather than treating each interaction as isolated.\n",
"\n",
"![short-vs-long-term-memory](img/short-vs-long.png)\n",
"\n",
"The [Store](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) is the foundation of this architecture, providing a flexible database where memories can be organized, retrieved, and updated. What makes this approach powerful is that regardless of which memory type you're working with, the same Store interface provides consistent access patterns. This allows your agent's code to remain unchanged whether you're using a simple in-memory implementation during development or a production-grade database in deployment. \n",
"\n",
"### LangGraph Store\n",
"\n",
"LangGraph offers different [Store implementations](https://langchain-ai.github.io/langgraph/reference/store/#langgraph.store.base.BaseStore) depending on your [deployment](https://langchain-ai.github.io/langgraph/tutorials/deployment/#other-deployment-options):\n",
"\n",
"1. **In-Memory (e.g., notebooks)**:\n",
" - Uses `from langgraph.store.memory import InMemoryStore`\n",
" - Purely a Python dictionary in memory with no persistence\n",
" - Data is lost when the process terminates\n",
" - Useful for quick experiments and testing\n",
" - Semantic search can be configured as shown [here](https://langchain-ai.github.io/langgraph/how-tos/memory/semantic-search/)\n",
"\n",
"2. **Local Development with `langgraph dev`**:\n",
" - Similar to InMemoryStore but with pseudo-persistence\n",
" - Data is pickled to the local filesystem between restarts\n",
" - Lightweight and fast, no need for external databases\n",
" - Semantic search can be configured as shown [here](https://langchain-ai.github.io/langgraph/cloud/deployment/semantic_search/)\n",
" - Great for development but not designed for production use\n",
"\n",
"3. **LangGraph Platform or Production Deployments**:\n",
" - Uses PostgreSQL with pgvector for production-grade persistence\n",
" - Fully persistent data storage with reliable backups\n",
" - Scalable for larger datasets\n",
" - Semantic search can be configured as shown [here](https://langchain-ai.github.io/langgraph/cloud/deployment/semantic_search/)\n",
" - Default distance metric is cosine similarity (customizable)\n",
"\n",
"Let's use the `InMemoryStore` here in the notebook! "
]
},
{
"cell_type": "code",
"execution_count": 91,
"id": "3632b034-0237-4dd6-bca6-20346e86233b",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.store.memory import InMemoryStore\n",
"in_memory_store = InMemoryStore()"
]
},
{
"cell_type": "markdown",
"id": "6adf24f8-aa0b-4476-8bda-d5b603a9cec7",
"metadata": {},
"source": [
"Memories are namespaced by a tuple, which in this specific example will be (`<user_id>`, \"memories\"). The namespace can be any length and represent anything, it does not have to be user specific."
]
},
{
"cell_type": "code",
"execution_count": 92,
"id": "d5a0bc4c-5368-48d6-8c3d-7dc98654f4ce",
"metadata": {},
"outputs": [],
"source": [
"user_id = \"1\"\n",
"namespace_for_memory = (user_id, \"memories\")"
]
},
{
"cell_type": "markdown",
"id": "6f721548-8ef3-46fd-b943-c8bfab94dc37",
"metadata": {},
"source": [
"We use the `store.put` method to save memories to our namespace in the store. When we do this, we specify the namespace, as defined above, and a key-value pair for the memory: the key is simply a unique identifier for the memory (memory_id) and the value (a dictionary) is the memory itself."
]
},
{
"cell_type": "code",
"execution_count": 89,
"id": "5bd5d0b2-ae1d-4247-a230-04e5b7660e3e",
"metadata": {},
"outputs": [],
"source": [
"memory_id = str(uuid.uuid4())\n",
"memory = {\"food_preference\" : \"I like pizza\"}\n",
"in_memory_store.put(namespace_for_memory, memory_id, memory)"
]
},
{
"cell_type": "markdown",
"id": "70a5fd3a-a0ac-4078-8035-272f9ff3f1ed",
"metadata": {},
"source": [
"We can read out memories in our namespace using the `store.search` method, which will return all memories for a given user as a list. The most recent memory is the last in the list. Each memory type is a Python class (`Item`) with certain attributes. We can access it as a dictionary by converting via `.dict`. The attributes it has are shown below, but the most important one is typically `value`."
]
},
{
"cell_type": "code",
"execution_count": 90,
"id": "30c30f82-cd6a-4ca7-af4a-980dacd9abb1",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'namespace': ['1', 'memories'],\n",
" 'key': '6b71b487-c3de-4d24-a28a-376812fcb6f0',\n",
" 'value': {'food_preference': 'I like pizza'},\n",
" 'created_at': '2025-08-13T03:10:08.481617+00:00',\n",
" 'updated_at': '2025-08-13T03:10:08.481622+00:00',\n",
" 'score': None}"
]
},
"execution_count": 90,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"memories = in_memory_store.search(namespace_for_memory)\n",
"memories[-1].dict()"
]
},
{
"cell_type": "markdown",
"id": "5fc3df0c-ceb7-423d-80be-2acdfb950e51",
"metadata": {},
"source": [
"To use this in a graph, all we need to do is compile the graph with the store:"
]
},
{
"cell_type": "code",
"execution_count": 48,
"id": "58bf13de-071d-4514-8271-a866826e3600",
"metadata": {},
"outputs": [],
"source": [
"# Compile the graph with the checkpointer and store\n",
"# graph = graph.compile(checkpointer=checkpointer, store=in_memory_store)"
]
},
{
"cell_type": "markdown",
"id": "71d690d4-8443-450e-b1fa-8278e0e2c1c1",
"metadata": {},
"source": [
"The store is then accessible in any node of the graph, as we'll see below!\n",
"\n",
"## Adding Memory to our Assistant\n",
"\n",
"Let's take our graph with HITL and add memory to it. This will be very similar to what we had previously. We'll simply update memory in the store when we get feedback from the user."
]
},
{
"cell_type": "code",
"execution_count": 93,
"id": "a3616935-a08a-45e6-b06a-a463c8f79681",
"metadata": {},
"outputs": [],
"source": [
"from email_assistant.prompts import triage_system_prompt, triage_user_prompt, agent_system_prompt_hitl_memory, default_triage_instructions, default_background, default_response_preferences, default_cal_preferences\n",
"from email_assistant.tools.default.prompt_templates import HITL_MEMORY_TOOLS_PROMPT"
]
},
{
"cell_type": "markdown",
"id": "36ce3085-8b6a-463f-91cc-95b32ca1da8f",
"metadata": {},
"source": [
"Now, this is the critical part! We currently don't capture any feedback from the user in our graph. \n",
"\n",
"### Memory Management \n",
"\n",
"What we *want* to do is fairly straightforward: we want to add the feedback to the memory `Store`. If we compile our graph with the `Store`, we can access it in any node. So that is not a problem! \n",
"\n",
"To manage memory, there are two main considerations: \n",
"\n",
"1) How do we want the memory to be structured?\n",
"2) How do we want to update the memory?\n",
"\n",
"For **1) structuring memory**, we'll just store memories as string to keep things simple. In the below function, we'll just fetch memories from the store as string and initialize with default if it doesn't exist.\n",
"\n",
"For our memory, we will have 3 main namespace, which includes\n",
"- `triage_preferences`: What emails are responded, notified, vs. ignored\n",
"- `response_preferences`: Guidance for drafting email \n",
"- `cal_preferences`: Preference for scheduling meetings"
]
},
{
"cell_type": "code",
"execution_count": 99,
"id": "f720d635-2f92-44cb-b828-868193bcb732",
"metadata": {},
"outputs": [],
"source": [
"def get_memory(store, namespace, default_content=None):\n",
" \"\"\"Get memory from the store or initialize with default if it doesn't exist.\n",
" \n",
" Args:\n",
" store: LangGraph BaseStore instance to search for existing memory\n",
" namespace: Tuple defining the memory namespace, e.g. (\"email_assistant\", \"triage_preferences\")\n",
" default_content: Default content to use if memory doesn't exist\n",
" \n",
" Returns:\n",
" str: The content of the memory profile, either from existing memory or the default\n",
" \"\"\"\n",
" # Search for existing memory with namespace and key\n",
" user_preferences = store.get(namespace, \"user_preferences\")\n",
" \n",
" # If memory exists, return its content (the value)\n",
" if user_preferences:\n",
" return user_preferences.value\n",
" \n",
" # If memory doesn't exist, add it to the store and return the default content\n",
" else:\n",
" # Namespace, key, value\n",
" store.put(namespace, \"user_preferences\", default_content)\n",
" user_preferences = default_content\n",
" \n",
" # Return the default content\n",
" return user_preferences "
]
},
{
"cell_type": "markdown",
"id": "c5efede5-4737-4755-9d2e-72b1de620175",
"metadata": {},
"source": [
"For **2) updating memory**, we can use a few tricks from the [GPT-4.1 prompting guide]((https://cookbook.openai.com/examples/gpt4-1_prompting_guide)) to help us update the memory: \n",
"\n",
"* For optimal performance, repeat the key instructions at the start and end of the prompt\n",
"* Create clear, explicit instructions \n",
"* Use XML delimiters for structure\n",
"* Provide examples "
]
},
{
"cell_type": "code",
"execution_count": 100,
"id": "a1c7e3a7-517e-46af-997a-ebf1165eddbd",
"metadata": {},
"outputs": [],
"source": [
"MEMORY_UPDATE_INSTRUCTIONS = \"\"\"\n",
"# Role and Objective\n",
"You are a memory profile manager for an email assistant agent that selectively updates user preferences based on feedback messages from human-in-the-loop interactions with the email assistant.\n",
"\n",
"# Instructions\n",
"- NEVER overwrite the entire memory profile\n",
"- ONLY make targeted additions of new information\n",
"- ONLY update specific facts that are directly contradicted by feedback messages\n",
"- PRESERVE all other existing information in the profile\n",
"- Format the profile consistently with the original style\n",
"- Generate the profile as a string\n",
"\n",
"# Reasoning Steps\n",
"1. Analyze the current memory profile structure and content\n",
"2. Review feedback messages from human-in-the-loop interactions\n",
"3. Extract relevant user preferences from these feedback messages (such as edits to emails/calendar invites, explicit feedback on assistant performance, user decisions to ignore certain emails)\n",
"4. Compare new information against existing profile\n",
"5. Identify only specific facts to add or update\n",
"6. Preserve all other existing information\n",
"7. Output the complete updated profile\n",
"\n",
"# Example\n",
"<memory_profile>\n",
"RESPOND:\n",
"- wife\n",
"- specific questions\n",
"- system admin notifications\n",
"NOTIFY: \n",
"- meeting invites\n",
"IGNORE:\n",
"- marketing emails\n",
"- company-wide announcements\n",
"- messages meant for other teams\n",
"</memory_profile>\n",
"\n",
"<user_messages>\n",
"\"The assistant shouldn't have responded to that system admin notification.\"\n",
"</user_messages>\n",
"\n",
"<updated_profile>\n",
"RESPOND:\n",
"- wife\n",
"- specific questions\n",
"NOTIFY: \n",
"- meeting invites\n",
"- system admin notifications\n",
"IGNORE:\n",
"- marketing emails\n",
"- company-wide announcements\n",
"- messages meant for other teams\n",
"</updated_profile>\n",
"\n",
"# Process current profile for {namespace}\n",
"<memory_profile>\n",
"{current_profile}\n",
"</memory_profile>\n",
"\n",
"Think step by step about what specific feedback is being provided and what specific information should be added or updated in the profile while preserving everything else.\"\"\"\n",
"\n",
"MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT = \"\"\"\n",
"Remember:\n",
"- NEVER overwrite the entire profile\n",
"- ONLY make targeted additions or changes based on explicit feedback\n",
"- PRESERVE all existing information not directly contradicted\n",
"- Output the complete updated profile as a string\n",
"\"\"\""
]
},
{
"cell_type": "code",
"execution_count": 101,
"id": "07d67da8-2351-44c6-b556-0d3efebd0a9a",
"metadata": {},
"outputs": [],
"source": [
"class UserPreferences(BaseModel):\n",
" \"\"\"User preferences.\"\"\"\n",
" preferences: str\n",
" justification: str\n",
"\n",
"\n",
"user_preference_llm = llm.with_structured_output(UserPreferences)"
]
},
{
"cell_type": "code",
"execution_count": 102,
"id": "83c2e91f-30ac-49fa-b2fc-9e6232446cd9",
"metadata": {},
"outputs": [],
"source": [
"def update_memory(store, namespace, messages):\n",
" \"\"\"Update memory profile in the store.\n",
" \n",
" Args:\n",
" store: LangGraph BaseStore instance to update memory\n",
" namespace: Tuple defining the memory namespace, e.g. (\"email_assistant\", \"triage_preferences\")\n",
" messages: List of messages to update the memory with\n",
" \"\"\"\n",
"\n",
" # Get the existing memory\n",
" user_preferences = store.get(namespace, \"user_preferences\")\n",
"\n",
" # Update the memory\n",
" result = user_preference_llm.invoke(\n",
" [\n",
" {\"role\": \"system\", \"content\": MEMORY_UPDATE_INSTRUCTIONS.format(current_profile=user_preferences.value, namespace=namespace)},\n",
" {\"role\": \"user\", \"content\": f\"Think carefully and update the memory profile based upon these user messages:\"}\n",
" ] + messages\n",
" )\n",
" \n",
" # Save the updated memory to the store\n",
" store.put(namespace, \"user_preferences\", result.preferences)"
]
},
{
"cell_type": "markdown",
"id": "80670186-c645-4d91-94ab-8943d1a3ddd4",
"metadata": {},
"source": [
"### Updating Existing Steps with Memory "
]
},
{
"cell_type": "markdown",
"id": "a4fbeebc-11c0-4233-bd62-8be9335f3fad",
"metadata": {},
"source": [
"Now that we have set up two functions that help us fetch and update memory. We can go and update the logic to store user preferences among our existing nodes. \n",
"\n",
"![overview-img](img/HITL_flow_memory.png)\n",
"\n",
"Particularly, there are four steps that need updating: \n",
"1. **triage_router**: Fetches any relevant memory from `triage_preferences`; pass to LLM to make triaging decision. \n",
"2. **triage_interrupt_handler**: Update `triage_preferences` in store when user provides feedback (e.g., respond, or ignore).\n",
"3. **llm_call**: Fetch `cal_preferences` and `respond_preferences` to LLM when executing tool call\n",
"4. **tool_handler**: Update preferences with human feedback on tool calls - `cal_preferences` and `respond_preferences` if user Edits or Responds to the tool output; `triage_preferences` if user Ignores the tool response"
]
},
{
"cell_type": "markdown",
"id": "8ca9a1f5-dda5-483b-b0ed-8f383532bac0",
"metadata": {},
"source": [
"#### Node 1. `triage_router`\n",
"`triage_router` currently categorizes incoming emails into 3 types of categories: `respond`, `ignore`, or `notify`. \n",
"In this step, we will want to fetch relevant memory on triage preferences from the `triage_preferences` namespace and pass to the LLM"
]
},
{
"cell_type": "code",
"execution_count": 104,
"id": "92aea53d-5083-4948-be39-92d041671357",
"metadata": {},
"outputs": [],
"source": [
"from langgraph.store.base import BaseStore\n",
"\n",
"def triage_router(state: State, store: BaseStore) -> Command[Literal[\"triage_interrupt_handler\", \"response_agent\", \"__end__\"]]:\n",
" \"\"\"Analyze email content to decide if we should respond, notify, or ignore.\n",
"\n",
" The triage step prevents the assistant from wasting time on:\n",
" - Marketing emails and spam\n",
" - Company-wide announcements\n",
" - Messages meant for other teams\n",
" \"\"\"\n",
" # Parse the email input\n",
" author, to, subject, email_thread = parse_email(state[\"email_input\"])\n",
" user_prompt = triage_user_prompt.format(\n",
" author=author, to=to, subject=subject, email_thread=email_thread\n",
" )\n",
"\n",
" # Create email markdown for Agent Inbox in case of notification \n",
" email_markdown = format_email_markdown(subject, author, to, email_thread)\n",
"\n",
" # NEW STEP! - Search for existing triage_preferences memory\n",
" triage_instructions = get_memory(store, (\"email_assistant\", \"triage_preferences\"), default_triage_instructions)\n",
"\n",
" # Format system prompt with background and triage instructions from memory \n",
" system_prompt_with_memory = triage_system_prompt.format(\n",
" background=default_background,\n",
" triage_instructions=triage_instructions,\n",
" )\n",
"\n",
" # Run the router LLM with any preferences from memory\n",
" result = llm_router.invoke(\n",
" [\n",
" {\"role\": \"system\", \"content\": system_prompt_with_memory},\n",
" {\"role\": \"user\", \"content\": user_prompt},\n",
" ]\n",
" )\n",
"\n",
" # Decision\n",
" classification = result.classification\n",
"\n",
" # Process the classification decision\n",
" if classification == \"respond\":\n",
" print(\"📧 Classification: RESPOND - This email requires a response\")\n",
" # Next node\n",
" goto = \"response_agent\"\n",
" # Update the state\n",
" update = {\n",
" \"classification_decision\": result.classification,\n",
" \"messages\": [{\"role\": \"user\",\n",
" \"content\": f\"Respond to the email: {email_markdown}\"\n",
" }],\n",
" }\n",
" \n",
" elif classification == \"ignore\":\n",
" print(\"🚫 Classification: IGNORE - This email can be safely ignored\")\n",
"\n",
" # Next node\n",
" goto = END\n",
" # Update the state\n",
" update = {\n",
" \"classification_decision\": classification,\n",
" }\n",
"\n",
" elif classification == \"notify\":\n",
" print(\"🔔 Classification: NOTIFY - This email contains important information\") \n",
"\n",
" # Next node\n",
" goto = \"triage_interrupt_handler\"\n",
" # Update the state\n",
" update = {\n",
" \"classification_decision\": classification,\n",
" }\n",
"\n",
" else:\n",
" raise ValueError(f\"Invalid classification: {classification}\")\n",
" \n",
" return Command(goto=goto, update=update)\n"
]
},
{
"cell_type": "markdown",
"id": "b08ff04a-d473-40f0-94c4-0bca21820213",
"metadata": {},
"source": [
"#### Node 2. `triage_interrupt_handler`\n",
"\n",
"`triage_interrupt_handler` currently handles the interrupt for \"notify\" type emails.\n",
"\n",
"With memory, we'd want to update triage preferences from the `triage_preferences` namespace when user provides feedback (e.g., respond or ignore) on \"notify\" type emails. "
]
},
{
"cell_type": "code",
"execution_count": 105,
"id": "50fa6309-2804-4efc-b606-4bd524fe2aa8",
"metadata": {},
"outputs": [],
"source": [
"def triage_interrupt_handler(state: State, store: BaseStore) -> Command[Literal[\"response_agent\", \"__end__\"]]:\n",
" \"\"\"Handles interrupts from the triage step\"\"\"\n",
" \n",
" # Parse the email input\n",
" author, to, subject, email_thread = parse_email(state[\"email_input\"])\n",
"\n",
" # Create email markdown for Agent Inbox in case of notification \n",
" email_markdown = format_email_markdown(subject, author, to, email_thread)\n",
"\n",
" # Create messages\n",
" messages = [{\"role\": \"user\",\n",
" \"content\": f\"Email to notify user about: {email_markdown}\"\n",
" }]\n",
"\n",
" # Create interrupt for Agent Inbox\n",
" request = {\n",
" \"action_request\": {\n",
" \"action\": f\"Email Assistant: {state['classification_decision']}\",\n",
" \"args\": {}\n",
" },\n",
" \"config\": {\n",
" \"allow_ignore\": True, \n",
" \"allow_respond\": True,\n",
" \"allow_edit\": False, \n",
" \"allow_accept\": False, \n",
" },\n",
" # Email to show in Agent Inbox\n",
" \"description\": email_markdown,\n",
" }\n",
"\n",
" # Send to Agent Inbox and wait for response\n",
" response = interrupt([request])[0]\n",
"\n",
" # If user provides feedback, go to response agent and use feedback to respond to email \n",
" if response[\"type\"] == \"response\":\n",
" # Add feedback to messages \n",
" user_input = response[\"args\"]\n",
" messages.append({\"role\": \"user\",\n",
" \"content\": f\"User wants to reply to the email. Use this feedback to respond: {user_input}\"\n",
" })\n",
" # NEW STEP! Update triage_preferences with feedback\n",
" update_memory(store, (\"email_assistant\", \"triage_preferences\"), [{\n",
" \"role\": \"user\",\n",
" \"content\": f\"The user decided to respond to the email, so update the triage preferences to capture this.\"\n",
" }] + messages)\n",
"\n",
" goto = \"response_agent\"\n",
"\n",
" # If user ignores email, go to END\n",
" elif response[\"type\"] == \"ignore\":\n",
" # Make note of the user's decision to ignore the email\n",
" messages.append({\"role\": \"user\",\n",
" \"content\": f\"The user decided to ignore the email even though it was classified as notify. Update triage preferences to capture this.\"\n",
" })\n",
" # NEW STEP! Update triage_preferences with feedback\n",
" update_memory(store, (\"email_assistant\", \"triage_preferences\"), messages)\n",
" goto = END\n",
"\n",
" # Catch all other responses\n",
" else:\n",
" raise ValueError(f\"Invalid response: {response}\")\n",
"\n",
" # Update the state \n",
" update = {\n",
" \"messages\": messages,\n",
" }\n",
"\n",
" return Command(goto=goto, update=update)"
]
},
{
"cell_type": "markdown",
"id": "65f964cc-737c-4b96-8cf6-4c64fa913fd4",
"metadata": {},
"source": [
"#### Node 3. `llm_call` \n",
"\n",
"`llm_call` node decides on which tool(s) to invoke or how to respond to an email after tool invocation. \n",
"\n",
"Now that we have memory managers set up, we can incorporate memory and use the stored preferences when generating responses. "
]
},
{
"cell_type": "code",
"execution_count": 106,
"id": "d0ea81ce-b1c7-45af-8f54-1a219adf4812",
"metadata": {},
"outputs": [],
"source": [
"def llm_call(state: State, store: BaseStore):\n",
" \"\"\"LLM decides whether to call a tool or not\"\"\"\n",
"\n",
" # Search for existing cal_preferences memory\n",
" cal_preferences = get_memory(store, (\"email_assistant\", \"cal_preferences\"), default_cal_preferences)\n",
" \n",
" # Search for existing response_preferences memory\n",
" response_preferences = get_memory(store, (\"email_assistant\", \"response_preferences\"), default_response_preferences)\n",
"\n",
" return {\n",
" \"messages\": [\n",
" llm_with_tools.invoke(\n",
" [\n",
" {\"role\": \"system\", \"content\": agent_system_prompt_hitl_memory.format(tools_prompt=HITL_MEMORY_TOOLS_PROMPT,\n",
" background=default_background,\n",
" response_preferences=response_preferences, \n",
" cal_preferences=cal_preferences)}\n",
" ]\n",
" + state[\"messages\"]\n",
" )\n",
" ]\n",
" }"
]
},
{
"cell_type": "markdown",
"id": "085a3cee-1c75-4af9-950c-55ef1178d869",
"metadata": {},
"source": [
"#### Node 4. `tool_handler`\n",
"\n",
"`tool_handler` handles the interrupts and executions of tools. Similarly, we will integrate memory to the node. \n",
"\n",
"Specifically, if a user: \n",
"- **Edits** or **Responds** to the tool output, then preferences around calendar or responding will be saved \n",
"- **Ignores** the tool response, then triaging preferences are saved "
]
},
{
"cell_type": "code",
"execution_count": 107,
"id": "d57b8eca-7350-4e7a-b944-a2e7be83c3d4",
"metadata": {},
"outputs": [],
"source": [
"def tool_handler(state: State, store: BaseStore) -> Command[Literal[\"llm_call\", \"__end__\"]]:\n",
" \"\"\"Creates an interrupt for human review of tool calls\"\"\"\n",
" \n",
" # Store messages\n",
" result = []\n",
"\n",
" # Go to the LLM call node next\n",
" goto = \"llm_call\"\n",
"\n",
" # Iterate over the tool calls in the last message\n",
" for tool_call in state[\"messages\"][-1].tool_calls:\n",
" \n",
" # Allowed tools for HITL\n",
" hitl_tools = [\"write_email\", \"schedule_meeting\", \"Question\"]\n",
" \n",
" # If tool is not in our HITL list, execute it directly without interruption\n",
" if tool_call[\"name\"] not in hitl_tools:\n",
"\n",
" # Execute search_memory and other tools without interruption\n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" observation = tool.invoke(tool_call[\"args\"])\n",
" result.append({\"role\": \"tool\", \"content\": observation, \"tool_call_id\": tool_call[\"id\"]})\n",
" continue\n",
" \n",
" # Get original email from email_input in state\n",
" email_input = state[\"email_input\"]\n",
" author, to, subject, email_thread = parse_email(email_input)\n",
" original_email_markdown = format_email_markdown(subject, author, to, email_thread)\n",
" \n",
" # Format tool call for display and prepend the original email\n",
" tool_display = format_for_display(tool_call)\n",
" description = original_email_markdown + tool_display\n",
"\n",
" # Configure what actions are allowed in Agent Inbox\n",
" if tool_call[\"name\"] == \"write_email\":\n",
" config = {\n",
" \"allow_ignore\": True,\n",
" \"allow_respond\": True,\n",
" \"allow_edit\": True,\n",
" \"allow_accept\": True,\n",
" }\n",
" elif tool_call[\"name\"] == \"schedule_meeting\":\n",
" config = {\n",
" \"allow_ignore\": True,\n",
" \"allow_respond\": True,\n",
" \"allow_edit\": True,\n",
" \"allow_accept\": True,\n",
" }\n",
" elif tool_call[\"name\"] == \"Question\":\n",
" config = {\n",
" \"allow_ignore\": True,\n",
" \"allow_respond\": True,\n",
" \"allow_edit\": False,\n",
" \"allow_accept\": False,\n",
" }\n",
" else:\n",
" raise ValueError(f\"Invalid tool call: {tool_call['name']}\")\n",
"\n",
" # Create the interrupt request\n",
" request = {\n",
" \"action_request\": {\n",
" \"action\": tool_call[\"name\"],\n",
" \"args\": tool_call[\"args\"]\n",
" },\n",
" \"config\": config,\n",
" \"description\": description,\n",
" }\n",
"\n",
" # Send to Agent Inbox and wait for response\n",
" response = interrupt([request])[0]\n",
"\n",
" # For \"ACCEPT\", we don't update preferences in memory \n",
" if response[\"type\"] == \"accept\":\n",
"\n",
" # Execute the tool with original args\n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" observation = tool.invoke(tool_call[\"args\"])\n",
" result.append({\"role\": \"tool\", \"content\": observation, \"tool_call_id\": tool_call[\"id\"]})\n",
"\n",
"\n",
" # For \"EDIT\", we edit preferences for the specific tool \n",
" elif response[\"type\"] == \"edit\":\n",
"\n",
" # Tool selection \n",
" tool = tools_by_name[tool_call[\"name\"]]\n",
" initial_tool_call = tool_call[\"args\"]\n",
" \n",
" # Get edited args from Agent Inbox\n",
" edited_args = response[\"args\"][\"args\"]\n",
"\n",
" # Update the AI message's tool call with edited content (reference to the message in the state)\n",
" ai_message = state[\"messages\"][-1] # Get the most recent message from the state\n",
" current_id = tool_call[\"id\"] # Store the ID of the tool call being edited\n",
" \n",
" # Create a new list of tool calls by filtering out the one being edited and adding the updated version\n",
" # This avoids modifying the original list directly (immutable approach)\n",
" updated_tool_calls = [tc for tc in ai_message.tool_calls if tc[\"id\"] != current_id] + [\n",
" {\"type\": \"tool_call\", \"name\": tool_call[\"name\"], \"args\": edited_args, \"id\": current_id}\n",
" ]\n",
"\n",
" # Create a new copy of the message with updated tool calls rather than modifying the original\n",
" # This ensures state immutability and prevents side effects in other parts of the code\n",
" result.append(ai_message.model_copy(update={\"tool_calls\": updated_tool_calls}))\n",
"\n",
" # Save feedback in memory and update the write_email tool call with the edited content from Agent Inbox\n",
" if tool_call[\"name\"] == \"write_email\":\n",
" \n",
" # Execute the tool with edited args\n",
" observation = tool.invoke(edited_args)\n",
" \n",
" # Add only the tool response message\n",
" result.append({\"role\": \"tool\", \"content\": observation, \"tool_call_id\": current_id})\n",
"\n",
" # NEW STEP! Update the memory for response_preferences, passing in initial version and edited version \n",
" update_memory(store, (\"email_assistant\", \"response_preferences\"), [{\n",
" \"role\": \"user\",\n",
" \"content\": f\"User edited the email response. Here is the initial email generated by the assistant: {initial_tool_call}. Here is the edited email: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.\"\n",
" }])\n",
" \n",
" # Save feedback in memory and update the schedule_meeting tool call with the edited content from Agent Inbox\n",
" elif tool_call[\"name\"] == \"schedule_meeting\":\n",
" \n",
" # Execute the tool with edited args\n",
" observation = tool.invoke(edited_args)\n",
" \n",
" # Add only the tool response message\n",
" result.append({\"role\": \"tool\", \"content\": observation, \"tool_call_id\": current_id})\n",
"\n",
" # NEW STEP! Update the memory for response_preferences, passing in initial version and edited version \n",
" update_memory(store, (\"email_assistant\", \"cal_preferences\"), [{\n",
" \"role\": \"user\",\n",
" \"content\": f\"User edited the calendar invitation. Here is the initial calendar invitation generated by the assistant: {initial_tool_call}. Here is the edited calendar invitation: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.\"\n",
" }])\n",
" \n",
" # Catch all other tool calls\n",
" else:\n",
" raise ValueError(f\"Invalid tool call: {tool_call['name']}\")\n",
"\n",
"\n",
" # For \"IGNORE\", we save user preferences into triage preferences \n",
" elif response[\"type\"] == \"ignore\":\n",
"\n",
" if tool_call[\"name\"] == \"write_email\":\n",
" # Don't execute the tool, and tell the agent how to proceed\n",
" result.append({\"role\": \"tool\", \"content\": \"User ignored this email draft. Ignore this email and end the workflow.\", \"tool_call_id\": tool_call[\"id\"]})\n",
" # Go to END\n",
" goto = END\n",
" # NEW STEP! Update the memory for triage_preferences \n",
" update_memory(store, (\"email_assistant\", \"triage_preferences\"), state[\"messages\"] + result + [{\n",
" \"role\": \"user\",\n",
" \"content\": f\"The user ignored the email draft. That means they did not want to respond to the email. Update the triage preferences to ensure emails of this type are not classified as respond. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.\"\n",
" }])\n",
"\n",
" elif tool_call[\"name\"] == \"schedule_meeting\":\n",
" # Don't execute the tool, and tell the agent how to proceed\n",
" result.append({\"role\": \"tool\", \"content\": \"User ignored this calendar meeting draft. Ignore this email and end the workflow.\", \"tool_call_id\": tool_call[\"id\"]})\n",
" # Go to END\n",
" goto = END\n",
" # NEW STEP! Update the memory for triage_preferences \n",
" update_memory(store, (\"email_assistant\", \"triage_preferences\"), state[\"messages\"] + result + [{\n",
" \"role\": \"user\",\n",
" \"content\": f\"The user ignored the calendar meeting draft. That means they did not want to schedule a meeting for this email. Update the triage preferences to ensure emails of this type are not classified as respond. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.\"\n",
" }])\n",
"\n",
" elif tool_call[\"name\"] == \"Question\":\n",
" # Don't execute the tool, and tell the agent how to proceed\n",
" result.append({\"role\": \"tool\", \"content\": \"User ignored this question. Ignore this email and end the workflow.\", \"tool_call_id\": tool_call[\"id\"]})\n",
" # Go to END\n",
" goto = END\n",
" # NEW STEP! Update the memory for triage_preferences \n",
" update_memory(store, (\"email_assistant\", \"triage_preferences\"), state[\"messages\"] + result + [{\n",
" \"role\": \"user\",\n",
" \"content\": f\"The user ignored the Question. That means they did not want to answer the question or deal with this email. Update the triage preferences to ensure emails of this type are not classified as respond. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.\"\n",
" }])\n",
"\n",
" else:\n",
" raise ValueError(f\"Invalid tool call: {tool_call['name']}\")\n",
"\n",
" # For \"RESPOND\", we save user feedback to our agent into corresponding calendar or response preferences \n",
"\n",
" elif response[\"type\"] == \"response\":\n",
" # User provided feedback\n",
" user_feedback = response[\"args\"]\n",
" if tool_call[\"name\"] == \"write_email\":\n",
" # Don't execute the tool, and add a message with the user feedback to incorporate into the email\n",
" result.append({\"role\": \"tool\", \"content\": f\"User gave feedback, which can we incorporate into the email. Feedback: {user_feedback}\", \"tool_call_id\": tool_call[\"id\"]})\n",
" # NEW STEP! Update the memory for response_preferences \n",
" update_memory(store, (\"email_assistant\", \"response_preferences\"), state[\"messages\"] + result + [{\n",
" \"role\": \"user\",\n",
" \"content\": f\"User gave feedback, which we can use to update the response preferences. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.\"\n",
" }])\n",
"\n",
" elif tool_call[\"name\"] == \"schedule_meeting\":\n",
" # Don't execute the tool, and add a message with the user feedback to incorporate into the email\n",
" result.append({\"role\": \"tool\", \"content\": f\"User gave feedback, which can we incorporate into the meeting request. Feedback: {user_feedback}\", \"tool_call_id\": tool_call[\"id\"]})\n",
" # NEW STEP! Update the memory for cal_preferences \n",
" update_memory(store, (\"email_assistant\", \"cal_preferences\"), state[\"messages\"] + result + [{\n",
" \"role\": \"user\",\n",
" \"content\": f\"User gave feedback, which we can use to update the calendar preferences. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.\"\n",
" }])\n",
"\n",
" elif tool_call[\"name\"] == \"Question\":\n",
" # Don't execute the tool, and add a message with the user feedback to incorporate into the email\n",
" result.append({\"role\": \"tool\", \"content\": f\"User answered the question, which can we can use for any follow up actions. Feedback: {user_feedback}\", \"tool_call_id\": tool_call[\"id\"]})\n",
"\n",
" else:\n",
" raise ValueError(f\"Invalid tool call: {tool_call['name']}\")\n",
"\n",
" # Update the state \n",
" update = {\n",
" \"messages\": result,\n",
" }\n",
"\n",
" return Command(goto=goto, update=update)"
]
},
{
"cell_type": "markdown",
"id": "91c191bc-ddf8-496e-8013-74d95a7e649f",
"metadata": {},
"source": [
"The rest is the same as before!"
]
},
{
"cell_type": "code",
"execution_count": 108,
"id": "b7dd4681-5104-4513-998a-9aaff940106e",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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",
"text/plain": [
"<IPython.core.display.Image object>"
]
},
"execution_count": 108,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from email_assistant.utils import show_graph\n",
"\n",
"# Conditional edge function\n",
"def should_continue(state: State, store: BaseStore) -> Literal[\"tool_handler\", END]:\n",
" \"\"\"Route to tool handler, or end if Done tool called\"\"\"\n",
" messages = state[\"messages\"]\n",
" last_message = messages[-1]\n",
" if last_message.tool_calls:\n",
" for tool_call in last_message.tool_calls: \n",
" if tool_call[\"name\"] == \"Done\":\n",
" return END\n",
" else:\n",
" return \"tool_handler\"\n",
"\n",
"# Build workflow\n",
"agent_builder = StateGraph(State)\n",
"\n",
"# Add nodes - with store parameter\n",
"agent_builder.add_node(\"llm_call\", llm_call)\n",
"agent_builder.add_node(\"tool_handler\", tool_handler)\n",
"\n",
"# Add edges\n",
"agent_builder.add_edge(START, \"llm_call\")\n",
"agent_builder.add_conditional_edges(\n",
" \"llm_call\",\n",
" should_continue,\n",
" {\n",
" \"tool_handler\": \"tool_handler\",\n",
" END: END,\n",
" },\n",
")\n",
"\n",
"# Compile the agent\n",
"response_agent = agent_builder.compile()\n",
"\n",
"# Build overall workflow with store and checkpointer\n",
"overall_workflow = (\n",
" StateGraph(State, input=StateInput)\n",
" .add_node(triage_router)\n",
" .add_node(triage_interrupt_handler)\n",
" .add_node(\"response_agent\", response_agent)\n",
" .add_edge(START, \"triage_router\")\n",
")\n",
"\n",
"email_assistant = overall_workflow.compile()\n",
"show_graph(email_assistant, xray=True)"
]
},
{
"cell_type": "markdown",
"id": "89aeadbc-5774-4e43-9f2a-0dd01a2c8034",
"metadata": {},
"source": [
"### Testing the agent with memory\n",
"\n",
"Now that we've implemented memory into our email assistant, let's test how the system learns from user feedback and adapts over time. This testing section explores how different types of user interactions create distinct memory updates that improve the assistant's future performance.\n",
"\n",
"The key questions we're answering through these tests:\n",
"1. How does the system capture and store user preferences?\n",
"2. How do these stored preferences affect future decisions?\n",
"3. What patterns of interaction lead to which types of memory updates?\n",
"\n",
"First, let's build a helper function to display memory content so we can track how it evolves throughout our tests:"
]
},
{
"cell_type": "code",
"execution_count": 109,
"id": "5a4dbc47-d354-4841-a17b-a5600652d158",
"metadata": {},
"outputs": [],
"source": [
"# Helper function to display memory content\n",
"def display_memory_content(store, namespace=None):\n",
" # Display current memory content for all namespaces\n",
" print(\"\\n======= CURRENT MEMORY CONTENT =======\")\n",
" if namespace:\n",
" memory = store.get(namespace, \"user_preferences\")\n",
" if memory:\n",
" print(f\"\\n--- {namespace[1]} ---\")\n",
" print({\"preferences\": memory.value})\n",
" else:\n",
" print(f\"\\n--- {namespace[1]} ---\")\n",
" print(\"No memory found\")\n",
" else:\n",
" for namespace in [\n",
" (\"email_assistant\", \"triage_preferences\"),\n",
" (\"email_assistant\", \"response_preferences\"),\n",
" (\"email_assistant\", \"cal_preferences\"),\n",
" ]:\n",
" memory = store.get(namespace, \"user_preferences\")\n",
" if memory:\n",
" print(f\"\\n--- {namespace[1]} ---\")\n",
" print({\"preferences\": memory.value})\n",
" else:\n",
" print(f\"\\n--- {namespace[1]} ---\")\n",
" print(\"No memory found\")\n",
" print(\"=======================================\\n\")"
]
},
{
"cell_type": "markdown",
"id": "fa6f107e-0b27-467f-abff-b2b75ebf0934",
"metadata": {},
"source": [
"#### Test 1. Accept `write_email` and `schedule_meeting`\n",
"\n",
"Our first test examines what happens when a user accepts the agent's actions without modification. This baseline case helps us understand how the system behaves when no feedback is provided:\n",
"\n",
"1. We'll use the same tax planning email from our previous tests\n",
"2. The system will classify it as \"RESPOND\" and propose scheduling a meeting\n",
"3. We'll accept the meeting schedule without changes\n",
"4. The agent will generate an email confirming the meeting\n",
"5. We'll accept the email without changes\n",
"\n",
"This test demonstrates the default behavior of our memory-enabled system. When a user simply accepts proposed actions, we expect minimal or no memory updates since there's no explicit feedback to learn from. However, the system will still leverage existing memory (if any) when generating its responses."
]
},
{
"cell_type": "code",
"execution_count": 111,
"id": "57c4b323-6aae-479f-9d93-57a11bd2b888",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running the graph until the first interrupt...\n",
"📧 Classification: RESPOND - This email requires a response\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'schedule_meeting', 'args': {'attendees': ['pm@client.com', 'catherine@company.com'], 'subject': 'Tax Planning Strategies Discussion', 'duration_minutes': 45, 'preferred_day': '2025-05-06T00:00:00', 'start_time': 14}}\n",
"\n",
"======= CURRENT MEMORY CONTENT =======\n",
"\n",
"--- triage_preferences ---\n",
"{'preferences': \"\\nEmails that are not worth responding to:\\n- Marketing newsletters and promotional emails\\n- Spam or suspicious emails\\n- CC'd on FYI threads with no direct questions\\n\\nThere are also other things that should be known about, but don't require an email response. For these, you should notify (using the `notify` response). Examples of this include:\\n- Team member out sick or on vacation\\n- Build system notifications or deployments\\n- Project status updates without action items\\n- Important company announcements\\n- FYI emails that contain relevant information for current projects\\n- HR Department deadline reminders\\n- Subscription status / renewal reminders\\n- GitHub notifications\\n\\nEmails that are worth responding to:\\n- Direct questions from team members requiring expertise\\n- Meeting requests requiring confirmation\\n- Critical bug reports related to team's projects\\n- Requests from management requiring acknowledgment\\n- Client inquiries about project status or features\\n- Technical questions about documentation, code, or APIs (especially questions about missing endpoints or features)\\n- Personal reminders related to family (wife / daughter)\\n- Personal reminder related to self-care (doctor appointments, etc)\\n\"}\n",
"=======================================\n",
"\n",
"\n",
"--- response_preferences ---\n",
"{'preferences': \"\\nUse professional and concise language. If the e-mail mentions a deadline, make sure to explicitly acknowledge and reference the deadline in your response.\\n\\nWhen responding to technical questions that require investigation:\\n- Clearly state whether you will investigate or who you will ask\\n- Provide an estimated timeline for when you'll have more information or complete the task\\n\\nWhen responding to event or conference invitations:\\n- Always acknowledge any mentioned deadlines (particularly registration deadlines)\\n- If workshops or specific topics are mentioned, ask for more specific details about them\\n- If discounts (group or early bird) are mentioned, explicitly request information about them\\n- Don't commit \\n\\nWhen responding to collaboration or project-related requests:\\n- Acknowledge any existing work or materials mentioned (drafts, slides, documents, etc.)\\n- Explicitly mention reviewing these materials before or during the meeting\\n- When scheduling meetings, clearly state the specific day, date, and time proposed\\n\\nWhen responding to meeting scheduling requests:\\n- If times are proposed, verify calendar availability for all time slots mentioned in the original email and then commit to one of the proposed times based on your availability by scheduling the meeting. Or, say you can't make it at the time proposed.\\n- If no times are proposed, then check your calendar for availability and propose multiple time options when available instead of selecting just one.\\n- Mention the meeting duration in your response to confirm you've noted it correctly.\\n- Reference the meeting's purpose in your response.\\n\"}\n",
"=======================================\n",
"\n",
"\n",
"--- cal_preferences ---\n",
"{'preferences': '\\n30 minute meetings are preferred, but 15 minute meetings are also acceptable.\\n'}\n",
"=======================================\n",
"\n"
]
}
],
"source": [
"# Respond - Meeting Request Email \n",
"email_input_respond = {\n",
" \"to\": \"Catherine Qiao <catherine@company.com>\",\n",
" \"author\": \"Project Manager <pm@client.com>\",\n",
" \"subject\": \"Tax season let's schedule call\",\n",
" \"email_thread\": \"Catherine,\\n\\nIt's tax season again. Before the filing deadline, I would highly recommend scheduling a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\\n\\nAre you available sometime soon? Tuesday or Thursday afternoon on May 6, 2025 or May 8, 2025 would work best for me, for about 45 minutes.\\n\\nRegards,\\nProject Manager\"\n",
"}\n",
"\n",
"# Compile the graph\n",
"store = InMemoryStore()\n",
"graph = overall_workflow.compile(checkpointer=checkpointer, store=store)\n",
"thread_id_1 = uuid.uuid4()\n",
"thread_config_1 = {\"configurable\": {\"thread_id\": thread_id_1}}\n",
"\n",
"# Run the graph until the first interrupt \n",
"# Email will be classified as \"respond\" \n",
"# Agent will create a schedule_meeting and write_email tool call\n",
"print(\"Running the graph until the first interrupt...\")\n",
"for chunk in graph.stream({\"email_input\": email_input_respond}, config=thread_config_1):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")\n",
"\n",
"# Check memory after first interrupt\n",
"display_memory_content(store)"
]
},
{
"cell_type": "markdown",
"id": "22565bca-30ba-45bb-9e5a-c252793efa89",
"metadata": {},
"source": [
"**Accept the `schedule_meeting` tool call**\n",
"\n",
"As we examine the initial `schedule_meeting` proposal, note how the system uses existing memory to inform its decisions:\n",
"\n",
"1. The default calendar preferences show a preference for 30-minute meetings, though the email requests 45 minutes\n",
"2. The agent still proposes a 45-minute meeting, respecting the sender's specific request\n",
"3. We accept this proposal without modification to see if simple acceptance triggers any memory updates\n",
"\n",
"After running this step, we'll check the memory contents to confirm whether acceptance alone leads to memory updates. Simple acceptance represents the baseline user experience - the system works as intended without requiring adjustments."
]
},
{
"cell_type": "code",
"execution_count": 112,
"id": "d26435bd-5c4f-4819-8594-9b6a307430c3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user accepting the schedule_meeting tool call...\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'write_email', 'args': {'to': 'pm@client.com', 'subject': \"Re: Tax season let's schedule call\", 'content': 'Hello,\\n\\nThank you for reaching out about tax planning strategies ahead of the filing deadline. I appreciate your suggestions and am glad to schedule a call.\\n\\nIve scheduled our meeting for Tuesday, May 6, 2025, at 2:00 PM for 45 minutes, as requested. If you need to reschedule or prefer a different time on Thursday, please let me know.\\n\\nLooking forward to our discussion.\\n\\nBest regards,\\nCatherine'}}\n"
]
}
],
"source": [
"print(f\"\\nSimulating user accepting the {Interrupt_Object.value[0]['action_request']['action']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"accept\"}]), config=thread_config_1):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")"
]
},
{
"cell_type": "markdown",
"id": "56e39630-280a-48af-bfba-be32f76027f4",
"metadata": {},
"source": [
"**Accept the `write_email` tool call**\n",
"\n",
"Now we'll accept the email draft that confirms the meeting scheduling:\n",
"\n",
"1. The email draft is generated with knowledge of our calendar preferences\n",
"2. It includes details about the meeting time, duration, and purpose\n",
"3. We'll accept it without changes to complete the baseline test case\n",
"\n",
"After accepting, we'll check all memory stores to see if any updates occurred. As expected, simply accepting the agent's proposals doesn't provide strong learning signals - there's no clear feedback about what the user likes or dislikes about the agent's approach.\n",
"\n",
"The trace link shows the complete workflow execution, where we can see that the memory is used in the LLM call for response generation, but no memory updates occur, which is the expected behavior for simple acceptances."
]
},
{
"cell_type": "code",
"execution_count": 113,
"id": "49d57500-f596-482f-a51a-ef90ee699603",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user accepting the write_email tool call...\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_RFxR3Iq752oIMbviMud6IMQk)\n",
" Call ID: call_RFxR3Iq752oIMbviMud6IMQk\n",
" Args:\n",
" done: True\n",
"\n",
"======= CURRENT MEMORY CONTENT =======\n",
"\n",
"--- triage_preferences ---\n",
"{'preferences': \"\\nEmails that are not worth responding to:\\n- Marketing newsletters and promotional emails\\n- Spam or suspicious emails\\n- CC'd on FYI threads with no direct questions\\n\\nThere are also other things that should be known about, but don't require an email response. For these, you should notify (using the `notify` response). Examples of this include:\\n- Team member out sick or on vacation\\n- Build system notifications or deployments\\n- Project status updates without action items\\n- Important company announcements\\n- FYI emails that contain relevant information for current projects\\n- HR Department deadline reminders\\n- Subscription status / renewal reminders\\n- GitHub notifications\\n\\nEmails that are worth responding to:\\n- Direct questions from team members requiring expertise\\n- Meeting requests requiring confirmation\\n- Critical bug reports related to team's projects\\n- Requests from management requiring acknowledgment\\n- Client inquiries about project status or features\\n- Technical questions about documentation, code, or APIs (especially questions about missing endpoints or features)\\n- Personal reminders related to family (wife / daughter)\\n- Personal reminder related to self-care (doctor appointments, etc)\\n\"}\n",
"=======================================\n",
"\n",
"\n",
"--- response_preferences ---\n",
"{'preferences': \"\\nUse professional and concise language. If the e-mail mentions a deadline, make sure to explicitly acknowledge and reference the deadline in your response.\\n\\nWhen responding to technical questions that require investigation:\\n- Clearly state whether you will investigate or who you will ask\\n- Provide an estimated timeline for when you'll have more information or complete the task\\n\\nWhen responding to event or conference invitations:\\n- Always acknowledge any mentioned deadlines (particularly registration deadlines)\\n- If workshops or specific topics are mentioned, ask for more specific details about them\\n- If discounts (group or early bird) are mentioned, explicitly request information about them\\n- Don't commit \\n\\nWhen responding to collaboration or project-related requests:\\n- Acknowledge any existing work or materials mentioned (drafts, slides, documents, etc.)\\n- Explicitly mention reviewing these materials before or during the meeting\\n- When scheduling meetings, clearly state the specific day, date, and time proposed\\n\\nWhen responding to meeting scheduling requests:\\n- If times are proposed, verify calendar availability for all time slots mentioned in the original email and then commit to one of the proposed times based on your availability by scheduling the meeting. Or, say you can't make it at the time proposed.\\n- If no times are proposed, then check your calendar for availability and propose multiple time options when available instead of selecting just one.\\n- Mention the meeting duration in your response to confirm you've noted it correctly.\\n- Reference the meeting's purpose in your response.\\n\"}\n",
"=======================================\n",
"\n",
"\n",
"--- cal_preferences ---\n",
"{'preferences': '\\n30 minute meetings are preferred, but 15 minute meetings are also acceptable.\\n'}\n",
"=======================================\n",
"\n"
]
}
],
"source": [
"print(f\"\\nSimulating user accepting the {Interrupt_Object.value[0]['action_request']['action']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"accept\"}]), config=thread_config_1):\n",
" # Inspect response_agent most recent message\n",
" if 'response_agent' in chunk:\n",
" chunk['response_agent']['messages'][-1].pretty_print()\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")\n",
"\n",
"# Check memory after accepting the write_email tool call\n",
"display_memory_content(store)"
]
},
{
"cell_type": "markdown",
"id": "e5e35697-3bbb-477d-9c4b-6399d785a2c4",
"metadata": {},
"source": [
"We can look at the full messages, and the trace: \n",
"\n",
"https://smith.langchain.com/public/fa6ff783-5aa7-4dcc-a115-16fe74505048/r\n",
"\n",
"You'll notice that memory is used in the LLM call to respond. \n",
"\n",
"But the memory store is *not* updated, because we haven't added any feedback via HITL."
]
},
{
"cell_type": "code",
"execution_count": 114,
"id": "2a636d76-4e6c-4e6a-ba2c-847cb42da8d5",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Respond to the email: \n",
"\n",
"**Subject**: Tax season let's schedule call\n",
"**From**: Project Manager <pm@client.com>\n",
"**To**: Catherine Qiao <catherine@company.com>\n",
"\n",
"Catherine,\n",
"\n",
"It's tax season again. Before the filing deadline, I would highly recommend scheduling a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\n",
"\n",
"Are you available sometime soon? Tuesday or Thursday afternoon on May 6, 2025 or May 8, 2025 would work best for me, for about 45 minutes.\n",
"\n",
"Regards,\n",
"Project Manager\n",
"\n",
"---\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" check_calendar_availability (call_S7C2GdjN2XRKDDsos5wvXGP2)\n",
" Call ID: call_S7C2GdjN2XRKDDsos5wvXGP2\n",
" Args:\n",
" day: 2025-05-06\n",
" check_calendar_availability (call_RVBJZFKqLxcOMYj3Bgxh08Cp)\n",
" Call ID: call_RVBJZFKqLxcOMYj3Bgxh08Cp\n",
" Args:\n",
" day: 2025-05-08\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-05-06: 9:00 AM, 2:00 PM, 4:00 PM\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-05-08: 9:00 AM, 2:00 PM, 4:00 PM\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" schedule_meeting (call_QoAhuMIm62iXpn5G7kfYUb5i)\n",
" Call ID: call_QoAhuMIm62iXpn5G7kfYUb5i\n",
" Args:\n",
" attendees: ['pm@client.com', 'catherine@company.com']\n",
" subject: Tax Planning Strategies Discussion\n",
" duration_minutes: 45\n",
" preferred_day: 2025-05-06T00:00:00\n",
" start_time: 14\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Meeting 'Tax Planning Strategies Discussion' scheduled on Tuesday, May 06, 2025 at 14 for 45 minutes with 2 attendees\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" write_email (call_A5cS0nYiJiRL1shjzlwyq6lm)\n",
" Call ID: call_A5cS0nYiJiRL1shjzlwyq6lm\n",
" Args:\n",
" to: pm@client.com\n",
" subject: Re: Tax season let's schedule call\n",
" content: Hello,\n",
"\n",
"Thank you for reaching out about tax planning strategies ahead of the filing deadline. I appreciate your suggestions and am glad to schedule a call.\n",
"\n",
"Ive scheduled our meeting for Tuesday, May 6, 2025, at 2:00 PM for 45 minutes, as requested. If you need to reschedule or prefer a different time on Thursday, please let me know.\n",
"\n",
"Looking forward to our discussion.\n",
"\n",
"Best regards,\n",
"Catherine\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Email sent to pm@client.com with subject 'Re: Tax season let's schedule call' and content: Hello,\n",
"\n",
"Thank you for reaching out about tax planning strategies ahead of the filing deadline. I appreciate your suggestions and am glad to schedule a call.\n",
"\n",
"Ive scheduled our meeting for Tuesday, May 6, 2025, at 2:00 PM for 45 minutes, as requested. If you need to reschedule or prefer a different time on Thursday, please let me know.\n",
"\n",
"Looking forward to our discussion.\n",
"\n",
"Best regards,\n",
"Catherine\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_RFxR3Iq752oIMbviMud6IMQk)\n",
" Call ID: call_RFxR3Iq752oIMbviMud6IMQk\n",
" Args:\n",
" done: True\n"
]
}
],
"source": [
"state = graph.get_state(thread_config_1)\n",
"for m in state.values['messages']:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "fc176108-d800-49ca-897a-1c9b850bac3a",
"metadata": {},
"source": [
"#### Test 2. Edit `write_email` and `schedule_meeting`\n",
"\n",
"This test explores how the system learns from direct edits to its proposed actions. When users modify the agent's suggestions, it creates clear, specific learning signals about their preferences:\n",
"\n",
"1. We'll use the same tax planning email as before\n",
"2. When the agent proposes a 45-minute meeting, we'll edit it to:\n",
" - Change the duration to 30 minutes (matching our stored preference)\n",
" - Make the subject line more concise\n",
"3. When the agent drafts an email, we'll edit it to be:\n",
" - Shorter and less formal\n",
" - Structured differently\n",
"\n",
"Edits provide the most explicit feedback about user preferences, letting the system learn exactly what changes are desired. We expect to see specific, targeted updates to our memory stores that reflect these edits."
]
},
{
"cell_type": "code",
"execution_count": 115,
"id": "673e0852-0cd9-4f3a-86af-029534b244bc",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running the graph until the first interrupt...\n",
"📧 Classification: RESPOND - This email requires a response\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'schedule_meeting', 'args': {'attendees': ['pm@client.com', 'catherine@company.com'], 'subject': 'Tax Planning Strategies Discussion', 'duration_minutes': 45, 'preferred_day': '2025-05-06', 'start_time': 14}}\n",
"\n",
"======= CURRENT MEMORY CONTENT =======\n",
"\n",
"--- cal_preferences ---\n",
"{'preferences': '\\n30 minute meetings are preferred, but 15 minute meetings are also acceptable.\\n'}\n"
]
}
],
"source": [
"# Same email as before\n",
"email_input_respond = {\n",
" \"to\": \"Catherine Qiao <catherine@company.com>\",\n",
" \"author\": \"Project Manager <pm@client.com>\",\n",
" \"subject\": \"Tax season let's schedule call\",\n",
" \"email_thread\": \"Catherine,\\n\\nIt's tax season again. Before the filing deadline, I would highly recommend scheduling a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\\n\\nAre you available sometime soon? Tuesday or Thursday afternoon on May 6, 2025 or May 8, 2025 would work best for me, for about 45 minutes.\\n\\nRegards,\\nProject Manager\"\n",
"}\n",
"# Compile the graph with new thread\n",
"store = InMemoryStore()\n",
"graph = overall_workflow.compile(checkpointer=checkpointer, store=store)\n",
"thread_id_2 = uuid.uuid4()\n",
"thread_config_2 = {\"configurable\": {\"thread_id\": thread_id_2}}\n",
"\n",
"# Run the graph until the first interrupt - will be classified as \"respond\" and the agent will create a write_email tool call\n",
"print(\"Running the graph until the first interrupt...\")\n",
"for chunk in graph.stream({\"email_input\": email_input_respond}, config=thread_config_2):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")\n",
"\n",
"# Check memory after first interrupt\n",
"display_memory_content(store,(\"email_assistant\", \"cal_preferences\"))"
]
},
{
"cell_type": "markdown",
"id": "f21111a5-c053-4866-a0a2-c55ae0f05061",
"metadata": {},
"source": [
"**Edit the `schedule_meeting` tool call**\n",
"\n",
"When we edit the meeting proposal, we're providing direct, explicit feedback about our preferences. This creates a significant learning opportunity for the system:\n",
"\n",
"1. The agent initially proposes a 45-minute meeting (the duration requested in the email)\n",
"2. We edit it to 30 minutes and simplify the subject from \"Tax Planning Strategies Discussion\" to \"Tax Planning Discussion\"\n",
"3. This creates clear, specific feedback about our time preferences and naming conventions\n",
"\n",
"After the edit, we'll check the calendar preferences memory store to see how it's updated. The memory update should capture both:\n",
"- Our preference for shorter 30-minute meetings\n",
"- Our preference for more concise meeting subjects\n",
"\n",
"The trace reveals the precise memory update logic, showing how the system analyzes the difference between its proposal and our edits to extract meaningful patterns and preferences. We can see the detailed justification for each memory update, ensuring transparency in the learning process."
]
},
{
"cell_type": "code",
"execution_count": 116,
"id": "aa54c7da-65a8-4835-92d2-483d96830a65",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user editing the schedule_meeting tool call...\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'write_email', 'args': {'to': 'pm@client.com', 'subject': \"RE: Tax season let's schedule call\", 'content': \"Thank you for reaching out. I agree it's a good idea to review tax planning strategies ahead of the filing deadline. \\n\\nI've scheduled a 30-minute call for Tuesday, May 6, 2025 at 2:00 PM to discuss this. If you believe we'll need more time, please let me know and I can adjust the meeting duration accordingly. \\n\\nLooking forward to your suggestions.\\n\\nBest regards,\\nCatherine\"}}\n",
"\n",
"Checking memory after editing schedule_meeting:\n",
"\n",
"======= CURRENT MEMORY CONTENT =======\n",
"\n",
"--- cal_preferences ---\n",
"{'preferences': \"30 minute meetings are preferred, but 15 minute meetings are also acceptable.\\n\\nSUBJECT LINES:\\n- Prefer concise meeting subject lines (e.g., 'Tax Planning' instead of 'Tax Planning Strategies Discussion').\"}\n"
]
}
],
"source": [
"# Now simulate user editing the schedule_meeting tool call\n",
"print(\"\\nSimulating user editing the schedule_meeting tool call...\")\n",
"edited_schedule_args = {\n",
" \"attendees\": [\"pm@client.com\", \"catherine@company.com\"],\n",
" \"subject\": \"Tax Planning\",\n",
" \"duration_minutes\": 30, # Changed from 45 to 30\n",
" \"preferred_day\": \"2025-05-06\",\n",
" \"start_time\": 14 \n",
"}\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"edit\", \"args\": {\"args\": edited_schedule_args}}]), config=thread_config_2):\n",
" # Inspect response_agent most recent message\n",
" if 'response_agent' in chunk:\n",
" chunk['response_agent']['messages'][-1].pretty_print()\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")\n",
"\n",
"# Check memory after editing schedule_meeting\n",
"print(\"\\nChecking memory after editing schedule_meeting:\")\n",
"display_memory_content(store,(\"email_assistant\", \"cal_preferences\"))"
]
},
{
"cell_type": "markdown",
"id": "c6727295-fe18-4365-b395-928c3ba23a4c",
"metadata": {},
"source": [
"**An example update:**\n",
"\n",
"```\n",
"{'preferences': '\\n30 minute meetings are preferred, but 15 minute meetings are also acceptable.\\n'}\n",
"```\n",
"\n",
"```\n",
"{'preferences': \"30 minute meetings are preferred, but 15 minute meetings are also acceptable.\\n\\nUser prefers 30 minute meetings over longer durations such as 45 minutes. When scheduling, default to 30 minutes unless otherwise specified. Subject lines should be concise (e.g., 'Tax Planning Discussion' instead of 'Tax Planning Strategies Discussion').\"}\n",
"```"
]
},
{
"cell_type": "markdown",
"id": "7ace4691-a6eb-48c7-95bd-02f0ad006e82",
"metadata": {},
"source": [
"Looking at the memory after editing the calendar invitation:\n",
"\n",
"1. The system identifies that we prefer 30-minute meetings over longer durations\n",
"2. It's also captured our preference for concise meeting subjects\n",
"\n",
"What's particularly impressive about this memory update is:\n",
"- It doesn't just record our specific edit, but generalizes to a broader preference pattern\n",
"- It preserves all existing memory content while adding the new information\n",
"- It extracts multiple preference signals from a single edit interaction\n",
"\n",
"Now, let's **edit the email draft** to see how the system captures different types of communication preferences:"
]
},
{
"cell_type": "code",
"execution_count": 117,
"id": "b7f4e97c-2c5f-47b7-b07b-10dd1cedea87",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"======= CURRENT MEMORY CONTENT =======\n",
"\n",
"--- response_preferences ---\n",
"{'preferences': \"\\nUse professional and concise language. If the e-mail mentions a deadline, make sure to explicitly acknowledge and reference the deadline in your response.\\n\\nWhen responding to technical questions that require investigation:\\n- Clearly state whether you will investigate or who you will ask\\n- Provide an estimated timeline for when you'll have more information or complete the task\\n\\nWhen responding to event or conference invitations:\\n- Always acknowledge any mentioned deadlines (particularly registration deadlines)\\n- If workshops or specific topics are mentioned, ask for more specific details about them\\n- If discounts (group or early bird) are mentioned, explicitly request information about them\\n- Don't commit \\n\\nWhen responding to collaboration or project-related requests:\\n- Acknowledge any existing work or materials mentioned (drafts, slides, documents, etc.)\\n- Explicitly mention reviewing these materials before or during the meeting\\n- When scheduling meetings, clearly state the specific day, date, and time proposed\\n\\nWhen responding to meeting scheduling requests:\\n- If times are proposed, verify calendar availability for all time slots mentioned in the original email and then commit to one of the proposed times based on your availability by scheduling the meeting. Or, say you can't make it at the time proposed.\\n- If no times are proposed, then check your calendar for availability and propose multiple time options when available instead of selecting just one.\\n- Mention the meeting duration in your response to confirm you've noted it correctly.\\n- Reference the meeting's purpose in your response.\\n\"}\n",
"\n",
"Simulating user editing the write_email tool call...\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_BhofJmfzJdDBGKW63JVwLHBb)\n",
" Call ID: call_BhofJmfzJdDBGKW63JVwLHBb\n",
" Args:\n",
" done: True\n",
"\n",
"Checking memory after editing write_email:\n",
"\n",
"======= CURRENT MEMORY CONTENT =======\n",
"\n",
"--- response_preferences ---\n",
"{'preferences': \"Use professional and concise language. If the e-mail mentions a deadline, make sure to explicitly acknowledge and reference the deadline in your response.\\n\\nWhen responding to technical questions that require investigation:\\n- Clearly state whether you will investigate or who you will ask\\n- Provide an estimated timeline for when you'll have more information or complete the task\\n\\nWhen responding to event or conference invitations:\\n- Always acknowledge any mentioned deadlines (particularly registration deadlines)\\n- If workshops or specific topics are mentioned, ask for more specific details about them\\n- If discounts (group or early bird) are mentioned, explicitly request information about them\\n- Don't commit \\n\\nWhen responding to collaboration or project-related requests:\\n- Acknowledge any existing work or materials mentioned (drafts, slides, documents, etc.)\\n- Explicitly mention reviewing these materials before or during the meeting\\n- When scheduling meetings, clearly state the specific day, date, and time proposed\\n\\nWhen responding to meeting scheduling requests:\\n- If times are proposed, verify calendar availability for all time slots mentioned in the original email and then commit to one of the proposed times based on your availability by scheduling the meeting. Or, say you can't make it at the time proposed.\\n- If no times are proposed, then check your calendar for availability and propose multiple time options when available instead of selecting just one.\\n- Mention the meeting duration in your response to confirm you've noted it correctly.\\n- Reference the meeting's purpose in your response.\\n- Prefer confirming availability with the recipient by proposing a specific time and asking if it works for them, rather than directly scheduling unilaterally.\"}\n"
]
}
],
"source": [
"display_memory_content(store,(\"email_assistant\", \"response_preferences\"))\n",
"# Now simulate user editing the write_email tool call\n",
"print(\"\\nSimulating user editing the write_email tool call...\")\n",
"edited_email_args = {\n",
" \"to\": \"pm@client.com\",\n",
" \"subject\": \"Re: Tax season let's schedule call\",\n",
" \"content\": \"Hello Project Manager,\\n\\nThank you for reaching out about tax planning. I believe half an hour would be sufficient for us. I scheduled a 30-minute call on May 6, 2025 at 3:00 PM. Would that work for you?\\n\\nBest regards,\\nCatherine Qiao\"\n",
"}\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"edit\", \"args\": {\"args\": edited_email_args}}]), config=thread_config_2):\n",
" # Inspect response_agent most recent message\n",
" if 'response_agent' in chunk:\n",
" chunk['response_agent']['messages'][-1].pretty_print()\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")\n",
"\n",
"# Check memory after editing write_email\n",
"print(\"\\nChecking memory after editing write_email:\")\n",
"display_memory_content(store,(\"email_assistant\", \"response_preferences\"))"
]
},
{
"cell_type": "markdown",
"id": "edcb5c79-ad78-4215-9997-1da69c13b8e9",
"metadata": {},
"source": [
"Our email edit reveals even more sophisticated learning capabilities:\n",
"\n",
"1. We've shortened and simplified the email content\n",
"2. We've added a question asking for confirmation rather than assuming the time works\n",
"\n",
"Looking at the updated memory, we can see that the system has extracted a key insight about our communication style:\n",
"\n",
"```\n",
"When scheduling a meeting, ask the recipient to confirm if the proposed time works for them, rather than assuming and stating the meeting is already scheduled.\n",
"```\n",
"\n",
"This demonstrates the system's ability to:\n",
"- Analyze our edit not just at a superficial level, but to understand intent\n",
"- Extract generalizable principles from specific examples\n",
"- Preserve all existing guidance while adding new insights\n",
"- Maintain the organization and structure of the memory\n",
"\n",
"These targeted, high-quality memory updates will improve all future interactions without requiring repeated corrections."
]
},
{
"cell_type": "code",
"execution_count": 118,
"id": "fd0ec9b0-6861-4866-a51d-aca1326eca64",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Respond to the email: \n",
"\n",
"**Subject**: Tax season let's schedule call\n",
"**From**: Project Manager <pm@client.com>\n",
"**To**: Catherine Qiao <catherine@company.com>\n",
"\n",
"Catherine,\n",
"\n",
"It's tax season again. Before the filing deadline, I would highly recommend scheduling a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\n",
"\n",
"Are you available sometime soon? Tuesday or Thursday afternoon on May 6, 2025 or May 8, 2025 would work best for me, for about 45 minutes.\n",
"\n",
"Regards,\n",
"Project Manager\n",
"\n",
"---\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" check_calendar_availability (call_CfOZvLFFmR3Mxxq4OJKcbuzM)\n",
" Call ID: call_CfOZvLFFmR3Mxxq4OJKcbuzM\n",
" Args:\n",
" day: 2025-05-06\n",
" check_calendar_availability (call_XlDSqf5XMdtUbLFSROKobAFz)\n",
" Call ID: call_XlDSqf5XMdtUbLFSROKobAFz\n",
" Args:\n",
" day: 2025-05-08\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-05-06: 9:00 AM, 2:00 PM, 4:00 PM\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-05-08: 9:00 AM, 2:00 PM, 4:00 PM\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" schedule_meeting (call_3erbeIgvgUftxdzpUhjhXp57)\n",
" Call ID: call_3erbeIgvgUftxdzpUhjhXp57\n",
" Args:\n",
" attendees: ['pm@client.com', 'catherine@company.com']\n",
" subject: Tax Planning\n",
" duration_minutes: 30\n",
" preferred_day: 2025-05-06\n",
" start_time: 14\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Meeting 'Tax Planning' scheduled on Tuesday, May 06, 2025 at 14 for 30 minutes with 2 attendees\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" write_email (call_e3udgk6XrCk4dJIFFcx7TZNk)\n",
" Call ID: call_e3udgk6XrCk4dJIFFcx7TZNk\n",
" Args:\n",
" to: pm@client.com\n",
" subject: Re: Tax season let's schedule call\n",
" content: Hello Project Manager,\n",
"\n",
"Thank you for reaching out about tax planning. I believe half an hour would be sufficient for us. I scheduled a 30-minute call on May 6, 2025 at 3:00 PM. Would that work for you?\n",
"\n",
"Best regards,\n",
"Catherine Qiao\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Email sent to pm@client.com with subject 'Re: Tax season let's schedule call' and content: Hello Project Manager,\n",
"\n",
"Thank you for reaching out about tax planning. I believe half an hour would be sufficient for us. I scheduled a 30-minute call on May 6, 2025 at 3:00 PM. Would that work for you?\n",
"\n",
"Best regards,\n",
"Catherine Qiao\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_BhofJmfzJdDBGKW63JVwLHBb)\n",
" Call ID: call_BhofJmfzJdDBGKW63JVwLHBb\n",
" Args:\n",
" done: True\n"
]
}
],
"source": [
"state = graph.get_state(thread_config_2)\n",
"for m in state.values['messages']:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "30006ddc-29a3-4b69-8592-80d11f878763",
"metadata": {},
"source": [
"#### Test 3. Respond (with feedback) `write_email`, `schedule_meeting`, and `question`\n",
"\n",
"Our final test set explores the \"response\" feedback pattern - providing guidance without directly editing or accepting. This conversational feedback mechanism offers a middle ground between acceptance and editing:\n",
"\n",
"1. First, we'll test feedback for meeting scheduling by requesting:\n",
" - Shorter duration (30 minutes instead of 45)\n",
" - Afternoon meeting times (after 2pm)\n",
" \n",
"2. Next, we'll test feedback for email drafting by requesting:\n",
" - Shorter, less formal language\n",
" - A specific closing statement about looking forward to the meeting\n",
" \n",
"3. Finally, we'll test feedback for questions by providing:\n",
" - A direct answer with additional context\n",
" - Specific preferences (brunch location, time)\n",
"\n",
"This natural language feedback approach lets users guide the assistant without having to do the work themselves. We expect to see detailed memory updates that extract the general principles from our specific feedback."
]
},
{
"cell_type": "code",
"execution_count": 120,
"id": "5e00d916-e2b4-40bf-9175-9b859688f8ac",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Running the graph until the first interrupt...\n",
"📧 Classification: RESPOND - This email requires a response\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'schedule_meeting', 'args': {'attendees': ['pm@client.com', 'catherine@company.com'], 'subject': 'Tax Planning Strategies Discussion', 'duration_minutes': 45, 'preferred_day': '2025-05-06', 'start_time': 14}}\n",
"\n",
"======= CURRENT MEMORY CONTENT =======\n",
"\n",
"--- cal_preferences ---\n",
"{'preferences': '\\n30 minute meetings are preferred, but 15 minute meetings are also acceptable.\\n'}\n"
]
}
],
"source": [
"# Respond - Meeting Request Email\n",
"email_input_respond = {\n",
" \"to\": \"Catherine Qiao <catherine@company.com>\",\n",
" \"author\": \"Project Manager <pm@client.com>\",\n",
" \"subject\": \"Tax season let's schedule call\",\n",
" \"email_thread\": \"Catherine,\\n\\nIt's tax season again. Before the filing deadline, I would highly recommend scheduling a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\\n\\nAre you available sometime soon? Tuesday or Thursday afternoon on May 6, 2025 or May 8, 2025 would work best for me, for about 45 minutes.\\n\\nRegards,\\nProject Manager\"\n",
"}\n",
"# Compile the graph\n",
"store = InMemoryStore()\n",
"graph = overall_workflow.compile(checkpointer=checkpointer, store=store)\n",
"thread_id_5 = uuid.uuid4()\n",
"thread_config_5 = {\"configurable\": {\"thread_id\": thread_id_5}}\n",
"\n",
"# Run the graph until the first interrupt \n",
"# Email will be classified as \"respond\" \n",
"# Agent will create a schedule_meeting and write_email tool call\n",
"print(\"Running the graph until the first interrupt...\")\n",
"for chunk in graph.stream({\"email_input\": email_input_respond}, config=thread_config_5):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")\n",
"\n",
"# Check memory after first interrupt \n",
"display_memory_content(store, (\"email_assistant\", \"cal_preferences\"))"
]
},
{
"cell_type": "markdown",
"id": "48ca01c9-a21b-4e0c-ac28-896c5b5a2643",
"metadata": {},
"source": [
"**Provide feedback for the `schedule_meeting` tool call**\n",
"\n",
"Instead of directly editing the meeting proposal or simply accepting it, we'll provide natural language feedback:\n",
"\n",
"1. We request a 30-minute meeting instead of 45 minutes\n",
"2. We express a preference for afternoon meetings after 2pm\n",
"3. The system must interpret this feedback and generate a new proposal\n",
"\n",
"This conversational approach is often more natural and efficient than direct editing, especially for mobile users or those who prefer to give high-level direction rather than detailed edits.\n",
"\n",
"After providing feedback, we'll examine the calendar preferences memory to see how this natural language guidance is captured. We expect to see the system extract both the meeting duration and time-of-day preferences as general principles."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f8b9d4be-90d9-4737-9be5-a227a171ad6d",
"metadata": {},
"outputs": [],
"source": [
"print(f\"\\nSimulating user providing feedback for the {Interrupt_Object.value[0]['action_request']['action']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"response\", \"args\": \"Please schedule this for 30 minutes instead of 45 minutes, and I prefer afternoon meetings after 2pm.\"}]), config=thread_config_5):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")\n",
"\n",
"# Check memory after providing feedback for schedule_meeting\n",
"print(\"\\nChecking memory after providing feedback for schedule_meeting:\")\n",
"display_memory_content(store, (\"email_assistant\", \"cal_preferences\"))"
]
},
{
"cell_type": "markdown",
"id": "59853ed4-03d7-4184-8916-1992beb996eb",
"metadata": {},
"source": [
"Our memory check after providing feedback shows an elegantly simple calendar preference update:\n",
"\n",
"```\n",
"30 minute meetings are preferred, but 15 minute meetings are also acceptable.\n",
"Afternoon meetings after 2pm are preferred.\n",
"```\n",
"\n",
"The system has:\n",
"1. Captured both aspects of our feedback (duration and time of day)\n",
"2. Preserved the existing preference about 15-minute meetings\n",
"3. Added our preference for afternoon meetings after 2pm as a new line\n",
"4. Kept the format clean and readable\n",
"\n",
"This natural language feedback mechanism creates the same quality of memory updates as direct editing but requires less effort from the user. The system is able to extract structured preferences from unstructured feedback, showing its ability to learn from conversational interactions.\n",
"\n",
"Let's accept this revised meeting proposal and move to the email draft:"
]
},
{
"cell_type": "code",
"execution_count": 121,
"id": "7df92aea-91c9-4dcf-9fd4-a1a0007db313",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user accepting the schedule_meeting tool call...\n",
"\n",
"INTERRUPT OBJECT:\n",
"Action Request: {'action': 'write_email', 'args': {'to': 'pm@client.com', 'subject': \"Re: Tax season let's schedule call\", 'content': \"Hello,\\n\\nThank you for reaching out regarding tax planning for this year. I appreciate your suggestions and agree that a timely discussion is important before the filing deadline.\\n\\nI've scheduled our call for Tuesday, May 6, 2025, at 2:00 PM for 45 minutes, as you suggested an afternoon availability. Please let me know if this still works for you or if another time is preferable.\\n\\nLooking forward to discussing your recommendations.\\n\\nBest regards,\\nCatherine\"}}\n",
"\n",
"Checking memory after accepting schedule_meeting after feedback:\n",
"\n",
"======= CURRENT MEMORY CONTENT =======\n",
"\n",
"--- response_preferences ---\n",
"{'preferences': \"\\nUse professional and concise language. If the e-mail mentions a deadline, make sure to explicitly acknowledge and reference the deadline in your response.\\n\\nWhen responding to technical questions that require investigation:\\n- Clearly state whether you will investigate or who you will ask\\n- Provide an estimated timeline for when you'll have more information or complete the task\\n\\nWhen responding to event or conference invitations:\\n- Always acknowledge any mentioned deadlines (particularly registration deadlines)\\n- If workshops or specific topics are mentioned, ask for more specific details about them\\n- If discounts (group or early bird) are mentioned, explicitly request information about them\\n- Don't commit \\n\\nWhen responding to collaboration or project-related requests:\\n- Acknowledge any existing work or materials mentioned (drafts, slides, documents, etc.)\\n- Explicitly mention reviewing these materials before or during the meeting\\n- When scheduling meetings, clearly state the specific day, date, and time proposed\\n\\nWhen responding to meeting scheduling requests:\\n- If times are proposed, verify calendar availability for all time slots mentioned in the original email and then commit to one of the proposed times based on your availability by scheduling the meeting. Or, say you can't make it at the time proposed.\\n- If no times are proposed, then check your calendar for availability and propose multiple time options when available instead of selecting just one.\\n- Mention the meeting duration in your response to confirm you've noted it correctly.\\n- Reference the meeting's purpose in your response.\\n\"}\n"
]
}
],
"source": [
"print(f\"\\nSimulating user accepting the {Interrupt_Object.value[0]['action_request']['action']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"accept\"}]), config=thread_config_5):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")\n",
"\n",
"# Check memory after accepting schedule_meeting after feedback\n",
"print(\"\\nChecking memory after accepting schedule_meeting after feedback:\")\n",
"display_memory_content(store, (\"email_assistant\", \"response_preferences\"))"
]
},
{
"cell_type": "markdown",
"id": "e9b6cec0-e955-4441-9a01-3d7d52576714",
"metadata": {},
"source": [
"**Now provide feedback for the `write_email` tool call**\n",
"\n",
"Similar to our meeting feedback, we'll now provide natural language guidance for the email draft:\n",
"\n",
"1. We request \"shorter and less formal\" language - a style preference\n",
"2. We ask for a specific closing statement about looking forward to the meeting\n",
"3. The system must interpret this guidance and rewrite the email accordingly\n",
"\n",
"After providing this feedback, we'll check the response preferences memory to see how these style and structure preferences are captured. We expect to see generalizable guidelines about email brevity, formality, and closing statements added to our preference profile."
]
},
{
"cell_type": "code",
"execution_count": 124,
"id": "6ce95ab5-1749-4838-866a-786cbc537c9f",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user providing feedback for the write_email tool call...\n",
"\n",
"Checking memory after providing feedback for write_email:\n",
"\n",
"======= CURRENT MEMORY CONTENT =======\n",
"\n",
"--- response_preferences ---\n",
"{'preferences': \"Use professional and concise language. If the e-mail mentions a deadline, make sure to explicitly acknowledge and reference the deadline in your response.\\n\\nWhen responding to technical questions that require investigation:\\n- Clearly state whether you will investigate or who you will ask\\n- Provide an estimated timeline for when you'll have more information or complete the task\\n\\nWhen responding to event or conference invitations:\\n- Always acknowledge any mentioned deadlines (particularly registration deadlines)\\n- If workshops or specific topics are mentioned, ask for more specific details about them\\n- If discounts (group or early bird) are mentioned, explicitly request information about them\\n- Don't commit \\n\\nWhen responding to collaboration or project-related requests:\\n- Acknowledge any existing work or materials mentioned (drafts, slides, documents, etc.)\\n- Explicitly mention reviewing these materials before or during the meeting\\n- When scheduling meetings, clearly state the specific day, date, and time proposed\\n\\nWhen responding to meeting scheduling requests:\\n- If times are proposed, verify calendar availability for all time slots mentioned in the original email and then commit to one of the proposed times based on your availability by scheduling the meeting. Or, say you can't make it at the time proposed.\\n- If no times are proposed, then check your calendar for availability and propose multiple time options when available instead of selecting just one.\\n- Mention the meeting duration in your response to confirm you've noted it correctly.\\n- Reference the meeting's purpose in your response.\\n\\nWhen responding, prefer shorter and less formal replies when possible. Include a closing statement expressing that you look forward to the meeting or discussion.\"}\n"
]
}
],
"source": [
"print(f\"\\nSimulating user providing feedback for the {Interrupt_Object.value[0]['action_request']['action']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"response\", \"args\": \"Shorter and less formal. Always include a joke\"}]), config=thread_config_5):\n",
" # Inspect response_agent most recent message\n",
" if 'response_agent' in chunk:\n",
" chunk['response_agent']['messages'][-1].pretty_print()\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")\n",
"\n",
"# Check memory after providing feedback for write_email\n",
"print(\"\\nChecking memory after providing feedback for write_email:\")\n",
"display_memory_content(store, (\"email_assistant\", \"response_preferences\"))"
]
},
{
"cell_type": "markdown",
"id": "27c2656e-451f-4770-926f-f0a53e3b4588",
"metadata": {},
"source": [
"The memory update after our email feedback shows sophisticated learning about both meeting scheduling and email writing preferences:\n",
"\n",
"1. The system has added a complete new section to the response preferences entitled \"When writing email responses\" with two key preferences:\n",
" - \"Favor shorter and less formal language when possible, unless the context requires formality\"\n",
" - \"Include a closing statement expressing that you look forward to the meeting or conversation when confirming appointments\"\n",
"\n",
"2. It has also added a new bullet point to the \"When responding to meeting scheduling requests\" section:\n",
" - \"When scheduling meetings, prefer afternoon times after 2pm when possible, and default to 30-minute durations unless otherwise specified\"\n",
"\n",
"This demonstrates the system's ability to:\n",
"- Organize learned preferences into appropriate categories\n",
"- Extract multiple insights from a single feedback instance\n",
"- Apply meeting preferences to both calendar and email contexts\n",
"- Capture nuance with appropriate qualifiers (\"when possible,\" \"unless otherwise specified\")\n",
"- Maintain the hierarchical structure of the memory\n",
"\n",
"The resulting email shows all these preferences applied: it's shorter, less formal, includes a closing statement about looking forward to the chat, and correctly references the 30-minute meeting time."
]
},
{
"cell_type": "code",
"execution_count": 125,
"id": "0ca611db-ce63-4057-9dda-cd6b3d804c9a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Simulating user accepting the write_email tool call...\n",
"\n",
"Checking memory after accepting write_email after feedback:\n",
"\n",
"======= CURRENT MEMORY CONTENT =======\n",
"\n",
"--- response_preferences ---\n",
"{'preferences': \"Use professional and concise language. If the e-mail mentions a deadline, make sure to explicitly acknowledge and reference the deadline in your response.\\n\\nWhen responding to technical questions that require investigation:\\n- Clearly state whether you will investigate or who you will ask\\n- Provide an estimated timeline for when you'll have more information or complete the task\\n\\nWhen responding to event or conference invitations:\\n- Always acknowledge any mentioned deadlines (particularly registration deadlines)\\n- If workshops or specific topics are mentioned, ask for more specific details about them\\n- If discounts (group or early bird) are mentioned, explicitly request information about them\\n- Don't commit \\n\\nWhen responding to collaboration or project-related requests:\\n- Acknowledge any existing work or materials mentioned (drafts, slides, documents, etc.)\\n- Explicitly mention reviewing these materials before or during the meeting\\n- When scheduling meetings, clearly state the specific day, date, and time proposed\\n\\nWhen responding to meeting scheduling requests:\\n- If times are proposed, verify calendar availability for all time slots mentioned in the original email and then commit to one of the proposed times based on your availability by scheduling the meeting. Or, say you can't make it at the time proposed.\\n- If no times are proposed, then check your calendar for availability and propose multiple time options when available instead of selecting just one.\\n- Mention the meeting duration in your response to confirm you've noted it correctly.\\n- Reference the meeting's purpose in your response.\\n\\nWhen responding, prefer shorter and less formal replies when possible. Include a closing statement expressing that you look forward to the meeting or discussion.\"}\n"
]
}
],
"source": [
"print(f\"\\nSimulating user accepting the {Interrupt_Object.value[0]['action_request']['action']} tool call...\")\n",
"for chunk in graph.stream(Command(resume=[{\"type\": \"accept\"}]), config=thread_config_5):\n",
" # Inspect interrupt object if present\n",
" if '__interrupt__' in chunk:\n",
" Interrupt_Object = chunk['__interrupt__'][0]\n",
" print(\"\\nINTERRUPT OBJECT:\")\n",
" print(f\"Action Request: {Interrupt_Object.value[0]['action_request']}\")\n",
"\n",
"# Check memory after accepting write_email after feedback\n",
"print(\"\\nChecking memory after accepting write_email after feedback:\")\n",
"display_memory_content(store, (\"email_assistant\", \"response_preferences\"))"
]
},
{
"cell_type": "markdown",
"id": "01f55990-d28c-4ba2-a5c2-960ba74367c4",
"metadata": {},
"source": [
"Look at the full message history."
]
},
{
"cell_type": "code",
"execution_count": 126,
"id": "a800cb35-0aa9-4ed2-a42b-1df904d9e0b8",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001b[1m Human Message \u001b[0m=================================\n",
"\n",
"Respond to the email: \n",
"\n",
"**Subject**: Tax season let's schedule call\n",
"**From**: Project Manager <pm@client.com>\n",
"**To**: Catherine Qiao <catherine@company.com>\n",
"\n",
"Catherine,\n",
"\n",
"It's tax season again. Before the filing deadline, I would highly recommend scheduling a call to discuss your tax planning strategies for this year. I have some suggestions that could potentially save you money.\n",
"\n",
"Are you available sometime soon? Tuesday or Thursday afternoon on May 6, 2025 or May 8, 2025 would work best for me, for about 45 minutes.\n",
"\n",
"Regards,\n",
"Project Manager\n",
"\n",
"---\n",
"\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" check_calendar_availability (call_98fQpFG6ksdQ36Shqq15LTjd)\n",
" Call ID: call_98fQpFG6ksdQ36Shqq15LTjd\n",
" Args:\n",
" day: 2025-05-06\n",
" check_calendar_availability (call_17rVLP5S0GXMWqw29ujwWe7P)\n",
" Call ID: call_17rVLP5S0GXMWqw29ujwWe7P\n",
" Args:\n",
" day: 2025-05-08\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-05-06: 9:00 AM, 2:00 PM, 4:00 PM\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Available times on 2025-05-08: 9:00 AM, 2:00 PM, 4:00 PM\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" schedule_meeting (call_qNBepSQILnWDOFXelyMHs86t)\n",
" Call ID: call_qNBepSQILnWDOFXelyMHs86t\n",
" Args:\n",
" attendees: ['pm@client.com', 'catherine@company.com']\n",
" subject: Tax Planning Strategies Discussion\n",
" duration_minutes: 45\n",
" preferred_day: 2025-05-06\n",
" start_time: 14\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Meeting 'Tax Planning Strategies Discussion' scheduled on Tuesday, May 06, 2025 at 14 for 45 minutes with 2 attendees\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" write_email (call_mtZMGw4jR5CXnnaj67aHtHME)\n",
" Call ID: call_mtZMGw4jR5CXnnaj67aHtHME\n",
" Args:\n",
" to: pm@client.com\n",
" subject: Re: Tax season let's schedule call\n",
" content: Hello,\n",
"\n",
"Thank you for reaching out regarding tax planning for this year. I appreciate your suggestions and agree that a timely discussion is important before the filing deadline.\n",
"\n",
"I've scheduled our call for Tuesday, May 6, 2025, at 2:00 PM for 45 minutes, as you suggested an afternoon availability. Please let me know if this still works for you or if another time is preferable.\n",
"\n",
"Looking forward to discussing your recommendations.\n",
"\n",
"Best regards,\n",
"Catherine\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"User gave feedback, which can we incorporate into the email. Feedback: Shorter and less formal. Include a closing statement about looking forward to the meeting!\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" write_email (call_WPP2Dvw93frZAP3z2E8Q9KeO)\n",
" Call ID: call_WPP2Dvw93frZAP3z2E8Q9KeO\n",
" Args:\n",
" to: pm@client.com\n",
" subject: Re: Tax season let's schedule call\n",
" content: Hi,\n",
"\n",
"Thanks for suggesting a call! I've booked us for Tuesday, May 6 at 2:00 PM for 45 minutes. Looking forward to hearing your tax planning ideas and saving some money this year.\n",
"\n",
"See you then!\n",
"=================================\u001b[1m Tool Message \u001b[0m=================================\n",
"\n",
"Email sent to pm@client.com with subject 'Re: Tax season let's schedule call' and content: Hi,\n",
"\n",
"Thanks for suggesting a call! I've booked us for Tuesday, May 6 at 2:00 PM for 45 minutes. Looking forward to hearing your tax planning ideas and saving some money this year.\n",
"\n",
"See you then!\n",
"==================================\u001b[1m Ai Message \u001b[0m==================================\n",
"Tool Calls:\n",
" Done (call_Un9NdIgxLEN8pgU0tvZpvkrt)\n",
" Call ID: call_Un9NdIgxLEN8pgU0tvZpvkrt\n",
" Args:\n",
" done: True\n"
]
}
],
"source": [
"state = graph.get_state(thread_config_5)\n",
"for m in state.values['messages']:\n",
" m.pretty_print()"
]
},
{
"cell_type": "markdown",
"id": "fed660f7-1ae3-48b8-83e7-fbd4655ec6bc",
"metadata": {},
"source": [
"## Part IV. Deployment\n",
"\n",
"You can find this graph with memory integration in the `src/email_assistant` directory:\n",
"\n",
"* `src/email_assistant/email_assistant_hitl_memory.py`"
]
},
{
"cell_type": "markdown",
"id": "244711e2-72f5-4f91-be15-b611a8cd90b4",
"metadata": {},
"source": [
"Email to test: \n",
"```\n",
"{\n",
" \"author\": \"Alice Smith <alice.smith@company.com>\",\n",
" \"to\": \"John Doe <john.doe@company.com>\",\n",
" \"subject\": \"Quick question about API documentation\",\n",
" \"email_thread\": \"Hi John,\\nI was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\\nSpecifically, I'm looking at:\\n- /auth/refresh\\n- /auth/validate\\nThanks!\\nAlice\"\n",
"}\n",
"```\n",
"\n",
"Testing and Deploying with LangGraph Platform gives you the full experience of a memory-enabled HITL system:\n",
"\n",
"1. **Deploy with LangGraph Platform**: Create a deployment on LangGraph Platform; you can also use local deployment by running `langgraph dev`\n",
"2. **Connect Agent Inbox**: Use the graph URL from the deployment to view in [Agent Inbox](https://github.com/langchain-ai/agent-inbox), our UI for human-in-the-loop LangGraph agents. You can follow the instructions to spin up a local version, or simply use the hosted version at [dev.agentinbox.ai](dev.agentinbox.ai)\n",
"3. **Submit test emails**: Try different email types to see classification in action\n",
"4. **Provide various feedback types**: Try accepting, editing, ignoring, and responding\n",
"5. **Observe memory evolution**: Check the Memory tab in LangGraph Studio to see changes\n",
"\n",
"\n",
"\n",
"\n",
"![inbox](img/agent-inbox-edit.png)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ef0df637-07e5-4aa9-a672-6c295070a3d2",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}