LangGraph (3/4): long-term memory and human-in-the-loop

LangGraph (3/4): long-term memory and human-in-the-loop

In the previous parts, we created a chatbot with tools (Part 1) and short-term memory within a thread (Part 2). Now we take two more steps: **long-term memory** (across threads) to remember information between conversations, and the **human-in-the-loop** pattern to pause the graph and request human approval.

⚠️ This chapter continues the code from the previous parts (Parte 1 · Parte 2).

Disclaimer: This post has been translated to English using a machine translation model. Please, let me know if you find any mistakes.

📚 **This entry is part of the _Complete LangGraph Guide_ series**, divided into four chapters that are read in order:

> * Part 1: Basic chatbot and tools

* Part 2: Short-term memory

* 👉 **Part 3: Long-term memory and human-in-the-loop**

* Part 4: State customization and checkpoints

Long-term memory, memory between threadslink image 15

Memory is a cognitive function that allows people to store, retrieve, and use information to understand, based on their past, their present, and their future.

There are several types of memory long-term memory that can be used in AI applications.

Introduction to LangGraph Memory Storelink image 16

LangGraph provides the LangGraph Memory Store, which is a way to save and retrieve long-term memory across different threads. In this way, in one conversation, a user can indicate that they like something, and in another conversation, the chatbot can retrieve that information to generate a more personalized response.

This is a class for persistent key-value (key-value) stores.

When objects are stored in memory, three things are needed:

  • A namespace for the object is created using a tuple
  • A unique key
  • The value of the object

Let's see an example

	
< > Input
Python
import uuid
from langgraph.store.memory import InMemoryStore
in_memory_store = InMemoryStore()
# Namespace for the memory to save
user_id = "1"
namespace_for_memory = (user_id, "memories")
# Save a memory to namespace as key and value
key = str(uuid.uuid4())
# The value needs to be a dictionary
value = {"food_preference" : "I like pizza"}
# Save the memory
in_memory_store.put(namespace_for_memory, key, value)
Copied

The in_memory_store object we have created has several methods, and one of them is search, which allows us to search by namespace

	
< > Input
Python
# Search
memories = in_memory_store.search(namespace_for_memory)
type(memories), len(memories)
Copied
>_ Output
			
(list, 1)

It is a list with a single value, which makes sense, because we have only stored one value, so let’s take a look at it.

	
< > Input
Python
value = memories[0]
value.dict()
Copied
>_ Output
			
{'namespace': ['1', 'memories'],
'key': '70006131-948a-4d7a-bdce-78351c44fc4d',
'value': {'food_preference': 'I like pizza'},
'created_at': '2025-05-11T07:24:31.462465+00:00',
'updated_at': '2025-05-11T07:24:31.462468+00:00',
'score': None}

We can see its key and its value

	
< > Input
Python
# The key, value
memories[0].key, memories[0].value
Copied
>_ Output
			
('70006131-948a-4d7a-bdce-78351c44fc4d', {'food_preference': 'I like pizza'})

We can also use the get method to obtain an object from memory based on its namespace and key

	
< > Input
Python
# Get the memory by namespace and key
memory = in_memory_store.get(namespace_for_memory, key)
memory.dict()
Copied
>_ Output
			
{'namespace': ['1', 'memories'],
'key': '70006131-948a-4d7a-bdce-78351c44fc4d',
'value': {'food_preference': 'I like pizza'},
'created_at': '2025-05-11T07:24:31.462465+00:00',
'updated_at': '2025-05-11T07:24:31.462468+00:00'}

Just as we used checkpoints for short-term memory, for long-term memory we are going to use LangGraph Store

Chatbot with long-term memorylink image 17

We created a basic chatbot, with long-term memory and short-term memory.

	
< > Input
Python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.graph.message import add_messages
from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langgraph.checkpoint.memory import MemorySaver # Short-term memory
from langgraph.store.base import BaseStore # Long-term memory
from langchain_core.runnables.config import RunnableConfig
from langgraph.store.memory import InMemoryStore
from huggingface_hub import login
from IPython.display import Image, display
import os
import dotenv
dotenv.load_dotenv()
HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_LANGGRAPH")
class State(TypedDict):
messages: Annotated[list, add_messages]
os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing
# Create the LLM model
login(token=HUGGINGFACE_TOKEN) # Login to HuggingFace to use the model
MODEL = "Qwen/Qwen2.5-72B-Instruct"
model = HuggingFaceEndpoint(
repo_id=MODEL,
task="text-generation",
max_new_tokens=512,
do_sample=False,
repetition_penalty=1.03,
)
# Create the chat model
llm = ChatHuggingFace(llm=model)
# Chatbot instruction
MODEL_SYSTEM_MESSAGE = """You are a helpful assistant that can answer questions and help with tasks.
You have access to a long-term memory that you can use to answer questions and help with tasks.
Here is the memory (it may be empty): {memory}"""
# Create new memory from the chat history and any existing memory
CREATE_MEMORY_INSTRUCTION = """You are a helpful assistant that gets information from the user to personalize your responses.
# INFORMATION FROM THE USER:
{memory}
# INSTRUCTIONS:
1. Carefully review the chat history
2. Identify new information from the user, such as:
- Personal details (name, location)
- Preferences (likes, dislikes)
- Interests and hobbies
- Past experiences
- Goals or future plans
3. Combine any new information with the existing memory
4. Format the memory as a clear, bulleted list
5. If new information conflicts with existing memory, keep the most recent version
Remember: Only include factual information directly stated by the user. Do not make assumptions or inferences.
Based on the chat history below, please update the user information:"""
# Nodes
def call_model(state: MessagesState, config: RunnableConfig, store: BaseStore):
"""Load memory from the store and use it to personalize the chatbot's response."""
# Get the user ID from the config
user_id = config["configurable"]["user_id"]
# Retrieve memory from the store
namespace = ("memory", user_id)
key = "user_memory"
existing_memory = store.get(namespace, key)
# Extract the actual memory content if it exists and add a prefix
if existing_memory:
# Value is a dictionary with a memory key
existing_memory_content = existing_memory.value.get('memory')
else:
existing_memory_content = "No existing memory found."
if isinstance(existing_memory_content, str):
print(f" [Call model debug] Existing memory: {existing_memory_content}")
else:
print(f" [Call model debug] Existing memory: {existing_memory_content.content}")
# Format the memory in the system prompt
system_msg = MODEL_SYSTEM_MESSAGE.format(memory=existing_memory_content)
# Respond using memory as well as the chat history
response = llm.invoke([SystemMessage(content=system_msg)]+state["messages"])
return {"messages": response}
def write_memory(state: MessagesState, config: RunnableConfig, store: BaseStore):
"""Reflect on the chat history and save a memory to the store."""
# Get the user ID from the config
user_id = config["configurable"]["user_id"]
# Retrieve existing memory from the store
namespace = ("memory", user_id)
existing_memory = store.get(namespace, "user_memory")
# Extract the memory
if existing_memory:
existing_memory_content = existing_memory.value.get('memory')
else:
existing_memory_content = "No existing memory found."
if isinstance(existing_memory_content, str):
print(f" [Write memory debug] Existing memory: {existing_memory_content}")
else:
print(f" [Write memory debug] Existing memory: {existing_memory_content.content}")
# Format the memory in the system prompt
system_msg = CREATE_MEMORY_INSTRUCTION.format(memory=existing_memory_content)
new_memory = llm.invoke([SystemMessage(content=system_msg)]+state['messages'])
if isinstance(new_memory, str):
print(f" [Write memory debug] New memory: {new_memory}")
else:
print(f" [Write memory debug] New memory: {new_memory.content}")
# Overwrite the existing memory in the store
key = "user_memory"
# Write value as a dictionary with a memory key
store.put(namespace, key, {"memory": new_memory.content})
# Create graph builder
graph_builder = StateGraph(State)
# Add nodes
graph_builder.add_node("call_model", call_model)
graph_builder.add_node("write_memory", write_memory)
# Connect nodes
graph_builder.add_edge(START, "call_model")
graph_builder.add_edge("call_model", "write_memory")
graph_builder.add_edge("write_memory", END)
# Store for long-term (across-thread) memory
long_term_memory = InMemoryStore()
# Checkpointer for short-term (within-thread) memory
short_term_memory = MemorySaver()
# Compile the graph
graph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)
display(Image(graph.get_graph().draw_mermaid_png()))
Copied
>_ Output
			
&lt;IPython.core.display.Image object&gt;

Let's try it

	
< > Input
Python
# We supply a thread ID for short-term (within-thread) memory
# We supply a user ID for long-term (across-thread) memory
config = {"configurable": {"thread_id": "1", "user_id": "1"}}
# User input
input_messages = [HumanMessage(content="Hi, my name is Maximo")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
Hi, my name is Maximo
[Call model debug] Existing memory: No existing memory found.
================================== Ai Message ==================================
Hello Maximo! It's nice to meet you. How can I assist you today?
[Write memory debug] Existing memory: No existing memory found.
[Write memory debug] New memory:
Here's the updated information I have about you:
- Name: Maximo
	
< > Input
Python
# User input
input_messages = [HumanMessage(content="I like to bike around San Francisco")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
I like to bike around San Francisco
[Call model debug] Existing memory:
Here's the updated information I have about you:
- Name: Maximo
================================== Ai Message ==================================
That sounds like a great way to explore the city! San Francisco has some fantastic biking routes. Are there any specific areas or routes you enjoy biking the most, or are you looking for some new recommendations?
[Write memory debug] Existing memory:
Here's the updated information I have about you:
- Name: Maximo
[Write memory debug] New memory:
Here's the updated information about you:
- Name: Maximo
- Location: San Francisco
- Interest: Biking around San Francisco

If we recover long-term memory

	
< > Input
Python
# Namespace for the memory to save
user_id = "1"
namespace = ("memory", user_id)
existing_memory = long_term_memory.get(namespace, "user_memory")
existing_memory.dict()
Copied
>_ Output
			
{'namespace': ['memory', '1'],
'key': 'user_memory',
'value': {'memory': " Here's the updated information about you: - Name: Maximo - Location: San Francisco - Interest: Biking around San Francisco"},
'created_at': '2025-05-11T09:41:26.739207+00:00',
'updated_at': '2025-05-11T09:41:26.739211+00:00'}

We obtain its value

	
< > Input
Python
print(existing_memory.value.get('memory'))
Copied
>_ Output
			
Here's the updated information about you:
- Name: Maximo
- Location: San Francisco
- Interest: Biking around San Francisco

Now we can start a new conversation thread, but with the same long-term memory. We will see that the chatbot remembers the user information.

	
< > Input
Python
# We supply a user ID for across-thread memory as well as a new thread ID
config = {"configurable": {"thread_id": "2", "user_id": "1"}}
# User input
input_messages = [HumanMessage(content="Hi! Where would you recommend that I go biking?")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
Hi! Where would you recommend that I go biking?
[Call model debug] Existing memory:
Here's the updated information about you:
- Name: Maximo
- Location: San Francisco
- Interest: Biking around San Francisco
================================== Ai Message ==================================
Hi there! Given my interest in biking around San Francisco, I'd recommend a few great routes:
1. **Golden Gate Park**: This is a fantastic place to bike, with wide paths that are separated from vehicle traffic. You can start at the eastern end near Stow Lake and bike all the way to the western end at Ocean Beach. There are plenty of scenic spots to stop and enjoy along the way.
2. **The Embarcadero**: This route follows the waterfront from Fisherman’s Wharf to the Bay Bridge. It’s relatively flat and offers beautiful views of the San Francisco Bay and the city skyline. You can also stop by the Ferry Building for some delicious food and drinks.
3. **Presidio**: The Presidio is a large park with numerous trails that offer diverse landscapes, from forests to coastal bluffs. The Crissy Field area is especially popular for its views of the Golden Gate Bridge.
4. **Golden Gate Bridge**: Riding across the Golden Gate Bridge is a must-do experience. You can start from the San Francisco side, bike across the bridge, and then continue into Marin County for a longer ride with stunning views.
5. **Lombard Street**: While not a long ride, biking down the famous crooked section of Lombard Street can be a fun and memorable experience. Just be prepared for the steep hill on the way back up!
Each of these routes offers a unique experience, so you can choose based on your interests and the type of scenery you enjoy. Happy biking!
[Write memory debug] Existing memory:
Here's the updated information about you:
- Name: Maximo
- Location: San Francisco
- Interest: Biking around San Francisco
[Write memory debug] New memory: 😊
Let me know if you have any other questions or if you need more recommendations!

I opened a new conversation thread, asked where I could go cycling, and it remembered that I had told it I like to go cycling around San Francisco. It replied with places in San Francisco that I could go to.

Chatbot with user profilelink image 18

Note: In this section, we will use Sonnet 3.7, since the HuggingFace integration does not have the with_structured_output functionality that provides structured output with a defined structure.

We can create types so that the LLM generates output with a structure defined by us.

Let's create a type for the user profile.

	
< > Input
Python
from typing import TypedDict, List
class UserProfile(TypedDict):
"""User profile schema with typed fields"""
user_name: str # The user's preferred name
interests: List[str] # A list of the user's interests
Copied

Now we go back to creating the graph, but this time with the UserProfile typing.

We are going to use with_structured_output so that the LLM generates output with a structure defined by us; we will define that structure with the Subjects class, which is a BaseModel-type class from Pydantic.

	
< > Input
Python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.graph.message import add_messages
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langgraph.checkpoint.memory import MemorySaver # Short-term memory
from langgraph.store.base import BaseStore # Long-term memory
from langchain_core.runnables.config import RunnableConfig
from langgraph.store.memory import InMemoryStore
from IPython.display import Image, display
from pydantic import BaseModel, Field
import os
import dotenv
dotenv.load_dotenv()
ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")
class State(TypedDict):
messages: Annotated[list, add_messages]
os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing
# Create the LLM model
llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)
llm_with_structured_output = llm.with_structured_output(UserProfile)
# Chatbot instruction
MODEL_SYSTEM_MESSAGE = """You are a helpful assistant with memory that provides information about the user.
If you have memory for this user, use it to personalize your responses.
Here is the memory (it may be empty): {memory}"""
# Create new memory from the chat history and any existing memory
CREATE_MEMORY_INSTRUCTION = """Create or update a user profile memory based on the user's chat history.
This will be saved for long-term memory. If there is an existing memory, simply update it.
Here is the existing memory (it may be empty): {memory}"""
# Nodes
def call_model(state: MessagesState, config: RunnableConfig, store: BaseStore):
"""Load memory from the store and use it to personalize the chatbot's response."""
# Get the user ID from the config
user_id = config["configurable"]["user_id"]
# Retrieve memory from the store
namespace = ("memory", user_id)
existing_memory = store.get(namespace, "user_memory")
# Format the memories for the system prompt
if existing_memory and existing_memory.value:
memory_dict = existing_memory.value
formatted_memory = (
f"Name: {memory_dict.get('user_name', 'Unknown')} "
f"Interests: {', '.join(memory_dict.get('interests', []))}"
)
else:
formatted_memory = None
# if isinstance(existing_memory_content, str):
print(f" [Call model debug] Existing memory: {formatted_memory}")
# else:
# print(f" [Call model debug] Existing memory: {existing_memory_content.content}")
# Format the memory in the system prompt
system_msg = MODEL_SYSTEM_MESSAGE.format(memory=formatted_memory)
# Respond using memory as well as the chat history
response = llm.invoke([SystemMessage(content=system_msg)]+state["messages"])
return {"messages": response}
def write_memory(state: MessagesState, config: RunnableConfig, store: BaseStore):
"""Reflect on the chat history and save a memory to the store."""
# Get the user ID from the config
user_id = config["configurable"]["user_id"]
# Retrieve existing memory from the store
namespace = ("memory", user_id)
existing_memory = store.get(namespace, "user_memory")
# Format the memories for the system prompt
if existing_memory and existing_memory.value:
memory_dict = existing_memory.value
formatted_memory = (
f"Name: {memory_dict.get('user_name', 'Unknown')} "
f"Interests: {', '.join(memory_dict.get('interests', []))}"
)
else:
formatted_memory = None
print(f" [Write memory debug] Existing memory: {formatted_memory}")
# Format the existing memory in the instruction
system_msg = CREATE_MEMORY_INSTRUCTION.format(memory=formatted_memory)
# Invoke the model to produce structured output that matches the schema
new_memory = llm_with_structured_output.invoke([SystemMessage(content=system_msg)]+state['messages'])
print(f" [Write memory debug] New memory: {new_memory}")
# Overwrite the existing use profile memory
key = "user_memory"
store.put(namespace, key, new_memory)
# Create graph builder
graph_builder = StateGraph(MessagesState)
# Add nodes
graph_builder.add_node("call_model", call_model)
graph_builder.add_node("write_memory", write_memory)
# Connect nodes
graph_builder.add_edge(START, "call_model")
graph_builder.add_edge("call_model", "write_memory")
graph_builder.add_edge("write_memory", END)
# Store for long-term (across-thread) memory
long_term_memory = InMemoryStore()
# Checkpointer for short-term (within-thread) memory
short_term_memory = MemorySaver()
# Compile the graph
graph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)
display(Image(graph.get_graph().draw_mermaid_png()))
Copied
>_ Output
			
&lt;IPython.core.display.Image object&gt;

We run the graph

	
< > Input
Python
# We supply a thread ID for short-term (within-thread) memory
# We supply a user ID for long-term (across-thread) memory
config = {"configurable": {"thread_id": "1", "user_id": "1"}}
# User input
input_messages = [HumanMessage(content="Hi, my name is Maximo and I like to bike around Madrid and eat salads.")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
Hi, my name is Maximo and I like to bike around Madrid and eat salads.
[Call model debug] Existing memory: None
================================== Ai Message ==================================
Hello Maximo! It's nice to meet you. I see you enjoy biking around Madrid and eating salads - those are great healthy habits! Madrid has some beautiful areas to explore by bike, and the city has been improving its cycling infrastructure in recent years.
Is there anything specific about Madrid's cycling routes or perhaps some good places to find delicious salads in the city that you'd like to know more about? I'd be happy to help with any questions you might have.
[Write memory debug] Existing memory: None
[Write memory debug] New memory: {'user_name': 'Maximo', 'interests': ['biking', 'Madrid', 'salads']}

As we can see, the LLM has generated output with the structure defined by us.

Let's see how long-term memory has been stored.

	
< > Input
Python
# Namespace for the memory to save
user_id = "1"
namespace = ("memory", user_id)
existing_memory = long_term_memory.get(namespace, "user_memory")
existing_memory.value
Copied
>_ Output
			
{'user_name': 'Maximo', 'interests': ['biking', 'Madrid', 'salads']}

Morelink image 19

Update structured schemas with Trustcalllink image 20

In the previous example, we created user profiles with structured data

In reality, what is done behind the scenes is to regenerate the user profile on each interaction. This creates an unnecessary token expense and can cause important information from the user profile to be lost.

So to solve it, we are going to use the TrustCall library, which is an open source library for updating JSON schemas. When it has to update a JSON schema, it does so incrementally, meaning it does not delete the previous schema, but instead adds the new fields.

Let's create a conversation example to see how it works.

	
< > Input
Python
from langchain_core.messages import HumanMessage, AIMessage
# Conversation
conversation = [HumanMessage(content="Hi, I'm Maximo."),
AIMessage(content="Nice to meet you, Maximo."),
HumanMessage(content="I really like playing soccer.")]
Copied

We create a structured schema and an LLM model

	
< > Input
Python
from pydantic import BaseModel, Field
from typing import List
# Schema
class UserProfile(BaseModel):
"""User profile schema with typed fields"""
user_name: str = Field(description="The user's preferred name")
interests: List[str] = Field(description="A list of the user's interests")
from langchain_anthropic import ChatAnthropic
import os
import dotenv
dotenv.load_dotenv()
ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")
os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing
# Create the LLM model
llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)
Copied

We use the create_extractor function from trustcall to create a structured data extractor

	
< > Input
Python
from trustcall import create_extractor
# Create the extractor
trustcall_extractor = create_extractor(
llm,
tools=[UserProfile],
tool_choice="UserProfile"
)
Copied

As can be seen, the trustcall_extractor method is given an LLM, which will be used as the search engine

We extracted the structured data

	
< > Input
Python
from langchain_core.messages import SystemMessage
# Instruction
system_msg = "Extract the user profile from the following conversation"
# Invoke the extractor
result = trustcall_extractor.invoke({"messages": [SystemMessage(content=system_msg)]+conversation})
result
Copied
>_ Output
			
{'messages': [AIMessage(content=[{'id': 'toolu_01WfgbD1fG3rJYAXGrjqjfVY', 'input': {'user_name': 'Maximo', 'interests': ['soccer']}, 'name': 'UserProfile', 'type': 'tool_use'}], additional_kwargs={}, response_metadata={'id': 'msg_01TEB3FeDKLAeHJtbKo5noyW', 'model': 'claude-3-7-sonnet-20250219', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 497, 'output_tokens': 56}, 'model_name': 'claude-3-7-sonnet-20250219'}, id='run-8a15289b-fd39-4a2d-878a-fa6feaa805c5-0', tool_calls=[{'name': 'UserProfile', 'args': {'user_name': 'Maximo', 'interests': ['soccer']}, 'id': 'toolu_01WfgbD1fG3rJYAXGrjqjfVY', 'type': 'tool_call'}], usage_metadata={'input_tokens': 497, 'output_tokens': 56, 'total_tokens': 553, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}})],
'responses': [UserProfile(user_name='Maximo', interests=['soccer'])],
'response_metadata': [{'id': 'toolu_01WfgbD1fG3rJYAXGrjqjfVY'}],
'attempts': 1}

Let's see the messages that have been generated to extract the structured data

	
< > Input
Python
for m in result["messages"]:
m.pretty_print()
Copied
>_ Output
			
================================== Ai Message ==================================
[{'id': 'toolu_01WfgbD1fG3rJYAXGrjqjfVY', 'input': {'user_name': 'Maximo', 'interests': ['soccer']}, 'name': 'UserProfile', 'type': 'tool_use'}]
Tool Calls:
UserProfile (toolu_01WfgbD1fG3rJYAXGrjqjfVY)
Call ID: toolu_01WfgbD1fG3rJYAXGrjqjfVY
Args:
user_name: Maximo
interests: ['soccer']

The UserProfile schema has been updated with the new field.

	
< > Input
Python
schema = result["responses"]
schema
Copied
>_ Output
			
[UserProfile(user_name='Maximo', interests=['soccer'])]

As we can see, the schema is a list; let’s look at the data type of its only element.

	
< > Input
Python
type(schema[0])
Copied
>_ Output
			
__main__.UserProfile

Can we convert it to a dictionary with model_dump

	
< > Input
Python
schema[0].model_dump()
Copied
>_ Output
			
{'user_name': 'Maximo', 'interests': ['soccer']}

Thanks to giving an LLM to trustcall_extractor, we can ask it what we want it to extract

Let's simulate that the conversation continues to see how the schema is updated

	
< > Input
Python
# Update the conversation
updated_conversation = [HumanMessage(content="Hi, I'm Maximo."),
AIMessage(content="Nice to meet you, Maximo."),
HumanMessage(content="I really like playing soccer."),
AIMessage(content="It is great to play soccer! Where do you go after playing soccer?"),
HumanMessage(content="I really like to go to a bakery after playing soccer."),]
Copied

We ask the model to update the schema (a JSON) using the trustcall library

	
< > Input
Python
# Update the instruction
system_msg = f"""Update the memory (JSON doc) to incorporate new information from the following conversation"""
# Invoke the extractor with the updated instruction and existing profile with the corresponding tool name (UserProfile)
result = trustcall_extractor.invoke({"messages": [SystemMessage(content=system_msg)]+updated_conversation},
{"existing": {"UserProfile": schema[0].model_dump()}})
result
Copied
>_ Output
			
{'messages': [AIMessage(content=[{'id': 'toolu_01K1zTh33kXDAw1h18Yh2HBb', 'input': {'user_name': 'Maximo', 'interests': ['soccer', 'bakeries']}, 'name': 'UserProfile', 'type': 'tool_use'}], additional_kwargs={}, response_metadata={'id': 'msg_01RYUJvCdzL4b8kBYKo4BtQf', 'model': 'claude-3-7-sonnet-20250219', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 538, 'output_tokens': 60}, 'model_name': 'claude-3-7-sonnet-20250219'}, id='run-06994472-5ba0-46cc-a512-5fcacce283fc-0', tool_calls=[{'name': 'UserProfile', 'args': {'user_name': 'Maximo', 'interests': ['soccer', 'bakeries']}, 'id': 'toolu_01K1zTh33kXDAw1h18Yh2HBb', 'type': 'tool_call'}], usage_metadata={'input_tokens': 538, 'output_tokens': 60, 'total_tokens': 598, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}})],
'responses': [UserProfile(user_name='Maximo', interests=['soccer', 'bakeries'])],
'response_metadata': [{'id': 'toolu_01K1zTh33kXDAw1h18Yh2HBb'}],
'attempts': 1}

Let's see the messages that have been generated to update the schema

	
< > Input
Python
for m in result["messages"]:
m.pretty_print()
Copied
>_ Output
			
================================== Ai Message ==================================
[{'id': 'toolu_01K1zTh33kXDAw1h18Yh2HBb', 'input': {'user_name': 'Maximo', 'interests': ['soccer', 'bakeries']}, 'name': 'UserProfile', 'type': 'tool_use'}]
Tool Calls:
UserProfile (toolu_01K1zTh33kXDAw1h18Yh2HBb)
Call ID: toolu_01K1zTh33kXDAw1h18Yh2HBb
Args:
user_name: Maximo
interests: ['soccer', 'bakeries']

We see the updated diagram

	
< > Input
Python
updated_schema = result["responses"][0]
updated_schema.model_dump()
Copied
>_ Output
			
{'user_name': 'Maximo', 'interests': ['soccer', 'bakeries']}

Chatbot with user profile updated with Trustcalllink image 21

We recreate the graph that updates the user profile, but now with the trustcall library

	
< > Input
Python
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.graph.message import add_messages
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langgraph.checkpoint.memory import MemorySaver # Short-term memory
from langgraph.store.base import BaseStore # Long-term memory
from langchain_core.runnables.config import RunnableConfig
from langgraph.store.memory import InMemoryStore
from IPython.display import Image, display
from pydantic import BaseModel, Field
import os
import dotenv
from trustcall import create_extractor
dotenv.load_dotenv()
ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")
os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing
# Schema
class UserProfile(BaseModel):
""" Profile of a user """
user_name: str = Field(description="The user's preferred name")
user_location: str = Field(description="The user's location")
interests: list = Field(description="A list of the user's interests")
# Create the LLM model
llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)
# Create the extractor
trustcall_extractor = create_extractor(
llm,
tools=[UserProfile],
tool_choice="UserProfile", # Enforces use of the UserProfile tool
)
# Chatbot instruction
MODEL_SYSTEM_MESSAGE = """You are a helpful assistant with memory that provides information about the user.
If you have memory for this user, use it to personalize your responses.
Here is the memory (it may be empty): {memory}"""
# Create new memory from the chat history and any existing memory
TRUSTCALL_INSTRUCTION = """Create or update the memory (JSON doc) to incorporate information from the following conversation:"""
# Nodes
def call_model(state: MessagesState, config: RunnableConfig, store: BaseStore):
"""Load memory from the store and use it to personalize the chatbot's response."""
"""Load memory from the store and use it to personalize the chatbot's response."""
# Get the user ID from the config
user_id = config["configurable"]["user_id"]
# Retrieve memory from the store
namespace = ("memory", user_id)
existing_memory = store.get(namespace, "user_memory")
# Format the memories for the system prompt
if existing_memory and existing_memory.value:
memory_dict = existing_memory.value
formatted_memory = (
f"Name: {memory_dict.get('user_name', 'Unknown')} "
f"Location: {memory_dict.get('user_location', 'Unknown')} "
f"Interests: {', '.join(memory_dict.get('interests', []))}"
)
else:
formatted_memory = None
print(f" [Call model debug] Existing memory: {formatted_memory}")
# Format the memory in the system prompt
system_msg = MODEL_SYSTEM_MESSAGE.format(memory=formatted_memory)
# Respond using memory as well as the chat history
response = llm.invoke([SystemMessage(content=system_msg)]+state["messages"])
return {"messages": response}
def write_memory(state: MessagesState, config: RunnableConfig, store: BaseStore):
"""Reflect on the chat history and save a memory to the store."""
# Get the user ID from the config
user_id = config["configurable"]["user_id"]
# Retrieve existing memory from the store
namespace = ("memory", user_id)
existing_memory = store.get(namespace, "user_memory")
# Get the profile as the value from the list, and convert it to a JSON doc
existing_profile = {"UserProfile": existing_memory.value} if existing_memory else None
print(f" [Write memory debug] Existing profile: {existing_profile}")
# Invoke the extractor
result = trustcall_extractor.invoke({"messages": [SystemMessage(content=TRUSTCALL_INSTRUCTION)]+state["messages"], "existing": existing_profile})
# Get the updated profile as a JSON object
updated_profile = result["responses"][0].model_dump()
print(f" [Write memory debug] Updated profile: {updated_profile}")
# Save the updated profile
key = "user_memory"
store.put(namespace, key, updated_profile)
# Create graph builder
graph_builder = StateGraph(MessagesState)
# Add nodes
graph_builder.add_node("call_model", call_model)
graph_builder.add_node("write_memory", write_memory)
# Connect nodes
graph_builder.add_edge(START, "call_model")
graph_builder.add_edge("call_model", "write_memory")
graph_builder.add_edge("write_memory", END)
# Store for long-term (across-thread) memory
long_term_memory = InMemoryStore()
# Checkpointer for short-term (within-thread) memory
short_term_memory = MemorySaver()
# Compile the graph
graph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)
display(Image(graph.get_graph().draw_mermaid_png()))
Copied
>_ Output
			
&lt;IPython.core.display.Image object&gt;

We start the conversation

	
< > Input
Python
# We supply a thread ID for short-term (within-thread) memory
# We supply a user ID for long-term (across-thread) memory
config = {"configurable": {"thread_id": "1", "user_id": "1"}}
# User input
input_messages = [HumanMessage(content="Hi, my name is Maximo")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
Hi, my name is Maximo
[Call model debug] Existing memory: None
================================== Ai Message ==================================
Hello Maximo! It's nice to meet you. How can I help you today? Whether you have questions, need information, or just want to chat, I'm here to assist you. Is there something specific you'd like to talk about?
[Write memory debug] Existing profile: None
[Write memory debug] Updated profile: {'user_name': 'Maximo', 'user_location': '&lt;UNKNOWN&gt;', 'interests': []}

As we can see, it does not know either the user's location or interests. Let's update the user's profile.

	
< > Input
Python
# User input
input_messages = [HumanMessage(content="I like to play soccer and I live in Madrid")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
I like to play soccer and I live in Madrid
[Call model debug] Existing memory: Name: Maximo
Location: &lt;UNKNOWN&gt;
Interests:
================================== Ai Message ==================================
Hello Maximo! It's great to learn that you live in Madrid and enjoy playing soccer. Madrid is a fantastic city with a rich soccer culture, being home to world-famous clubs like Real Madrid and Atlético Madrid.
Soccer is truly a way of life in Spain, so you're in a perfect location for your interest. Do you support any particular team in Madrid? Or perhaps you enjoy playing soccer recreationally in the city's parks and facilities?
Is there anything specific about Madrid or soccer you'd like to discuss further?
[Write memory debug] Existing profile: {'UserProfile': {'user_name': 'Maximo', 'user_location': '&lt;UNKNOWN&gt;', 'interests': []}}
[Write memory debug] Updated profile: {'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer']}

Updated the profile with the user's location and interests

Let's see the updated memory

	
< > Input
Python
# Namespace for the memory to save
user_id = "1"
namespace = ("memory", user_id)
existing_memory = long_term_memory.get(namespace, "user_memory")
existing_memory.dict()
Copied
>_ Output
			
{'namespace': ['memory', '1'],
'key': 'user_memory',
'value': {'user_name': 'Maximo',
'user_location': 'Madrid',
'interests': ['soccer']},
'created_at': '2025-05-12T17:35:03.583258+00:00',
'updated_at': '2025-05-12T17:35:03.583259+00:00'}

We see the diagram with the updated user profile

	
< > Input
Python
# The user profile saved as a JSON object
existing_memory.value
Copied
>_ Output
			
{'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer']}

Let's add a new user interest

	
< > Input
Python
# User input
input_messages = [HumanMessage(content="I also like to play basketball")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
I also like to play basketball
[Call model debug] Existing memory: Name: Maximo
Location: Madrid
Interests: soccer
================================== Ai Message ==================================
That's great to know, Maximo! It's nice that you enjoy both soccer and basketball. Basketball is also quite popular in Spain, with Liga ACB being one of the strongest basketball leagues in Europe.
In Madrid, you have the opportunity to follow Real Madrid's basketball section, which is one of the most successful basketball teams in Europe. The city offers plenty of courts and facilities where you can play basketball too.
Do you play basketball casually with friends, or are you part of any local leagues in Madrid? And how do you balance your time between soccer and basketball?
[Write memory debug] Existing profile: {'UserProfile': {'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer']}}
[Write memory debug] Updated profile: {'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer', 'basketball']}

We see the updated memory again

	
< > Input
Python
# Namespace for the memory to save
user_id = "1"
namespace = ("memory", user_id)
existing_memory = long_term_memory.get(namespace, "user_memory")
existing_memory.value
Copied
>_ Output
			
{'user_name': 'Maximo',
'user_location': 'Madrid',
'interests': ['soccer', 'basketball']}

The new user interest has been added correctly.

With this long-term memory saved, we can start a new thread, and the chatbot will have access to our updated profile.

	
< > Input
Python
# We supply a thread ID for short-term (within-thread) memory
# We supply a user ID for long-term (across-thread) memory
config = {"configurable": {"thread_id": "2", "user_id": "1"}}
# User input
input_messages = [HumanMessage(content="What soccer players do you recommend for me?")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
What soccer players do you recommend for me?
[Call model debug] Existing memory: Name: Maximo
Location: Madrid
Interests: soccer, basketball
================================== Ai Message ==================================
Based on your interest in soccer, I can recommend some players who might appeal to you. Since you're from Madrid, you might already follow Real Madrid or Atlético Madrid players, but here are some recommendations:
From La Liga:
- Vinícius Júnior and Jude Bellingham (Real Madrid)
- Antoine Griezmann (Atlético Madrid)
- Robert Lewandowski (Barcelona)
- Lamine Yamal (Barcelona's young talent)
International stars:
- Kylian Mbappé
- Erling Haaland
- Mohamed Salah
- Kevin De Bruyne
You might also enjoy watching players with creative playing styles since you're interested in basketball as well, which is a sport that values creativity and flair - players like Rodrigo De Paul or João Félix.
Is there a particular league or playing style you prefer in soccer?
[Write memory debug] Existing profile: {'UserProfile': {'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer', 'basketball']}}
[Write memory debug] Updated profile: {'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer', 'basketball']}

Since it knows I live in Madrid, it first suggested football players from the Spanish league. And then it suggested players from other leagues

Chatbot with User Document Collections Updated with Trustcalllink image 22

Another approach is to store a collection of documents instead of saving the user profile in a single document, so we are not tied to a single closed schema.

Let's see how to do it

	
< > Input
Python
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_core.messages import merge_message_runs
from langgraph.checkpoint.memory import MemorySaver # Short-term memory
from langgraph.store.base import BaseStore # Long-term memory
from langchain_core.runnables.config import RunnableConfig
from langgraph.store.memory import InMemoryStore
from IPython.display import Image, display
from trustcall import create_extractor
from pydantic import BaseModel, Field
import uuid
import os
import dotenv
dotenv.load_dotenv()
ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")
os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing
# Memory schema
class Memory(BaseModel):
"""A memory item representing a piece of information learned about the user."""
content: str = Field(description="The main content of the memory. For example: User expressed interest in learning about French.")
# Create the LLM model
llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)
# Create the extractor
trustcall_extractor = create_extractor(
llm,
tools=[Memory],
tool_choice="Memory",
# This allows the extractor to insert new memories
enable_inserts=True,
)
# Chatbot instruction
MODEL_SYSTEM_MESSAGE = """You are a helpful chatbot. You are designed to be a companion to a user.
You have a long term memory which keeps track of information you learn about the user over time.
Current Memory (may include updated memories from this conversation):
{memory}"""
# Create new memory from the chat history and any existing memory
TRUSTCALL_INSTRUCTION = """Reflect on following interaction.
Use the provided tools to retain any necessary memories about the user.
Use parallel tool calling to handle updates and insertions simultaneously:"""
# Nodes
def call_model(state: MessagesState, config: RunnableConfig, store: BaseStore):
"""Load memory from the store and use it to personalize the chatbot's response."""
# Get the user ID from the config
user_id = config["configurable"]["user_id"]
# Retrieve memory from the store
namespace = ("memories", user_id)
memories = store.search(namespace)
print(f" [Call model debug] Memories: {memories}")
# Format the memories for the system prompt
info = " ".join(f"- {mem.value['content']}" for mem in memories)
system_msg = MODEL_SYSTEM_MESSAGE.format(memory=info)
# Respond using memory as well as the chat history
response = llm.invoke([SystemMessage(content=system_msg)]+state["messages"])
return {"messages": response}
def write_memory(state: MessagesState, config: RunnableConfig, store: BaseStore):
"""Reflect on the chat history and save a memory to the store."""
# Get the user ID from the config
user_id = config["configurable"]["user_id"]
# Define the namespace for the memories
namespace = ("memories", user_id)
# Retrieve the most recent memories for context
existing_items = store.search(namespace)
# Format the existing memories for the Trustcall extractor
tool_name = "Memory"
existing_memories = ([(existing_item.key, tool_name, existing_item.value)
for existing_item in existing_items]
if existing_items
else None
)
print(f" [Write memory debug] Existing memories: {existing_memories}")
# Merge the chat history and the instruction
updated_messages=list(merge_message_runs(messages=[SystemMessage(content=TRUSTCALL_INSTRUCTION)] + state["messages"]))
# Invoke the extractor
result = trustcall_extractor.invoke({"messages": updated_messages,
"existing": existing_memories})
# Save the memories from Trustcall to the store
for r, rmeta in zip(result["responses"], result["response_metadata"]):
store.put(namespace,
rmeta.get("json_doc_id", str(uuid.uuid4())),
r.model_dump(mode="json"),
)
print(f" [Write memory debug] Saved memories: {result['responses']}")
# Create graph builder
graph_builder = StateGraph(MessagesState)
# Add nodes
graph_builder.add_node("call_model", call_model)
graph_builder.add_node("write_memory", write_memory)
# Connect nodes
graph_builder.add_edge(START, "call_model")
graph_builder.add_edge("call_model", "write_memory")
graph_builder.add_edge("write_memory", END)
# Store for long-term (across-thread) memory
long_term_memory = InMemoryStore()
# Checkpointer for short-term (within-thread) memory
short_term_memory = MemorySaver()
# Compile the graph
graph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)
display(Image(graph.get_graph().draw_mermaid_png()))
Copied
>_ Output
			
&lt;IPython.core.display.Image object&gt;

We are starting a new conversation

	
< > Input
Python
# We supply a thread ID for short-term (within-thread) memory
# We supply a user ID for long-term (across-thread) memory
config = {"configurable": {"thread_id": "1", "user_id": "1"}}
# User input
input_messages = [HumanMessage(content="Hi, my name is Maximo")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
Hi, my name is Maximo
[Call model debug] Memories: []
================================== Ai Message ==================================
Hello Maximo! It's nice to meet you. I'm your companion chatbot, here to chat, help answer questions, or just be someone to talk to.
I'll remember your name is Maximo for our future conversations. What would you like to talk about today? How are you doing?
[Write memory debug] Existing memories: None
[Write memory debug] Saved memories: [Memory(content="User's name is Maximo.")]

We added a new user interest

	
< > Input
Python
# User input
input_messages = [HumanMessage(content="I like to play soccer")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
I like to play soccer
[Call model debug] Memories: [Item(namespace=['memories', '1'], key='6d06c4f5-3a74-46b2-92b4-1e29ba128c90', value={'content': "User's name is Maximo."}, created_at='2025-05-12T18:32:38.070902+00:00', updated_at='2025-05-12T18:32:38.070903+00:00', score=None)]
================================== Ai Message ==================================
That's great to know, Maximo! Soccer is such a wonderful sport. Do you play on a team, or more casually with friends? I'd also be curious to know what position you typically play, or if you have a favorite professional team you follow. I'll remember that you enjoy soccer for our future conversations.
[Write memory debug] Existing memories: [('6d06c4f5-3a74-46b2-92b4-1e29ba128c90', 'Memory', {'content': "User's name is Maximo."})]
[Write memory debug] Saved memories: [Memory(content='User enjoys playing soccer.')]

As we can see, the user's new interest has been added to memory.

Let's see the updated memory

	
< > Input
Python
# Namespace for the memory to save
user_id = "1"
namespace = ("memories", user_id)
memories = long_term_memory.search(namespace)
for m in memories:
print(m.dict())
Copied
>_ Output
			
{'namespace': ['memories', '1'], 'key': '6d06c4f5-3a74-46b2-92b4-1e29ba128c90', 'value': {'content': "User's name is Maximo."}, 'created_at': '2025-05-12T18:32:38.070902+00:00', 'updated_at': '2025-05-12T18:32:38.070903+00:00', 'score': None}
{'namespace': ['memories', '1'], 'key': '25d2ee8c-5890-415b-85e0-d9fb0ea4cd43', 'value': {'content': 'User enjoys playing soccer.'}, 'created_at': '2025-05-12T18:32:42.558787+00:00', 'updated_at': '2025-05-12T18:32:42.558789+00:00', 'score': None}
	
< > Input
Python
for m in memories:
print(m.value)
Copied
>_ Output
			
{'content': "User's name is Maximo."}
{'content': 'User enjoys playing soccer.'}

We see that memory documents are saved, not a user profile.

Let's add a new user interest

	
< > Input
Python
# User input
input_messages = [HumanMessage(content="I also like to play basketball")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
I also like to play basketball
[Call model debug] Memories: [Item(namespace=['memories', '1'], key='6d06c4f5-3a74-46b2-92b4-1e29ba128c90', value={'content': "User's name is Maximo."}, created_at='2025-05-12T18:32:38.070902+00:00', updated_at='2025-05-12T18:32:38.070903+00:00', score=None), Item(namespace=['memories', '1'], key='25d2ee8c-5890-415b-85e0-d9fb0ea4cd43', value={'content': 'User enjoys playing soccer.'}, created_at='2025-05-12T18:32:42.558787+00:00', updated_at='2025-05-12T18:32:42.558789+00:00', score=None)]
================================== Ai Message ==================================
That's awesome, Maximo! Both soccer and basketball are fantastic sports. I'll remember that you enjoy basketball as well. Do you find yourself playing one more than the other? And similar to soccer, do you play basketball with a team or more casually? Many people enjoy the different skills and dynamics each sport offers - soccer with its continuous flow and footwork, and basketball with its fast pace and shooting precision. Any favorite basketball teams you follow?
[Write memory debug] Existing memories: [('6d06c4f5-3a74-46b2-92b4-1e29ba128c90', 'Memory', {'content': "User's name is Maximo."}), ('25d2ee8c-5890-415b-85e0-d9fb0ea4cd43', 'Memory', {'content': 'User enjoys playing soccer.'})]
[Write memory debug] Saved memories: [Memory(content='User enjoys playing basketball.')]

We see the updated memory again

	
< > Input
Python
# Namespace for the memory to save
user_id = "1"
namespace = ("memories", user_id)
memories = long_term_memory.search(namespace)
for m in memories:
print(m.value)
Copied
>_ Output
			
{'content': "User's name is Maximo."}
{'content': 'User enjoys playing soccer.'}
{'content': 'User enjoys playing basketball.'}

We are starting a new conversation with a new thread

	
< > Input
Python
# We supply a thread ID for short-term (within-thread) memory
# We supply a user ID for long-term (across-thread) memory
config = {"configurable": {"thread_id": "2", "user_id": "1"}}
# User input
input_messages = [HumanMessage(content="What soccer players do you recommend for me?")]
# Run the graph
for chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):
chunk["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
What soccer players do you recommend for me?
[Call model debug] Memories: [Item(namespace=['memories', '1'], key='6d06c4f5-3a74-46b2-92b4-1e29ba128c90', value={'content': "User's name is Maximo."}, created_at='2025-05-12T18:32:38.070902+00:00', updated_at='2025-05-12T18:32:38.070903+00:00', score=None), Item(namespace=['memories', '1'], key='25d2ee8c-5890-415b-85e0-d9fb0ea4cd43', value={'content': 'User enjoys playing soccer.'}, created_at='2025-05-12T18:32:42.558787+00:00', updated_at='2025-05-12T18:32:42.558789+00:00', score=None), Item(namespace=['memories', '1'], key='965f2e52-bea0-44d4-8534-4fce2bbc1c4b', value={'content': 'User enjoys playing basketball.'}, created_at='2025-05-12T18:33:38.613626+00:00', updated_at='2025-05-12T18:33:38.613629+00:00', score=None)]
================================== Ai Message ==================================
Hi Maximo! Since you enjoy soccer, I'd be happy to recommend some players you might find interesting to follow or learn from.
Based on your interests in both soccer and basketball, I might suggest players who are known for their athleticism and skill:
1. Lionel Messi - Widely considered one of the greatest players of all time
2. Cristiano Ronaldo - Known for incredible athleticism and dedication
3. Kylian Mbappé - Young talent with amazing speed and technical ability
4. Kevin De Bruyne - Master of passing and vision
5. Erling Haaland - Goal-scoring phenomenon
Is there a particular position or playing style you're most interested in? That would help me refine my recommendations further. I could also suggest players from specific leagues or teams if you have preferences!
[Write memory debug] Existing memories: [('6d06c4f5-3a74-46b2-92b4-1e29ba128c90', 'Memory', {'content': "User's name is Maximo."}), ('25d2ee8c-5890-415b-85e0-d9fb0ea4cd43', 'Memory', {'content': 'User enjoys playing soccer.'}), ('965f2e52-bea0-44d4-8534-4fce2bbc1c4b', 'Memory', {'content': 'User enjoys playing basketball.'})]
[Write memory debug] Saved memories: [Memory(content='User asked for soccer player recommendations, suggesting an active interest in following professional soccer beyond just playing it.')]

We saw that he remembered that we liked soccer and basketball.

Human in the looplink image 23

Although an agent can perform tasks, for certain tasks, human supervision is necessary. This is called human in the loop. So let’s see how this can be done with LangGraph.

The persistence layer of LangGraph supports human-in-the-loop workflows, allowing execution to pause and resume based on user feedback. The main interface for this functionality is the interrupt function. Calling interrupt within a node will stop execution. Execution can be resumed, along with the new human input, passed in a Command primitive. interrupt is similar to Python's input() command, but with some extra considerations.

We are going to add to the chatbot that it has short-term memory and access to tools, but we will make a change, which is to add a simple human_assistance tool. This tool uses interrupt to receive information from a human.

First, we load the API KEY values

	
< > Input
Python
import os
import dotenv
dotenv.load_dotenv()
HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_LANGGRAPH")
TAVILY_API_KEY = os.getenv("TAVILY_LANGGRAPH_API_KEY")
Copied

We create the graph

	
< > Input
Python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class State(TypedDict):
messages: Annotated[list, add_messages]
graph_builder = StateGraph(State)
Copied

We define the search tool

	
< > Input
Python
from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper
from langchain_community.tools.tavily_search import TavilySearchResults
wrapper = TavilySearchAPIWrapper(tavily_api_key=TAVILY_API_KEY)
search_tool = TavilySearchResults(api_wrapper=wrapper, max_results=2)
Copied

Now we create the human support tool

	
< > Input
Python
from langgraph.types import Command, interrupt
from langchain_core.tools import tool
@tool
def human_assistance(query: str) -&gt; str:
"""
Request assistance from a human expert. Use this tool ONLY ONCE per conversation.
After receiving the expert's response, you should provide an elaborated response to the user based on the information received
based on the information received, without calling this tool again.
Args:
query: The query to ask the human expert.
Returns:
The response from the human expert.
"""
human_response = interrupt({"query": query})
return human_response["data"]
Copied

LangGraph obtains information about tools through the tool's documentation, that is, the function's docstring. Therefore, it is very important to generate a good docstring for the tool.

We create a list of tools

	
< > Input
Python
tools_list = [search_tool, human_assistance]
Copied

Next, the LLM with the bind_tools and we add it to the graph

	
< > Input
Python
from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
from huggingface_hub import login
os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing
# Create the LLM
login(token=HUGGINGFACE_TOKEN)
MODEL = "Qwen/Qwen2.5-72B-Instruct"
model = HuggingFaceEndpoint(
repo_id=MODEL,
task="text-generation",
max_new_tokens=512,
do_sample=False,
repetition_penalty=1.03,
)
# Create the chat model
llm = ChatHuggingFace(llm=model)
# Modification: tell the LLM which tools it can call
llm_with_tools = llm.bind_tools(tools_list)
# Define the chatbot function
def chatbot_function(state: State):
message = llm_with_tools.invoke(state["messages"])
assert len(message.tool_calls) &lt;= 1
return {"messages": [message]}
# Add the chatbot node
graph_builder.add_node("chatbot_node", chatbot_function)
Copied
>_ Output
			
&lt;langgraph.graph.state.StateGraph at 0x10764b380&gt;

If you look closely, we have changed the way the chatbot_function is defined, since it now has to handle interruption.

We add the tool_node to the graph

	
< > Input
Python
from langgraph.prebuilt import ToolNode, tools_condition
tool_node = ToolNode(tools=tools_list)
graph_builder.add_node("tools", tool_node)
graph_builder.add_conditional_edges("chatbot_node", tools_condition)
graph_builder.add_edge("tools", "chatbot_node")
Copied
>_ Output
			
&lt;langgraph.graph.state.StateGraph at 0x10764b380&gt;

We added the START node to the graph

	
< > Input
Python
graph_builder.add_edge(START, "chatbot_node")
Copied
>_ Output
			
&lt;langgraph.graph.state.StateGraph at 0x10764b380&gt;

We create a checkpointer MemorySaver.

	
< > Input
Python
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
Copied

We compiled the graph with the checkpointer

	
< > Input
Python
graph = graph_builder.compile(checkpointer=memory)
Copied

We represent it graphically

	
< > Input
Python
from IPython.display import Image, display
try:
display(Image(graph.get_graph().draw_mermaid_png()))
except Exception as e:
print(f"Error al visualizar el grafo: {e}")
Copied
>_ Output
			
&lt;IPython.core.display.Image object&gt;

Now let’s ask the chatbot a question that will involve the new human_assistance tool:

	
< > Input
Python
user_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?"
config = {"configurable": {"thread_id": "1"}}
events = graph.stream(
{"messages": [{"role": "user", "content": user_input}]},
config,
stream_mode="values",
)
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
I need some expert guidance for building an AI agent. Could you request assistance for me?
================================== Ai Message ==================================
Tool Calls:
human_assistance (0)
Call ID: 0
Args:
query: I need some expert guidance for building an AI agent. Could you provide me with some advice?

As can be seen, the chatbot generated a call to the human assistance tool.

Tool Calls:
human assistance (0)
Call ID: 0
Args:query: I need some expert guidance for building an AI agent. Could you provide advice on key considerations, best practices, and potential pitfalls to avoid?

But then the execution was interrupted. Let's check the state of the graph.

	
< > Input
Python
snapshot = graph.get_state(config)
snapshot.next
Copied
>_ Output
			
('tools',)

We see that it stopped at the tools node. We analyze how the human_assistance tool has been defined.

from langgraph.types import Command, interrupt
from langchain_core.tools import tool

@tool

python

def human_assistance(query: str) -> str:

"""
Request assistance from a human expert. Use this tool ONLY ONCE per conversation.
After receiving the expert’s response, you should provide a detailed response to the user based on the information received
based on the information received, without calling this tool again.

Args:
query: The query to ask the human expert.

Returns:
The response from the human expert.
"""
human_response = interrupt({"query": query})
return human_response["data"]

Calling the interrupt tool will stop execution, similar to Python's input() function.

Progress is maintained based on our choice of checkpointer. In other words, the choice of where the graph state is saved. So if we are persisting (saving the graph state) with a database such as SQLite, Postgres, etc., we can resume execution at any time as long as the database is alive.

Here we are persisting (saving the graph state) with the checkpoint pointer in RAM, so we can resume at any time as long as our Python kernel is running. In my case, as long as I don’t reset my Jupyter Notebook kernel.

To resume execution, we pass a Command object that contains the data expected by the tool. The format of this data can be customized according to our needs. Here, we only need a dictionary with a data key

	
< > Input
Python
human_response = (
"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent."
"It's much more reliable and extensible than simple autonomous agents."
)
human_command = Command(resume={"data": human_response})
events = graph.stream(human_command, config, stream_mode="values")
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
Copied
>_ Output
			
================================== Ai Message ==================================
Tool Calls:
human_assistance (0)
Call ID: 0
Args:
query: I need some expert guidance for building an AI agent. Could you provide me with some advice?
================================= Tool Message =================================
Name: human_assistance
We, the experts are here to help! We'd recommend you check out LangGraph to build your agent.It's much more reliable and extensible than simple autonomous agents.
================================== Ai Message ==================================
The experts recommend checking out LangGraph for building your AI agent. It's known for being more reliable and extensible compared to simple autonomous agents.

As we can see, the chatbot has waited for a human to provide the answer and then generated a response based on the information received. We asked it for help from an expert on how to create agents, the human told it that the best thing is to use LangGraph, and the chatbot generated a response based on that information.

But it still has the ability to perform web searches. So now we’re going to ask it for the latest news about LangGraph.

	
< > Input
Python
user_input = "What's the latest news about LangGraph?"
events = graph.stream(
{"messages": [{"role": "user", "content": user_input}]},
config,
stream_mode="values",
)
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
What's the latest news about LangGraph?
================================== Ai Message ==================================
Tool Calls:
tavily_search_results_json (0)
Call ID: 0
Args:
query: latest news LangGraph
================================= Tool Message =================================
Name: tavily_search_results_json
[{"title": "LangChain - Changelog", "url": "https://changelog.langchain.com/", "content": "LangGraph `interrupt`: Simplifying human-in-the-loop agents --------------------------------------------------- Our latest feature in LangGraph, interrupt , makes building human-in-the-loop workflows easier. Agents aren’t perfect, so keeping humans “in the loop”... December 16, 2024 [...] LangGraph 🔁 Modify graph state from tools in LangGraph --------------------------------------------- LangGraph's latest update gives you greater control over your agents by enabling tools to directly update the graph state. This is a game-changer for use... December 18, 2024 [...] LangGraph Platform Custom authentication &amp; access control for LangGraph Platform ------------------------------------------------------------- Today, we're thrilled to announce Custom Authentication and Resource-Level Access Control for Python deployments in LangGraph Cloud and self-hosted... December 20, 2024", "score": 0.78650844}, {"title": "LangGraph 0.3 Release: Prebuilt Agents - LangChain Blog", "url": "https://blog.langchain.dev/langgraph-0-3-release-prebuilt-agents/", "content": "LangGraph 0.3 Release: Prebuilt Agents 2 min read Feb 27, 2025 By Nuno Campos and Vadym Barda Over the past year, we’ve invested heavily in making LangGraph the go-to framework for building AI agents. With companies like Replit, Klarna, LinkedIn and Uber choosing to build on top of LangGraph, we have more conviction than ever that we are on the right path. [...] Up to this point, we’ve had one higher level abstraction and it’s lived in the main langgraph package. It was create_react_agent, a wrapper for creating a simple tool calling agent. Today, we are splitting that out of langgraph as part of a 0.3 release, and moving it into langgraph-prebuilt. We are also introducing a new set of prebuilt agents built on top of LangGraph, in both Python and JavaScript. Over the past three weeks, we’ve already released a few of these: [...] Published Time: 2025-02-27T15:09:15.000Z LangGraph 0.3 Release: Prebuilt Agents Skip to content Case Studies In the Loop LangChain Docs Changelog Sign in Subscribe", "score": 0.72348577}]
================================== Ai Message ==================================
Tool Calls:
tavily_search_results_json (0)
Call ID: 0
Args:
query: latest news about LangGraph
================================= Tool Message =================================
Name: tavily_search_results_json
[{"title": "LangChain - Changelog", "url": "https://changelog.langchain.com/", "content": "LangGraph 🔁 Modify graph state from tools in LangGraph --------------------------------------------- LangGraph's latest update gives you greater control over your agents by enabling tools to directly update the graph state. This is a game-changer for use... December 18, 2024 [...] LangGraph `interrupt`: Simplifying human-in-the-loop agents --------------------------------------------------- Our latest feature in LangGraph, interrupt , makes building human-in-the-loop workflows easier. Agents aren’t perfect, so keeping humans “in the loop”... December 16, 2024 [...] LangGraph Platform Custom authentication &amp; access control for LangGraph Platform ------------------------------------------------------------- Today, we're thrilled to announce Custom Authentication and Resource-Level Access Control for Python deployments in LangGraph Cloud and self-hosted... December 20, 2024", "score": 0.79732054}, {"title": "LangGraph 0.3 Release: Prebuilt Agents - LangChain Blog", "url": "https://blog.langchain.dev/langgraph-0-3-release-prebuilt-agents/", "content": "LangGraph 0.3 Release: Prebuilt Agents 2 min read Feb 27, 2025 By Nuno Campos and Vadym Barda Over the past year, we’ve invested heavily in making LangGraph the go-to framework for building AI agents. With companies like Replit, Klarna, LinkedIn and Uber choosing to build on top of LangGraph, we have more conviction than ever that we are on the right path. [...] Up to this point, we’ve had one higher level abstraction and it’s lived in the main langgraph package. It was create_react_agent, a wrapper for creating a simple tool calling agent. Today, we are splitting that out of langgraph as part of a 0.3 release, and moving it into langgraph-prebuilt. We are also introducing a new set of prebuilt agents built on top of LangGraph, in both Python and JavaScript. Over the past three weeks, we’ve already released a few of these: [...] Published Time: 2025-02-27T15:09:15.000Z LangGraph 0.3 Release: Prebuilt Agents Skip to content Case Studies In the Loop LangChain Docs Changelog Sign in Subscribe", "score": 0.7552947}]
================================== Ai Message ==================================
The latest news about LangGraph includes several updates and releases. Firstly, the 'interrupt' feature has been added, which simplifies creating human-in-the-loop workflows, essential for maintaining oversight of AI agents. Secondly, an update allows tools to modify the graph state directly, providing more control over the agents. Lastly, custom authentication and resource-level access control have been implemented for Python deployments in LangGraph Cloud and self-hosted environments. In addition, LangGraph released version 0.3, which introduces prebuilt agents in both Python and JavaScript, aimed at making it even easier to develop AI agents.

It searched for the latest news about LangGraph and generated a response based on the information received.

Let's write everything together so it's easier to understand

	
< > Input
Python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
from huggingface_hub import login
from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import ToolMessage
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.types import Command, interrupt
from langchain_core.tools import tool
from langgraph.checkpoint.memory import MemorySaver
from IPython.display import Image, display
import json
import os
os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing
import dotenv
dotenv.load_dotenv()
HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_LANGGRAPH")
TAVILY_API_KEY = os.getenv("TAVILY_LANGGRAPH_API_KEY")
# State
class State(TypedDict):
messages: Annotated[list, add_messages]
# Tools
wrapper = TavilySearchAPIWrapper(tavily_api_key=TAVILY_API_KEY)
tool_search = TavilySearchResults(api_wrapper=wrapper, max_results=2)
@tool
def human_assistance(query: str) -&gt; str:
"""
Request assistance from a human expert. Use this tool ONLY ONCE per conversation.
After receiving the expert's response, you should provide an elaborated response to the user based on the information received
based on the information received, without calling this tool again.
Args:
query: The query to ask the human expert.
Returns:
The response from the human expert.
"""
human_response = interrupt({"query": query})
return human_response["data"]
tools_list = [tool_search, human_assistance]
# Create the LLM model
login(token=HUGGINGFACE_TOKEN) # Login to HuggingFace to use the model
MODEL = "Qwen/Qwen2.5-72B-Instruct"
model = HuggingFaceEndpoint(
repo_id=MODEL,
task="text-generation",
max_new_tokens=512,
do_sample=False,
repetition_penalty=1.03,
)
# Create the chat model
llm = ChatHuggingFace(llm=model)
# Create the LLM with tools
llm_with_tools = llm.bind_tools(tools_list)
# Tool node
tool_node = ToolNode(tools=tools_list)
# Functions
def chatbot_function(state: State):
message = llm_with_tools.invoke(state["messages"])
assert len(message.tool_calls) &lt;= 1
return {"messages": [message]}
# Start to build the graph
graph_builder = StateGraph(State)
# Add nodes to the graph
graph_builder.add_node("chatbot_node", chatbot_function)
graph_builder.add_node("tools", tool_node)
# Add edges
graph_builder.add_edge(START, "chatbot_node")
graph_builder.add_conditional_edges( "chatbot_node", tools_condition)
graph_builder.add_edge("tools", "chatbot_node")
# Compile the graph
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
# Display the graph
try:
display(Image(graph.get_graph().draw_mermaid_png()))
except Exception as e:
print(f"Error al visualizar el grafo: {e}")
Copied
>_ Output
			
Error al visualizar el grafo: Failed to reach https://mermaid.ink/ API while trying to render your graph after 1 retries. To resolve this issue:
1. Check your internet connection and try again
2. Try with higher retry settings: `draw_mermaid_png(..., max_retries=5, retry_delay=2.0)`
3. Use the Pyppeteer rendering method which will render your graph locally in a browser: `draw_mermaid_png(..., draw_method=MermaidDrawMethod.PYPPETEER)`

We ask the chatbot for help again to create agents. We ask it to seek help

	
< > Input
Python
user_input = "I need some expert guidance for building an AI agent. Could you request assistance for me?"
config = {"configurable": {"thread_id": "1"}}
events = graph.stream(
{"messages": [{"role": "user", "content": user_input}]},
config,
stream_mode="values",
)
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
I need some expert guidance for building an AI agent. Could you request assistance for me?
================================== Ai Message ==================================
Tool Calls:
human_assistance (0)
Call ID: 0
Args:
query: I need expert guidance for building an AI agent.

Let's see what state the graph is in.

	
< > Input
Python
snapshot = graph.get_state(config)
snapshot.next
Copied
>_ Output
			
('tools',)

We provide the assistance you are requesting

	
< > Input
Python
human_response = (
"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent."
"It's much more reliable and extensible than simple autonomous agents."
)
human_command = Command(resume={"data": human_response})
events = graph.stream(human_command, config, stream_mode="values")
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
Copied
>_ Output
			
================================== Ai Message ==================================
Tool Calls:
human_assistance (0)
Call ID: 0
Args:
query: I need expert guidance for building an AI agent.
================================= Tool Message =================================
Name: human_assistance
We, the experts are here to help! We'd recommend you check out LangGraph to build your agent.It's much more reliable and extensible than simple autonomous agents.
================================== Ai Message ==================================
Tool Calls:
human_assistance (0)
Call ID: 0
Args:
query: I need some expert guidance for building an AI agent. Could you recommend a platform and any tips for getting started?

And finally, we ask it to search the internet for the latest news about LangGraph

	
< > Input
Python
user_input = "What's the latest news about LangGraph?"
events = graph.stream(
{"messages": [{"role": "user", "content": user_input}]},
config,
stream_mode="values",
)
for event in events:
if "messages" in event:
event["messages"][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
What's the latest news about LangGraph?
================================== Ai Message ==================================
Tool Calls:
tavily_search_results_json (0)
Call ID: 0
Args:
query: latest news about LangGraph
================================= Tool Message =================================
Name: tavily_search_results_json
[{"title": "LangChain Blog", "url": "https://blog.langchain.dev/", "content": "LangSmith Incident on May 1, 2025 Requests to the US LangSmith API from both the web application and SDKs experienced an elevated error rate for 28 minutes on May 1, 2025 Featured How Klarna's AI assistant redefined customer support at scale for 85 million active users Is LangGraph Used In Production? Introducing Interrupt: The AI Agent Conference by LangChain Top 5 LangGraph Agents in Production 2024 [...] See how Harmonic uses LangSmith and LangGraph products to streamline venture investing workflows. Why Definely chose LangGraph for building their multi-agent AI system See how Definely used LangGraph to design a multi-agent system to help lawyers speed up their workflows. Introducing End-to-End OpenTelemetry Support in LangSmith LangSmith now provides end-to-end OpenTelemetry (OTel) support for applications built on LangChain and/or LangGraph.", "score": 0.6811549}, {"title": "LangGraph + UiPath: advancing agentic automation together", "url": "https://www.uipath.com/blog/product-and-updates/langgraph-uipath-advancing-agentic-automation-together", "content": "Raghu Malpani, Chief Technology Officer at UiPath, emphasizes the significance of these announcements for the UiPath developer community: Our collaboration with LangChain on LangSmith and Agent Protocol advances interoperability across agent frameworks. Further, by enabling the deployment of LangGraph agents into UiPath's enterprise-grade infrastructure, we are expanding the capabilities of our platform and opening up more possibilities for our developer community. [...] Today, we’re excited to announce: Native support for LangSmith observability in the UiPath LLM Gateway via OpenTelemetry (OTLP), enabling developers to monitor, debug, and evaluate LLM-powered features in UiPath using LangSmith either in LangChain’s cloud or self-hosted on-premises. This feature is currently in private preview.", "score": 0.6557114}]
================================== Ai Message ==================================
Tool Calls:
tavily_search_results_json (0)
Call ID: 0
Args:
query: latest news about LangGraph
================================= Tool Message =================================
Name: tavily_search_results_json
[{"title": "LangChain Blog", "url": "https://blog.langchain.dev/", "content": "LangSmith Incident on May 1, 2025 Requests to the US LangSmith API from both the web application and SDKs experienced an elevated error rate for 28 minutes on May 1, 2025 Featured How Klarna's AI assistant redefined customer support at scale for 85 million active users Is LangGraph Used In Production? Introducing Interrupt: The AI Agent Conference by LangChain Top 5 LangGraph Agents in Production 2024 [...] See how Harmonic uses LangSmith and LangGraph products to streamline venture investing workflows. Why Definely chose LangGraph for building their multi-agent AI system See how Definely used LangGraph to design a multi-agent system to help lawyers speed up their workflows. Introducing End-to-End OpenTelemetry Support in LangSmith LangSmith now provides end-to-end OpenTelemetry (OTel) support for applications built on LangChain and/or LangGraph.", "score": 0.6811549}, {"title": "LangGraph + UiPath: advancing agentic automation together", "url": "https://www.uipath.com/blog/product-and-updates/langgraph-uipath-advancing-agentic-automation-together", "content": "Raghu Malpani, Chief Technology Officer at UiPath, emphasizes the significance of these announcements for the UiPath developer community: Our collaboration with LangChain on LangSmith and Agent Protocol advances interoperability across agent frameworks. Further, by enabling the deployment of LangGraph agents into UiPath's enterprise-grade infrastructure, we are expanding the capabilities of our platform and opening up more possibilities for our developer community. [...] Today, we’re excited to announce: Native support for LangSmith observability in the UiPath LLM Gateway via OpenTelemetry (OTLP), enabling developers to monitor, debug, and evaluate LLM-powered features in UiPath using LangSmith either in LangChain’s cloud or self-hosted on-premises. This feature is currently in private preview.", "score": 0.6557114}]
...
================================= Tool Message =================================
Name: tavily_search_results_json
[{"title": "LangGraph - LangChain", "url": "https://www.langchain.com/langgraph", "content": "“As Ally advances its exploration of Generative AI, our tech labs is excited by LangGraph, the new library from LangChain, which is central to our experiments", "score": 0.98559}, {"title": "Evaluating LangGraph Framework : Series 1 | by Jalaj Agrawal", "url": "https://medium.com/@jalajagr/evaluating-langgraph-as-a-multiagent-framework-a-10-dimensional-framework-series-1-c7203b7f4659", "content": ": LangGraph excels with its intuitive graph-based abstraction that allows new developers to build working multi-agent systems within hours.", "score": 0.98196}]
================================== Ai Message ==================================
It looks like LangGraph has been generating some significant buzz in the AI community, especially for its capabilities in building multi-agent systems. Here are a few highlights from the latest news:
1. **LangGraph in Production**: Companies like Klarna and Definely are already using LangGraph to build and optimize their AI systems. Klarna has leveraged LangGraph to enhance their customer support, and Definely has used it to design a multi-agent system to speed up legal workflows.
2. **Integration with UiPath**: LangChain and UiPath have collaborated to advance agentic automation. This partnership includes native support for LangSmith observability in UiPath’s LLM Gateway via OpenTelemetry, which will allow developers to monitor, debug, and evaluate LLM-powered features more effectively.
3. **Intuitive Design**: LangGraph is praised for its intuitive graph-based abstraction, which enables developers to build working multi-agent systems quickly, even if they are new to the field.
4. **Community and Conferences**: LangChain is also hosting an AI Agent Conference called "Interrupt," which could be a great opportunity to learn more about the latest developments and best practices in building AI agents.
If you're considering using LangGraph for your project, these resources and updates might provide valuable insights and support. Would you like more detailed information on any specific aspect of LangGraph?

Morelink image 24

Tool Use Approvallink image 25

Note: We are going to do this section using Sonnet 3.7, since, as of the writing of the post, it is the best model for use with agents, and it is the only one that understands when it needs to call the tools and when it doesn’t for this example

We can add a human in the loop to approve tool usage. We are going to create a chatbot with several tools to perform mathematical operations; to do this, when building the graph, we indicate where we want to insert the breakpoint (graph_builder.compile(interrupt_before=["tools"], checkpointer=memory))

	
< > Input
Python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.tools import tool
from langchain_anthropic import ChatAnthropic
from IPython.display import Image, display
import os
import dotenv
dotenv.load_dotenv()
ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")
memory = MemorySaver()
class State(TypedDict):
messages: Annotated[list, add_messages]
os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing
# Tools
@tool
def multiply(a: int, b: int) -&gt; int:
"""Multiply a and b.
Args:
a: first int
b: second int
Returns:
The product of a and b.
"""
return a * b
@tool
def add(a: int, b: int) -&gt; int:
"""Adds a and b.
Args:
a: first int
b: second int
Returns:
The sum of a and b.
"""
return a + b
@tool
def subtract(a: int, b: int) -&gt; int:
"""Subtract b from a.
Args:
a: first int
b: second int
Returns:
The difference between a and b.
"""
return a - b
@tool
def divide(a: int, b: int) -&gt; float:
"""Divide a by b.
Args:
a: first int
b: second int
Returns:
The quotient of a and b.
"""
return a / b
tools_list = [multiply, add, subtract, divide]
# Create the LLM model
llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)
llm_with_tools = llm.bind_tools(tools_list)
# Nodes
def chat_model_node(state: State):
system_message = "You are a helpful assistant that can use tools to answer questions. Once you have the result of a tool, provide a final answer without calling more tools."
messages = [SystemMessage(content=system_message)] + state["messages"]
return {"messages": [llm_with_tools.invoke(messages)]}
# Create graph builder
graph_builder = StateGraph(State)
# Add nodes
graph_builder.add_node("chatbot_node", chat_model_node)
tool_node = ToolNode(tools=tools_list)
graph_builder.add_node("tools", tool_node)
# Connecto nodes
graph_builder.add_edge(START, "chatbot_node")
graph_builder.add_conditional_edges("chatbot_node", tools_condition)
graph_builder.add_edge("tools", "chatbot_node")
graph_builder.add_edge("chatbot_node", END)
# Compile the graph
graph = graph_builder.compile(interrupt_before=["tools"], checkpointer=memory)
display(Image(graph.get_graph().draw_mermaid_png()))
Copied
>_ Output
			
&lt;IPython.core.display.Image object&gt;

As we see in the graph, there is an interrupt before using the tools. That means it will stop before using them to ask us for permission

	
< > Input
Python
# Input
initial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}
config = {"configurable": {"thread_id": "1"}}
# Run the graph until the first interruption
for event in graph.stream(initial_input, config, stream_mode="updates"):
if 'chatbot_node' in event:
print(event['chatbot_node']['messages'][-1].pretty_print())
else:
print(event)
Copied
>_ Output
			
================================== Ai Message ==================================
[{'text': "I'll multiply 2 and 3 for you.", 'type': 'text'}, {'id': 'toolu_01QDuind1VBHWtvifELN9SPf', 'input': {'a': 2, 'b': 3}, 'name': 'multiply', 'type': 'tool_use'}]
Tool Calls:
multiply (toolu_01QDuind1VBHWtvifELN9SPf)
Call ID: toolu_01QDuind1VBHWtvifELN9SPf
Args:
a: 2
b: 3
None
{'__interrupt__': ()}

As we can see, the LLM knows that it has to use the multiply tool, but execution is interrupted because it has to wait for a human to authorize the use of the tool.

We can see the state the graph has been left in.

	
< > Input
Python
state = graph.get_state(config)
state.next
Copied
>_ Output
			
('tools',)

As we can see, it has remained in the tools node.

Can we create a function (not in the graph, but outside the graph, to improve the user experience and help them understand why execution stops) that asks the user to approve the use of the tool?

We create a new thread_id so that a new state is created.

	
< > Input
Python
# Input
initial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}
config = {"configurable": {"thread_id": "2"}}
# Run the graph until the first interruption
for event in graph.stream(initial_input, config, stream_mode="updates"):
function_name = None
function_args = None
if 'chatbot_node' in event:
for element in event['chatbot_node']['messages'][-1].content:
if element['type'] == 'text':
print(element['text'])
elif element['type'] == 'tool_use':
function_name = element['name']
function_args = element['input']
print(f"The LLM wants to use the tool {function_name} with the arguments {function_args}")
elif '__interrupt__' in event:
pass
else:
print(event)
question = f"Do you approve the use of the tool {function_name} with the arguments {function_args}? (y/n)"
user_approval = input(question)
print(f"{question}: {user_approval}")
if user_approval.lower() == 'y':
print("User approved the use of the tool")
for event in graph.stream(None, config, stream_mode="updates"):
if 'chatbot_node' in event:
for element in event['chatbot_node']['messages'][-1].content:
if isinstance(element, str):
print(element, end="")
elif 'tools' in event:
result = event['tools']['messages'][-1].content
tool_used = event['tools']['messages'][-1].name
print(f"The result of the tool {tool_used} is {result}")
else:
print(event)
Copied
>_ Output
			
I'll multiply 2 and 3 for you.
The LLM wants to use the tool multiply with the arguments {'a': 2, 'b': 3}
Do you approve the use of the tool None with the arguments None? (y/n): y
User approved the use of the tool
The result of the tool multiply is 6
The result of multiplying 2 and 3 is 6.

We can see that it has asked us whether we approve the use of the multiplication tool, we have approved it, and the graph has finished executing. We see the state of the graph.

	
< > Input
Python
state = graph.get_state(config)
state.next
Copied
>_ Output
			
()

We see that the following state of the graph is empty, which indicates that the execution of the graph has finished

State Modificationlink image 26

Note: We are going to do this section using Sonnet 3.7, since, as of the writing of the post, it is the best model for use with agents, and it is the only one that understands when it needs to call the tools and when it does not for this example

Let's repeat the previous example, but instead of interrupting the graph before the use of a tool, we will interrupt it in the LLM. To do this, when building the graph we indicate that we want to stop it at the agent (graph_builder.compile(interrupt_before=["chatbot_node"], checkpointer=memory))

	
< > Input
Python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.tools import tool
from langchain_anthropic import ChatAnthropic
from IPython.display import Image, display
import os
import dotenv
dotenv.load_dotenv()
ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")
memory = MemorySaver()
class State(TypedDict):
messages: Annotated[list, add_messages]
os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing
# Tools
@tool
def multiply(a: int, b: int) -&gt; int:
"""Multiply a and b.
Args:
a: first int
b: second int
Returns:
The product of a and b.
"""
return a * b
@tool
def add(a: int, b: int) -&gt; int:
"""Adds a and b.
Args:
a: first int
b: second int
Returns:
The sum of a and b.
"""
return a + b
@tool
def subtract(a: int, b: int) -&gt; int:
"""Subtract b from a.
Args:
a: first int
b: second int
Returns:
The difference between a and b.
"""
return a - b
@tool
def divide(a: int, b: int) -&gt; float:
"""Divide a by b.
Args:
a: first int
b: second int
Returns:
The quotient of a and b.
"""
return a / b
tools_list = [multiply, add, subtract, divide]
# Create the LLM model
llm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)
llm_with_tools = llm.bind_tools(tools_list)
# Nodes
def chat_model_node(state: State):
system_message = "You are a helpful assistant that can use tools to answer questions. Once you have the result of a tool, provide a final answer without calling more tools."
messages = [SystemMessage(content=system_message)] + state["messages"]
return {"messages": [llm_with_tools.invoke(messages)]}
# Create graph builder
graph_builder = StateGraph(State)
# Add nodes
graph_builder.add_node("chatbot_node", chat_model_node)
tool_node = ToolNode(tools=tools_list)
graph_builder.add_node("tools", tool_node)
# Connecto nodes
graph_builder.add_edge(START, "chatbot_node")
graph_builder.add_conditional_edges("chatbot_node", tools_condition)
graph_builder.add_edge("tools", "chatbot_node")
graph_builder.add_edge("chatbot_node", END)
# Compile the graph
graph = graph_builder.compile(interrupt_before=["chatbot_node"], checkpointer=memory)
display(Image(graph.get_graph().draw_mermaid_png()))
Copied
>_ Output
			
&lt;IPython.core.display.Image object&gt;

We can see in the graph representation that there is an interrupt before the execution of chatbot_node, so before the chatbot runs, execution will be interrupted and we will have to make it continue ourselves.

Now we ask for a multiplication again

	
< > Input
Python
# Input
initial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}
config = {"configurable": {"thread_id": "1"}}
# Run the graph until the first interruption
for event in graph.stream(initial_input, config, stream_mode="updates"):
if 'chatbot_node' in event:
print(event['chatbot_node']['messages'][-1].pretty_print())
else:
print(event)
Copied
>_ Output
			
{'__interrupt__': ()}

We can see that it hasn't done anything. If we look at the status

	
< > Input
Python
state = graph.get_state(config)
state.next
Copied
>_ Output
			
('chatbot_node',)

We see that the following node is the chatbot node. Also, if we look at its values, we see the message we have sent it

	
< > Input
Python
state.values
Copied
>_ Output
			
{'messages': [HumanMessage(content='Multiply 2 and 3', additional_kwargs={}, response_metadata={}, id='08fd6084-ecd2-4156-ab24-00d2d5c26f00')]}

Now we proceed to modify the state, adding a new message

	
< > Input
Python
graph.update_state(
config,
{"messages": [HumanMessage(content="No, actually multiply 3 and 3!")]}
)
Copied
>_ Output
			
{'configurable': {'thread_id': '1',
'checkpoint_ns': '',
'checkpoint_id': '1f027eb6-6c8b-6b6a-8001-bc0f8942566c'}}

We obtain the new state

	
< > Input
Python
new_state = graph.get_state(config)
new_state.next
Copied
>_ Output
			
('chatbot_node',)

The following node is still the chatbot's node, but if we now look at the messages

	
< > Input
Python
new_state.values
Copied
>_ Output
			
{'messages': [HumanMessage(content='Multiply 2 and 3', additional_kwargs={}, response_metadata={}, id='08fd6084-ecd2-4156-ab24-00d2d5c26f00'),
HumanMessage(content='No, actually multiply 3 and 3!', additional_kwargs={}, response_metadata={}, id='e95394c2-e62e-47d2-b9b2-51eba40f3e22')]}

We see that the new one has been added. So we make it continue execution

	
< > Input
Python
for event in graph.stream(None, config, stream_mode="values"):
event['messages'][-1].pretty_print()
Copied
>_ Output
			
================================ Human Message =================================
No, actually multiply 3 and 3!
================================== Ai Message ==================================
[{'text': "I'll multiply 3 and 3 for you.", 'type': 'text'}, {'id': 'toolu_01UABhLnEdg5ZqxVQTE5pGUx', 'input': {'a': 3, 'b': 3}, 'name': 'multiply', 'type': 'tool_use'}]
Tool Calls:
multiply (toolu_01UABhLnEdg5ZqxVQTE5pGUx)
Call ID: toolu_01UABhLnEdg5ZqxVQTE5pGUx
Args:
a: 3
b: 3
================================= Tool Message =================================
Name: multiply
9

The multiplication of 3 by 3 has been done, which is the state change we made, and not 2 by 3, which is what we asked for the first time

This can be useful when we have an agent and want to check that what it does is correct, so we can enter the execution and modify the state

Dynamic breakpointslink image 27

So far we have created static breakpoints by compiling the graph, but we can create dynamic breakpoints using NodeInterrupt. This is useful because execution can be interrupted based on logic rules introduced programmatically

These NodeInterrupts allow you to customize how the user will be notified of the interruption

	
< > Input
Python
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langgraph.checkpoint.memory import MemorySaver
from langgraph.errors import NodeInterrupt
from huggingface_hub import login
from IPython.display import Image, display
import os
import dotenv
dotenv.load_dotenv()
HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_LANGGRAPH")
memory_saver = MemorySaver()
class State(TypedDict):
messages: Annotated[list, add_messages]
os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing
# Create the LLM model
login(token=HUGGINGFACE_TOKEN) # Login to HuggingFace to use the model
MODEL = "Qwen/Qwen2.5-72B-Instruct"
model = HuggingFaceEndpoint(
repo_id=MODEL,
task="text-generation",
max_new_tokens=512,
do_sample=False,
repetition_penalty=1.03,
)
# Create the chat model
llm = ChatHuggingFace(llm=model)
# Nodes
def chatbot_function(state: State):
max_len = 15
input_message = state["messages"][-1]
# Check len message
if len(input_message.content) &gt; max_len:
raise NodeInterrupt(f"Received input is longer than {max_len} characters --&gt; {input_message}")
# Invoke the LLM with the messages
response = llm.invoke(state["messages"])
# Return the LLM's response in the correct state format
return {"messages": [response]}
# Create graph builder
graph_builder = StateGraph(State)
# Add nodes
graph_builder.add_node("chatbot_node", chatbot_function)
# Connecto nodes
graph_builder.add_edge(START, "chatbot_node")
graph_builder.add_edge("chatbot_node", END)
# Compile the graph
graph = graph_builder.compile(checkpointer=memory_saver)
display(Image(graph.get_graph().draw_mermaid_png()))
Copied
>_ Output
			
&lt;IPython.core.display.Image object&gt;

As can be seen, we have created a break in case the message is long. Let's test it.

	
< > Input
Python
initial_input = {"messages": HumanMessage(content="Hello, how are you? My name is Máximo")}
config = {"configurable": {"thread_id": "1"}}
# Run the graph until the first interruption
for event in graph.stream(initial_input, config, stream_mode="updates"):
if 'chatbot_node' in event:
print(event['chatbot_node']['messages'][-1].pretty_print())
else:
print(event)
Copied
>_ Output
			
{'__interrupt__': (Interrupt(value="Received input is longer than 15 characters --&gt; content='Hello, how are you? My name is Máximo' additional_kwargs={} response_metadata={} id='2bdc6d41-0cfe-4d3c-8748-ca7d46fd5a60'", resumable=False, ns=None),)}

Indeed, the interruption has been stopped and it has given us the error message that we created

If we look at the node where it has stopped

	
< > Input
Python
state = graph.get_state(config)
state.next
Copied
>_ Output
			
('chatbot_node',)

We can see that it is stopped at the chatbot node. We can make it continue execution again, but it will give us the same error.

	
< > Input
Python
for event in graph.stream(None, config, stream_mode="updates"):
if 'chatbot_node' in event:
print(event['chatbot_node']['messages'][-1].pretty_print())
else:
print(event)
Copied
>_ Output
			
{'__interrupt__': (Interrupt(value="Received input is longer than 15 characters --&gt; content='Hello, how are you? My name is Máximo' additional_kwargs={} response_metadata={} id='2bdc6d41-0cfe-4d3c-8748-ca7d46fd5a60'", resumable=False, ns=None),)}

So we have to modify the state

	
< > Input
Python
graph.update_state(
config,
{"messages": [HumanMessage(content="How are you?")]}
)
Copied
>_ Output
			
{'configurable': {'thread_id': '1',
'checkpoint_ns': '',
'checkpoint_id': '1f027f13-5827-6a18-8001-4209d5a866f0'}}

We look again at the state and its values

	
< > Input
Python
new_state = graph.get_state(config)
print(f"Siguiente nodo: {new_state.next}")
print("Valores:")
for value in new_state.values["messages"]:
print(f" {value.content}")
Copied
>_ Output
			
Siguiente nodo: ('chatbot_node',)
Valores:
Hello, how are you? My name is Máximo
How are you?

The last message is shorter, so we tried to resume the graph execution

	
< > Input
Python
for event in graph.stream(None, config, stream_mode="updates"):
if 'chatbot_node' in event:
print(event['chatbot_node']['messages'][-1].pretty_print())
else:
print(event)
Copied
>_ Output
			
================================== Ai Message ==================================
Hello Máximo! I'm doing well, thank you for asking. How about you? How can I assist you today?
None

---

➡️ **Continue in Part 4: state customization and checkpoints**.

Continue reading

Last posts -->

Have you seen these projects?

Gymnasia

Gymnasia Gymnasia
React Native
Expo
TypeScript
FastAPI
Next.js
OpenAI
Anthropic

Mobile personal training app with AI assistant, exercise library, workout tracking, diet and body measurements

Horeca chatbot

Horeca chatbot Horeca chatbot
Python
LangChain
PostgreSQL
PGVector
React
Kubernetes
Docker
GitHub Actions

Chatbot conversational for cooks of hotels and restaurants. A cook, kitchen manager or room service of a hotel or restaurant can talk to the chatbot to get information about recipes and menus. But it also implements agents, with which it can edit or create new recipes or menus

View all projects -->
>_ Available for projects

Do you have an AI project?

Let's talk.

maximofn@gmail.com

Machine Learning and AI specialist. I develop solutions with generative AI, intelligent agents and custom models.

Do you want to watch any talk?

Last talks -->

Do you want to improve with these tips?

Last tips -->

Use this locally

Hugging Face spaces allow us to run models with very simple demos, but what if the demo breaks? Or if the user deletes it? That's why I've created docker containers with some interesting spaces, to be able to use them locally, whatever happens. In fact, if you click on any project view button, it may take you to a space that doesn't work.

Flow edit

Flow edit Flow edit

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
View all containers -->
>_ Available for projects

Do you have an AI project?

Let's talk.

maximofn@gmail.com

Machine Learning and AI specialist. I develop solutions with generative AI, intelligent agents and custom models.

Do you want to train your model with these datasets?

short-jokes-dataset

HuggingFace

Dataset with jokes in English

Use: Fine-tuning text generation models for humor

231K rows 2 columns 45 MB
View on HuggingFace →

opus100

HuggingFace

Dataset with translations from English to Spanish

Use: Training English-Spanish translation models

1M rows 2 columns 210 MB
View on HuggingFace →

netflix_titles

HuggingFace

Dataset with Netflix movies and series

Use: Netflix catalog analysis and recommendation systems

8.8K rows 12 columns 3.5 MB
View on HuggingFace →
View more datasets -->