Welcome to the second installment of our series, “Managing Agentic Memory with LangMem.” In the first part, we explored the theoretical foundations of agentic memory.
Now, it’s time to roll up our sleeves and build something practical. In this tutorial, we will construct a baseline email assistant. This foundational agent will be able to triage incoming emails, draft responses, and even schedule meetings.
For now, our assistant will operate without any long-term memory. We’re focusing on building the core logic using hard-coded rules and a state machine architecture powered by LangGraph. This initial version will serve as the perfect scaffold upon which we’ll layer sophisticated memory capabilities in the upcoming lessons.
Table of Contents:
Setting the Stage: Profiles and Instructions
User Profile
Prompt Instructions
2. Defining The Triage Agent — Our Intelligent Router
3. The Main Agent — The Doer
Defining the Agent’s Tools
4. The Grand Assembly — Creating the Email Agent Graph
5. Testing the Full System
Scenario 1: A Spam Email
Scenario 2: An Actionable Email from a Colleague
Building agents with memory capabilities requires the right tools — and that’s where LangChain’s ecosystem comes in. LangChain provides the open-source foundation for LLM-powered applications, while LangGraph gives developers the framework to design stateful, multi-actor systems.
Now, with the addition of the Langmem SDK, there’s a dedicated library to handle the logic of creating, updating, and retrieving different types of long-term memory inside LangGraph.
Langmem is both flexible and modular: you can start with simple in-memory storage or connect to a production-ready database, depending on your needs.
In this series, we’ll explore how to put these ideas into practice by building an email assistant that demonstrates how semantic, episodic, and procedural memory can be effectively managed with LangGraph and Langmem.
Baseline Writing Assistant Agent [You are here!]
Writing Assistant Agent with Semantic Meomery [Coming Soon!]
Writing Assistant with Semantic & Episodic Memory [Coming Soon!]
Writing Assistant with Semantic, Episodic & Procedural Memory [Coming Soon!]
Get All My Books, One Button Away With 40% Off
I have created a bundle for my books and roadmaps, so you can buy everything with just one button and for 40% less than the original price. The bundle features 8 eBooks, including:
1. Setting the Stage: Profiles and Instructions
Before our agent can do anything, we need to give it a context to operate within. This involves defining who it’s working for and the rules it should follow.
First, we’ll load our necessary API tokens from a .env file.
import os
from dotenv import load_dotenv
_ = load_dotenv()
User Profile
Next, we’ll set up a profile. This contains basic facts about the user whose emails the assistant will manage. This information helps personalize the agent’s actions and responses. Feel free to change these details to match your own.
profile = {
"name": "Youssef",
"full_name": "Youssef Hosni",
"user_profile_background": "Senior Data Scientist leading a team of 5 AI engineers",
}
Prompt Instructions
We’ll then define a set of instructions. We’re separating these from the main prompts for two key reasons: it makes the system more modular and, importantly for this series, allows us to later generate some of these instructions dynamically using memory.
These instructions are split into two parts:
Triage Rules: These rules guide the first component of our agent, the “triage” step, which classifies emails into one of three categories: ignore, notify, or respond.
Agent Instructions: These are high-level instructions for the main response-drafting agent.
prompt_instructions = {
"triage_rules": {
"ignore": "Marketing newsletters, spam emails, mass company announcements",
"notify": "Team member out sick, build system notifications, project status updates",
"respond": "Direct questions from team members, meeting requests, critical bug reports",
},
"agent_instructions": "Use these tools when appropriate to help manage John's tasks efficiently."
}
Finally, let’s define a sample email to work with. This helps us understand the input schema (from, to, subject, body) and provides a concrete example for testing our components.
# Example incoming email
email = {
"from": "Oliver Jack <Oliver.Jack@ToDataBeyond.com>",
"to": "Youssef Hosni <Youssef.Hosni@ToDataBeyond.com>",
"subject": "Quick question about API documentation",
"body": """
Hi Youssef,
I was reviewing the apu 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?
Specifically, I'm looking at:
- /auth/refresh
- /auth/validate
Thanks!
Oliver""",
}
2. Defining The Triage Agent — Our Intelligent Router
The first and most critical step in our email processing pipeline is triage. We don’t want our agent wasting time and resources on spam or FYI-only announcements. This component will act as an intelligent router, deciding the fate of each incoming email.
First, let’s get our imports and set up the language model. We’re using OpenAI’s gpt-4o-mini for its speed and cost-effectiveness, but you can substitute it with your model of choice.
from pydantic import BaseModel, Field
from typing_extensions import TypedDict, Literal, Annotated
from langchain.chat_models import init_chat_model
llm = init_chat_model("openai:gpt-4o-mini")
To ensure our model’s output is predictable, we’ll define a Pydantic schema. This schema forces the LLM to provide not only a classification (ignore, respond, or notify) but also its reasoning.
class Router(BaseModel):
"""Analyze the unread email and route it according to its content."""
reasoning: str = Field(
description="Step-by-step reasoning behind the classification."
)
classification: Literal["ignore", "respond", "notify"] = Field(
description="The classification of an email: 'ignore' for irrelevant emails, "
"'notify' for important information that doesn't need a response, "
"'respond' for emails that need a reply",
)
We then bind this schema to our LLM using the .with_structured_output() method. This is a powerful feature that guarantees the LLM’s response will conform to our Router model.
llm_router = llm.with_structured_output(Router)
Keep reading with a 7-day free trial
Subscribe to To Data & Beyond to keep reading this post and get 7 days of free access to the full post archives.