LangGraph Memory Example

LangGraph is a library created by LangChain for building stateful, multi-agent applications. This example demonstrates using Zep for LangGraph agent memory.

A complete Notebook example of using Zep for LangGraph Memory may be found in the Zep Python SDK Repository.

The following example demonstrates a LangGraph agent using Zep for memory. Facts from Zep’s memory graph are used to personalize agent responses based on information learned from prior conversations.

The agent implements:

  • persistance of new chat turns to Zep and recall of relevant Facts using the most recent messages.
  • an in-memory MemorySaver to maintain agent state. We use this to add recent chat history to the agent prompt. As an alternative, you could use Zep for this.

You should consider truncating MemorySaver’s chat history as by default LangGraph state grows unbounded. We’ve included this in our example below. See the LangGraph documentation for insight.

Install dependencies

$pip install zep-cloud langchain-openai langgraph ipywidgets

Configure Zep

Ensure that you’ve configured the following API key in your environment. We’re using Zep’s Async client here, but we could also use the non-async equivalent.

$ZEP_API_KEY=
1from zep_cloud.client import AsyncZep
2from zep_cloud import Message
3
4zep = AsyncZep(api_key=os.environ.get('ZEP_API_KEY'))
1from langchain_core.messages import AIMessage, SystemMessage, trim_messages
2from langchain_core.tools import tool
3from langchain_openai import ChatOpenAI
4from langgraph.checkpoint.memory import MemorySaver
5from langgraph.graph import END, START, StateGraph, add_messages
6from langgraph.prebuilt import ToolNode

Using Zep’s Search as a Tool

This is an example of a simple Tool that searches Zep for facts related to a user, irrespective which chat session. We also include relevant facts from the current session in the Agent’s prompt.

1class State(TypedDict):
2 messages: Annotated[list, add_messages]
3 user_name: str
4 session_id: str
5
6
7@tool
8async def search_facts(state: State, query: str, limit: int = 5):
9 """Search for facts in all conversations had with a user.
10
11 Args:
12 state (State): The Agent's state.
13 query (str): The search query.
14 limit (int): The number of results to return. Defaults to 5.
15
16 Returns:
17 list: A list of facts that match the search query.
18 """
19 return await zep.memory.search_sessions(
20 user_id=state['user_name'],
21 text=query,
22 limit=limit,
23 search_scope="facts"
24 )
25
26
27tools = [search_facts]
28
29tool_node = ToolNode(tools)
30
31llm = ChatOpenAI(model='gpt-4o-mini', temperature=0).bind_tools(tools)

Chatbot Function Explanation

The chatbot uses Zep to provide context-aware responses. Here’s how it works:

  1. Context Retrieval: It retrieves relevant facts for the user’s current conversation (session). Zep uses the most recent messages to determine what facts to retrieve.

  2. System Message: It constructs a system message incorporating the facts retrieved in 1., setting the context for the AI’s response.

  3. Message Persistence: After generating a response, it asynchronously adds the user and assistant messages to Zep. New Facts are created and existing Facts updated using this new information.

  4. Messages in State: We use LangGraph state to store the most recent messages and add these to the Agent prompt. We limit the message list to the most recent 3 messages for demonstration purposes.

This approach enables the chatbot to maintain context across interactions and provide personalized responses based on the user’s history and preferences stored in Zep.

We could also use Zep to recall the chat history, rather than LangGraph’s MemorySaver.

See memory.get in the Zep API documentation.

1async def chatbot(state: State):
2 memory = await zep.memory.get(state["session_id"])
3 facts_string = ""
4 if memory.relevant_facts:
5 facts_string = "\n".join([f.fact for f in memory.relevant_facts])
6
7 system_message = SystemMessage(
8 content=f"""You are a compassionate mental health bot and caregiver. Review information about the user and their prior conversation below and respond accordingly.
9 Keep responses empathetic and supportive. And remember, always prioritize the user's well-being and mental health.
10
11 Facts about the user and their conversation:
12 {facts_string or 'No facts about the user and their conversation'}"""
13 )
14
15 messages = [system_message] + state["messages"]
16
17 response = await llm.ainvoke(messages)
18
19 # Add the new chat turn to the Zep graph
20 messages_to_save = [
21 Message(
22 role_type="user",
23 role=state["user_name"],
24 content=state["messages"][-1].content,
25 ),
26 Message(role_type="assistant", content=response.content),
27 ]
28
29 await zep.memory.add(
30 session_id=state["session_id"],
31 messages=messages_to_save,
32 )
33
34 # Truncate the chat history to keep the state from growing unbounded
35 # In this example, we going to keep the state small for demonstration purposes
36 # We'll use Zep's Facts to maintain conversation context
37 state["messages"] = trim_messages(
38 state["messages"],
39 strategy="last",
40 token_counter=len,
41 max_tokens=3,
42 start_on="human",
43 end_on=("human", "tool"),
44 include_system=True,
45 )
46
47 logger.info(f"Messages in state: {state['messages']}")
48
49 return {"messages": [response]}

Setting up the Agent

This section sets up the Agent’s LangGraph graph:

  1. Graph Structure: It defines a graph with nodes for the agent (chatbot) and tools, connected in a loop.

  2. Conditional Logic: The should_continue function determines whether to end the graph execution or continue to the tools node based on the presence of tool calls.

  3. Memory Management: It uses a MemorySaver to maintain conversation state across turns. This is in addition to using Zep for facts.

1graph_builder = StateGraph(State)
2
3memory = MemorySaver()
4
5
6# Define the function that determines whether to continue or not
7async def should_continue(state, config):
8 messages = state['messages']
9 last_message = messages[-1]
10 # If there is no function call, then we finish
11 if not last_message.tool_calls:
12 return 'end'
13 # Otherwise if there is, we continue
14 else:
15 return 'continue'
16
17graph_builder.add_node('agent', chatbot)
18graph_builder.add_node('tools', tool_node)
19
20graph_builder.add_edge(START, 'agent')
21
22graph_builder.add_conditional_edges('agent', should_continue, {'continue': 'tools', 'end': END})
23
24graph_builder.add_edge('tools', 'agent')
25
26
27graph = graph_builder.compile(checkpointer=memory)

Our LangGraph agent graph is illustrated below.

Agent Graph

Running the Agent

We generate a unique user name and thread id (session id) and add these to Zep, associating the Session with the new User.

1user_name = 'Daniel_' + uuid.uuid4().hex[:4]
2thread_id = uuid.uuid4().hex
3
4await zep.user.add(user_id=user_name)
5await zep.memory.add_session(session_id=thread_id, user_id=user_name)
6
7def extract_messages(result):
8 output = ""
9 for message in result['messages']:
10 if isinstance(message, AIMessage):
11 role = "assistant"
12 else:
13 role = result['user_name']
14 output += f"{role}: {message.content}\n"
15 return output.strip()
16
17
18
19async def graph_invoke(
20 message: str,
21 user_name: str,
22 thread_id: str,
23 ai_response_only: bool = True
24):
25 r = await graph.ainvoke(
26 {
27 'messages': [
28 {
29 'role': 'user',
30 'content': message,
31 }
32 ],
33 'user_name': user_name,
34 'session_id': thread_id,
35 },
36 config={'configurable': {'thread_id': thread_id}},
37 )
38
39 if ai_response_only:
40 return r['messages'][-1].content
41 else:
42 return extract_messages(r)
1r = await graph_invoke(
2 "Hi there?",
3 user_name,
4 thread_id,
5)
6
7print(r)

Hello! How are you feeling today? I’m here to listen and support you.

1r = await graph_invoke(
2 """
3 I'm fine. But have been a bit stressful lately. Mostly work related.
4 But also my dog. I'm worried about her. She's been sick.
5 """,
6 user_name,
7 thread_id,
8)
9
10print(r)

I’m sorry to hear that you’ve been feeling stressed. Work can be a significant source of pressure, and it sounds like your dog might be adding to that stress as well. If you feel comfortable sharing, what specifically has been causing you stress at work and with your dog? I’m here to help you through it.

1r = await graph_invoke(
2 "She's sick. I'm worried about her.",
3 user_name,
4 thread_id,
5)
6
7print(r)

I’m really sorry to hear that your dog is sick. It’s completely understandable to feel worried about her; our pets are like family. Have you been able to take her to the vet? Sometimes getting a professional opinion can help ease some of that worry. I’m here to support you through this.

Viewing All Conversation Facts

1facts = await zep.memory.get_session_facts(session_id=thread_id)
2
3print("\n".join([f"Fact: {fact.fact}" for fact in facts.facts]))

Fact: Daniel’s dog is also contributing to his stress.

Fact: Daniel’s stress is mostly work-related.

Fact: Daniel has been feeling stressed lately.

1r = await graph_invoke(
2 "She ate my shoes which were expensive.",
3 user_name,
4 thread_id,
5)
6
7print(r)

That sounds really frustrating, especially on top of everything else you’re dealing with. It’s tough when our pets do things that add to our stress, especially when it involves something valuable. It’s okay to feel upset about it. How are you managing with everything?

Let’s now test whether the Agent is correctly grounded with facts from the prior conversation.

1r = await graph_invoke(
2 "What are we talking about?",
3 user_name,
4 thread_id,
5)
6
7print(r)

We were discussing the stress you’ve been feeling lately, particularly related to work and your dog’s health. You mentioned that your dog has been sick and also chewed up your expensive shoes, which can be really frustrating. If there’s something else on your mind or if you’d like to talk about a different topic, I’m here to listen!

Let’s go even further back to determine whether context is kept by referencing a user message that is not currently in the Agent State. Zep will retrieve Facts related to the user’s job.

1r = await graph_invoke(
2 "What have I said about my job?",
3 user_name,
4 thread_id,
5)
6
7print(r)

You’ve mentioned that you’ve been feeling stressed lately, and it seems that your job is a significant part of that stress. If you’d like to share more about what’s been going on at work, I’m here to listen and help however I can.