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 threads
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 Store
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
namespacefor the object is created using atuple - A unique
key - The
valueof the object
Let's see an example
InputPythonimport uuidfrom langgraph.store.memory import InMemoryStorein_memory_store = InMemoryStore()# Namespace for the memory to saveuser_id = "1"namespace_for_memory = (user_id, "memories")# Save a memory to namespace as key and valuekey = str(uuid.uuid4())# The value needs to be a dictionaryvalue = {"food_preference" : "I like pizza"}# Save the memoryin_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
InputPython# Searchmemories = in_memory_store.search(namespace_for_memory)type(memories), len(memories)Copied
(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.
InputPythonvalue = memories[0]value.dict()Copied
{'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
InputPython# The key, valuememories[0].key, memories[0].valueCopied
('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
InputPython# Get the memory by namespace and keymemory = in_memory_store.get(namespace_for_memory, key)memory.dict()Copied
{'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 memory
We created a basic chatbot, with long-term memory and short-term memory.
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, MessagesState, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFacefrom langchain_core.messages import HumanMessage, AIMessage, SystemMessagefrom langgraph.checkpoint.memory import MemorySaver # Short-term memoryfrom langgraph.store.base import BaseStore # Long-term memoryfrom langchain_core.runnables.config import RunnableConfigfrom langgraph.store.memory import InMemoryStorefrom huggingface_hub import loginfrom IPython.display import Image, displayimport osimport dotenvdotenv.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 modellogin(token=HUGGINGFACE_TOKEN) # Login to HuggingFace to use the modelMODEL = "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 modelllm = ChatHuggingFace(llm=model)# Chatbot instructionMODEL_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 memoryCREATE_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 history2. Identify new information from the user, such as:- Personal details (name, location)- Preferences (likes, dislikes)- Interests and hobbies- Past experiences- Goals or future plans3. Combine any new information with the existing memory4. Format the memory as a clear, bulleted list5. If new information conflicts with existing memory, keep the most recent versionRemember: 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:"""# Nodesdef 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 configuser_id = config["configurable"]["user_id"]# Retrieve memory from the storenamespace = ("memory", user_id)key = "user_memory"existing_memory = store.get(namespace, key)# Extract the actual memory content if it exists and add a prefixif existing_memory:# Value is a dictionary with a memory keyexisting_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 promptsystem_msg = MODEL_SYSTEM_MESSAGE.format(memory=existing_memory_content)# Respond using memory as well as the chat historyresponse = 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 configuser_id = config["configurable"]["user_id"]# Retrieve existing memory from the storenamespace = ("memory", user_id)existing_memory = store.get(namespace, "user_memory")# Extract the memoryif 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 promptsystem_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 storekey = "user_memory"# Write value as a dictionary with a memory keystore.put(namespace, key, {"memory": new_memory.content})# Create graph buildergraph_builder = StateGraph(State)# Add nodesgraph_builder.add_node("call_model", call_model)graph_builder.add_node("write_memory", write_memory)# Connect nodesgraph_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) memorylong_term_memory = InMemoryStore()# Checkpointer for short-term (within-thread) memoryshort_term_memory = MemorySaver()# Compile the graphgraph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
Let's try it
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "1", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="Hi, my name is Maximo")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ 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
InputPython# User inputinput_messages = [HumanMessage(content="I like to bike around San Francisco")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ 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
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memory", user_id)existing_memory = long_term_memory.get(namespace, "user_memory")existing_memory.dict()Copied
{'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
InputPythonprint(existing_memory.value.get('memory'))Copied
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.
InputPython# We supply a user ID for across-thread memory as well as a new thread IDconfig = {"configurable": {"thread_id": "2", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="Hi! Where would you recommend that I go biking?")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ 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 profile
Note: In this section, we will use Sonnet 3.7, since the HuggingFace integration does not have the
with_structured_outputfunctionality 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.
InputPythonfrom typing import TypedDict, Listclass UserProfile(TypedDict):"""User profile schema with typed fields"""user_name: str # The user's preferred nameinterests: List[str] # A list of the user's interestsCopied
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.
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, MessagesState, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_anthropic import ChatAnthropicfrom langchain_core.messages import HumanMessage, AIMessage, SystemMessagefrom langgraph.checkpoint.memory import MemorySaver # Short-term memoryfrom langgraph.store.base import BaseStore # Long-term memoryfrom langchain_core.runnables.config import RunnableConfigfrom langgraph.store.memory import InMemoryStorefrom IPython.display import Image, displayfrom pydantic import BaseModel, Fieldimport osimport dotenvdotenv.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 modelllm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)llm_with_structured_output = llm.with_structured_output(UserProfile)# Chatbot instructionMODEL_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 memoryCREATE_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}"""# Nodesdef 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 configuser_id = config["configurable"]["user_id"]# Retrieve memory from the storenamespace = ("memory", user_id)existing_memory = store.get(namespace, "user_memory")# Format the memories for the system promptif existing_memory and existing_memory.value:memory_dict = existing_memory.valueformatted_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 promptsystem_msg = MODEL_SYSTEM_MESSAGE.format(memory=formatted_memory)# Respond using memory as well as the chat historyresponse = 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 configuser_id = config["configurable"]["user_id"]# Retrieve existing memory from the storenamespace = ("memory", user_id)existing_memory = store.get(namespace, "user_memory")# Format the memories for the system promptif existing_memory and existing_memory.value:memory_dict = existing_memory.valueformatted_memory = (f"Name: {memory_dict.get('user_name', 'Unknown')} "f"Interests: {', '.join(memory_dict.get('interests', []))}")else:formatted_memory = Noneprint(f" [Write memory debug] Existing memory: {formatted_memory}")# Format the existing memory in the instructionsystem_msg = CREATE_MEMORY_INSTRUCTION.format(memory=formatted_memory)# Invoke the model to produce structured output that matches the schemanew_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 memorykey = "user_memory"store.put(namespace, key, new_memory)# Create graph buildergraph_builder = StateGraph(MessagesState)# Add nodesgraph_builder.add_node("call_model", call_model)graph_builder.add_node("write_memory", write_memory)# Connect nodesgraph_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) memorylong_term_memory = InMemoryStore()# Checkpointer for short-term (within-thread) memoryshort_term_memory = MemorySaver()# Compile the graphgraph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
We run the graph
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "1", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="Hi, my name is Maximo and I like to bike around Madrid and eat salads.")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ 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.
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memory", user_id)existing_memory = long_term_memory.get(namespace, "user_memory")existing_memory.valueCopied
{'user_name': 'Maximo', 'interests': ['biking', 'Madrid', 'salads']}
More
Update structured schemas with Trustcall
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.
InputPythonfrom langchain_core.messages import HumanMessage, AIMessage# Conversationconversation = [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
InputPythonfrom pydantic import BaseModel, Fieldfrom typing import List# Schemaclass 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 ChatAnthropicimport osimport dotenvdotenv.load_dotenv()ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Create the LLM modelllm = 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
InputPythonfrom trustcall import create_extractor# Create the extractortrustcall_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
InputPythonfrom langchain_core.messages import SystemMessage# Instructionsystem_msg = "Extract the user profile from the following conversation"# Invoke the extractorresult = trustcall_extractor.invoke({"messages": [SystemMessage(content=system_msg)]+conversation})resultCopied
{'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
InputPythonfor m in result["messages"]:m.pretty_print()Copied
================================== Ai Message ==================================[{'id': 'toolu_01WfgbD1fG3rJYAXGrjqjfVY', 'input': {'user_name': 'Maximo', 'interests': ['soccer']}, 'name': 'UserProfile', 'type': 'tool_use'}]Tool Calls:UserProfile (toolu_01WfgbD1fG3rJYAXGrjqjfVY)Call ID: toolu_01WfgbD1fG3rJYAXGrjqjfVYArgs:user_name: Maximointerests: ['soccer']
The UserProfile schema has been updated with the new field.
InputPythonschema = result["responses"]schemaCopied
[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.
InputPythontype(schema[0])Copied
__main__.UserProfile
Can we convert it to a dictionary with model_dump
InputPythonschema[0].model_dump()Copied
{'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
InputPython# Update the conversationupdated_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
InputPython# Update the instructionsystem_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()}})resultCopied
{'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
InputPythonfor m in result["messages"]:m.pretty_print()Copied
================================== 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_01K1zTh33kXDAw1h18Yh2HBbArgs:user_name: Maximointerests: ['soccer', 'bakeries']
We see the updated diagram
InputPythonupdated_schema = result["responses"][0]updated_schema.model_dump()Copied
{'user_name': 'Maximo', 'interests': ['soccer', 'bakeries']}
Chatbot with user profile updated with Trustcall
We recreate the graph that updates the user profile, but now with the trustcall library
InputPythonfrom pydantic import BaseModel, Fieldfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, MessagesState, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_anthropic import ChatAnthropicfrom langchain_core.messages import HumanMessage, AIMessage, SystemMessagefrom langgraph.checkpoint.memory import MemorySaver # Short-term memoryfrom langgraph.store.base import BaseStore # Long-term memoryfrom langchain_core.runnables.config import RunnableConfigfrom langgraph.store.memory import InMemoryStorefrom IPython.display import Image, displayfrom pydantic import BaseModel, Fieldimport osimport dotenvfrom trustcall import create_extractordotenv.load_dotenv()ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Schemaclass 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 modelllm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)# Create the extractortrustcall_extractor = create_extractor(llm,tools=[UserProfile],tool_choice="UserProfile", # Enforces use of the UserProfile tool)# Chatbot instructionMODEL_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 memoryTRUSTCALL_INSTRUCTION = """Create or update the memory (JSON doc) to incorporate information from the following conversation:"""# Nodesdef 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 configuser_id = config["configurable"]["user_id"]# Retrieve memory from the storenamespace = ("memory", user_id)existing_memory = store.get(namespace, "user_memory")# Format the memories for the system promptif existing_memory and existing_memory.value:memory_dict = existing_memory.valueformatted_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 = Noneprint(f" [Call model debug] Existing memory: {formatted_memory}")# Format the memory in the system promptsystem_msg = MODEL_SYSTEM_MESSAGE.format(memory=formatted_memory)# Respond using memory as well as the chat historyresponse = 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 configuser_id = config["configurable"]["user_id"]# Retrieve existing memory from the storenamespace = ("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 docexisting_profile = {"UserProfile": existing_memory.value} if existing_memory else Noneprint(f" [Write memory debug] Existing profile: {existing_profile}")# Invoke the extractorresult = trustcall_extractor.invoke({"messages": [SystemMessage(content=TRUSTCALL_INSTRUCTION)]+state["messages"], "existing": existing_profile})# Get the updated profile as a JSON objectupdated_profile = result["responses"][0].model_dump()print(f" [Write memory debug] Updated profile: {updated_profile}")# Save the updated profilekey = "user_memory"store.put(namespace, key, updated_profile)# Create graph buildergraph_builder = StateGraph(MessagesState)# Add nodesgraph_builder.add_node("call_model", call_model)graph_builder.add_node("write_memory", write_memory)# Connect nodesgraph_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) memorylong_term_memory = InMemoryStore()# Checkpointer for short-term (within-thread) memoryshort_term_memory = MemorySaver()# Compile the graphgraph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
We start the conversation
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "1", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="Hi, my name is Maximo")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ 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': '<UNKNOWN>', 'interests': []}
As we can see, it does not know either the user's location or interests. Let's update the user's profile.
InputPython# User inputinput_messages = [HumanMessage(content="I like to play soccer and I live in Madrid")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================I like to play soccer and I live in Madrid[Call model debug] Existing memory: Name: MaximoLocation: <UNKNOWN>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': '<UNKNOWN>', '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
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memory", user_id)existing_memory = long_term_memory.get(namespace, "user_memory")existing_memory.dict()Copied
{'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
InputPython# The user profile saved as a JSON objectexisting_memory.valueCopied
{'user_name': 'Maximo', 'user_location': 'Madrid', 'interests': ['soccer']}
Let's add a new user interest
InputPython# User inputinput_messages = [HumanMessage(content="I also like to play basketball")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================I also like to play basketball[Call model debug] Existing memory: Name: MaximoLocation: MadridInterests: 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
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memory", user_id)existing_memory = long_term_memory.get(namespace, "user_memory")existing_memory.valueCopied
{'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.
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "2", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="What soccer players do you recommend for me?")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ Human Message =================================What soccer players do you recommend for me?[Call model debug] Existing memory: Name: MaximoLocation: MadridInterests: 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 BruyneYou 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 Trustcall
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
InputPythonfrom langgraph.graph import StateGraph, MessagesState, START, ENDfrom langchain_anthropic import ChatAnthropicfrom langchain_core.messages import HumanMessage, AIMessage, SystemMessagefrom langchain_core.messages import merge_message_runsfrom langgraph.checkpoint.memory import MemorySaver # Short-term memoryfrom langgraph.store.base import BaseStore # Long-term memoryfrom langchain_core.runnables.config import RunnableConfigfrom langgraph.store.memory import InMemoryStorefrom IPython.display import Image, displayfrom trustcall import create_extractorfrom pydantic import BaseModel, Fieldimport uuidimport osimport dotenvdotenv.load_dotenv()ANTHROPIC_TOKEN = os.getenv("ANTHROPIC_LANGGRAPH_API_KEY")os.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Memory schemaclass 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 modelllm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)# Create the extractortrustcall_extractor = create_extractor(llm,tools=[Memory],tool_choice="Memory",# This allows the extractor to insert new memoriesenable_inserts=True,)# Chatbot instructionMODEL_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 memoryTRUSTCALL_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:"""# Nodesdef 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 configuser_id = config["configurable"]["user_id"]# Retrieve memory from the storenamespace = ("memories", user_id)memories = store.search(namespace)print(f" [Call model debug] Memories: {memories}")# Format the memories for the system promptinfo = " ".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 historyresponse = 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 configuser_id = config["configurable"]["user_id"]# Define the namespace for the memoriesnamespace = ("memories", user_id)# Retrieve the most recent memories for contextexisting_items = store.search(namespace)# Format the existing memories for the Trustcall extractortool_name = "Memory"existing_memories = ([(existing_item.key, tool_name, existing_item.value)for existing_item in existing_items]if existing_itemselse None)print(f" [Write memory debug] Existing memories: {existing_memories}")# Merge the chat history and the instructionupdated_messages=list(merge_message_runs(messages=[SystemMessage(content=TRUSTCALL_INSTRUCTION)] + state["messages"]))# Invoke the extractorresult = trustcall_extractor.invoke({"messages": updated_messages,"existing": existing_memories})# Save the memories from Trustcall to the storefor 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 buildergraph_builder = StateGraph(MessagesState)# Add nodesgraph_builder.add_node("call_model", call_model)graph_builder.add_node("write_memory", write_memory)# Connect nodesgraph_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) memorylong_term_memory = InMemoryStore()# Checkpointer for short-term (within-thread) memoryshort_term_memory = MemorySaver()# Compile the graphgraph = graph_builder.compile(checkpointer=short_term_memory, store=long_term_memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
We are starting a new conversation
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "1", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="Hi, my name is Maximo")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ 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
InputPython# User inputinput_messages = [HumanMessage(content="I like to play soccer")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ 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
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memories", user_id)memories = long_term_memory.search(namespace)for m in memories:print(m.dict())Copied
{'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}
InputPythonfor m in memories:print(m.value)Copied
{'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
InputPython# User inputinput_messages = [HumanMessage(content="I also like to play basketball")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ 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
InputPython# Namespace for the memory to saveuser_id = "1"namespace = ("memories", user_id)memories = long_term_memory.search(namespace)for m in memories:print(m.value)Copied
{'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
InputPython# We supply a thread ID for short-term (within-thread) memory# We supply a user ID for long-term (across-thread) memoryconfig = {"configurable": {"thread_id": "2", "user_id": "1"}}# User inputinput_messages = [HumanMessage(content="What soccer players do you recommend for me?")]# Run the graphfor chunk in graph.stream({"messages": input_messages}, config, stream_mode="values"):chunk["messages"][-1].pretty_print()Copied
================================ 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 time2. Cristiano Ronaldo - Known for incredible athleticism and dedication3. Kylian Mbappé - Young talent with amazing speed and technical ability4. Kevin De Bruyne - Master of passing and vision5. Erling Haaland - Goal-scoring phenomenonIs 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 loop
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
InputPythonimport osimport dotenvdotenv.load_dotenv()HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_LANGGRAPH")TAVILY_API_KEY = os.getenv("TAVILY_LANGGRAPH_API_KEY")Copied
We create the graph
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messagesclass State(TypedDict):messages: Annotated[list, add_messages]graph_builder = StateGraph(State)Copied
We define the search tool
InputPythonfrom langchain_community.utilities.tavily_search import TavilySearchAPIWrapperfrom langchain_community.tools.tavily_search import TavilySearchResultswrapper = TavilySearchAPIWrapper(tavily_api_key=TAVILY_API_KEY)search_tool = TavilySearchResults(api_wrapper=wrapper, max_results=2)Copied
Now we create the human support tool
InputPythonfrom langgraph.types import Command, interruptfrom langchain_core.tools import tool@tooldef 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 an elaborated response to the user based on the information receivedbased 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
InputPythontools_list = [search_tool, human_assistance]Copied
Next, the LLM with the bind_tools and we add it to the graph
InputPythonfrom langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFacefrom huggingface_hub import loginos.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracing# Create the LLMlogin(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 modelllm = ChatHuggingFace(llm=model)# Modification: tell the LLM which tools it can callllm_with_tools = llm.bind_tools(tools_list)# Define the chatbot functiondef chatbot_function(state: State):message = llm_with_tools.invoke(state["messages"])assert len(message.tool_calls) <= 1return {"messages": [message]}# Add the chatbot nodegraph_builder.add_node("chatbot_node", chatbot_function)Copied
<langgraph.graph.state.StateGraph at 0x10764b380>
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
InputPythonfrom langgraph.prebuilt import ToolNode, tools_conditiontool_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
<langgraph.graph.state.StateGraph at 0x10764b380>
We added the START node to the graph
InputPythongraph_builder.add_edge(START, "chatbot_node")Copied
<langgraph.graph.state.StateGraph at 0x10764b380>
We create a checkpointer MemorySaver.
InputPythonfrom langgraph.checkpoint.memory import MemorySavermemory = MemorySaver()Copied
We compiled the graph with the checkpointer
InputPythongraph = graph_builder.compile(checkpointer=memory)Copied
We represent it graphically
InputPythonfrom IPython.display import Image, displaytry:display(Image(graph.get_graph().draw_mermaid_png()))except Exception as e:print(f"Error al visualizar el grafo: {e}")Copied
<IPython.core.display.Image object>
Now let’s ask the chatbot a question that will involve the new human_assistance tool:
InputPythonuser_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
================================ 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: 0Args: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.
InputPythonsnapshot = graph.get_state(config)snapshot.nextCopied
('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
@toolpython
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
InputPythonhuman_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
================================== Ai Message ==================================Tool Calls:human_assistance (0)Call ID: 0Args:query: I need some expert guidance for building an AI agent. Could you provide me with some advice?================================= Tool Message =================================Name: human_assistanceWe, 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.
InputPythonuser_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
================================ Human Message =================================What's the latest news about LangGraph?================================== Ai Message ==================================Tool Calls:tavily_search_results_json (0)Call ID: 0Args: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 & 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: 0Args: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 & 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
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFacefrom huggingface_hub import loginfrom langchain_community.utilities.tavily_search import TavilySearchAPIWrapperfrom langchain_community.tools.tavily_search import TavilySearchResultsfrom langchain_core.messages import ToolMessagefrom langgraph.prebuilt import ToolNode, tools_conditionfrom langgraph.types import Command, interruptfrom langchain_core.tools import toolfrom langgraph.checkpoint.memory import MemorySaverfrom IPython.display import Image, displayimport jsonimport osos.environ["LANGCHAIN_TRACING_V2"] = "false" # Disable LangSmith tracingimport dotenvdotenv.load_dotenv()HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_LANGGRAPH")TAVILY_API_KEY = os.getenv("TAVILY_LANGGRAPH_API_KEY")# Stateclass State(TypedDict):messages: Annotated[list, add_messages]# Toolswrapper = TavilySearchAPIWrapper(tavily_api_key=TAVILY_API_KEY)tool_search = TavilySearchResults(api_wrapper=wrapper, max_results=2)@tooldef 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 an elaborated response to the user based on the information receivedbased 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 modellogin(token=HUGGINGFACE_TOKEN) # Login to HuggingFace to use the modelMODEL = "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 modelllm = ChatHuggingFace(llm=model)# Create the LLM with toolsllm_with_tools = llm.bind_tools(tools_list)# Tool nodetool_node = ToolNode(tools=tools_list)# Functionsdef chatbot_function(state: State):message = llm_with_tools.invoke(state["messages"])assert len(message.tool_calls) <= 1return {"messages": [message]}# Start to build the graphgraph_builder = StateGraph(State)# Add nodes to the graphgraph_builder.add_node("chatbot_node", chatbot_function)graph_builder.add_node("tools", tool_node)# Add edgesgraph_builder.add_edge(START, "chatbot_node")graph_builder.add_conditional_edges( "chatbot_node", tools_condition)graph_builder.add_edge("tools", "chatbot_node")# Compile the graphmemory = MemorySaver()graph = graph_builder.compile(checkpointer=memory)# Display the graphtry:display(Image(graph.get_graph().draw_mermaid_png()))except Exception as e:print(f"Error al visualizar el grafo: {e}")Copied
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 again2. 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
InputPythonuser_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
================================ 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: 0Args:query: I need expert guidance for building an AI agent.
Let's see what state the graph is in.
InputPythonsnapshot = graph.get_state(config)snapshot.nextCopied
('tools',)
We provide the assistance you are requesting
InputPythonhuman_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
================================== Ai Message ==================================Tool Calls:human_assistance (0)Call ID: 0Args:query: I need expert guidance for building an AI agent.================================= Tool Message =================================Name: human_assistanceWe, 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: 0Args: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
InputPythonuser_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
================================ Human Message =================================What's the latest news about LangGraph?================================== Ai Message ==================================Tool Calls:tavily_search_results_json (0)Call ID: 0Args: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: 0Args: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?
More
Tool Use Approval
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))
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_core.messages import SystemMessage, HumanMessage, AIMessagefrom langgraph.prebuilt import ToolNode, tools_conditionfrom langgraph.checkpoint.memory import MemorySaverfrom langchain_core.tools import toolfrom langchain_anthropic import ChatAnthropicfrom IPython.display import Image, displayimport osimport dotenvdotenv.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@tooldef multiply(a: int, b: int) -> int:"""Multiply a and b.Args:a: first intb: second intReturns:The product of a and b."""return a * b@tooldef add(a: int, b: int) -> int:"""Adds a and b.Args:a: first intb: second intReturns:The sum of a and b."""return a + b@tooldef subtract(a: int, b: int) -> int:"""Subtract b from a.Args:a: first intb: second intReturns:The difference between a and b."""return a - b@tooldef divide(a: int, b: int) -> float:"""Divide a by b.Args:a: first intb: second intReturns:The quotient of a and b."""return a / btools_list = [multiply, add, subtract, divide]# Create the LLM modelllm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)llm_with_tools = llm.bind_tools(tools_list)# Nodesdef 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 buildergraph_builder = StateGraph(State)# Add nodesgraph_builder.add_node("chatbot_node", chat_model_node)tool_node = ToolNode(tools=tools_list)graph_builder.add_node("tools", tool_node)# Connecto nodesgraph_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 graphgraph = graph_builder.compile(interrupt_before=["tools"], checkpointer=memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
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
InputPython# Inputinitial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}config = {"configurable": {"thread_id": "1"}}# Run the graph until the first interruptionfor 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
================================== 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_01QDuind1VBHWtvifELN9SPfArgs:a: 2b: 3None{'__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.
InputPythonstate = graph.get_state(config)state.nextCopied
('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.
InputPython# Inputinitial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}config = {"configurable": {"thread_id": "2"}}# Run the graph until the first interruptionfor event in graph.stream(initial_input, config, stream_mode="updates"):function_name = Nonefunction_args = Noneif '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:passelse: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].contenttool_used = event['tools']['messages'][-1].nameprint(f"The result of the tool {tool_used} is {result}")else:print(event)Copied
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): yUser approved the use of the toolThe result of the tool multiply is 6The 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.
InputPythonstate = graph.get_state(config)state.nextCopied
()
We see that the following state of the graph is empty, which indicates that the execution of the graph has finished
State Modification
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))
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_core.messages import SystemMessage, HumanMessage, AIMessagefrom langgraph.prebuilt import ToolNode, tools_conditionfrom langgraph.checkpoint.memory import MemorySaverfrom langchain_core.tools import toolfrom langchain_anthropic import ChatAnthropicfrom IPython.display import Image, displayimport osimport dotenvdotenv.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@tooldef multiply(a: int, b: int) -> int:"""Multiply a and b.Args:a: first intb: second intReturns:The product of a and b."""return a * b@tooldef add(a: int, b: int) -> int:"""Adds a and b.Args:a: first intb: second intReturns:The sum of a and b."""return a + b@tooldef subtract(a: int, b: int) -> int:"""Subtract b from a.Args:a: first intb: second intReturns:The difference between a and b."""return a - b@tooldef divide(a: int, b: int) -> float:"""Divide a by b.Args:a: first intb: second intReturns:The quotient of a and b."""return a / btools_list = [multiply, add, subtract, divide]# Create the LLM modelllm = ChatAnthropic(model="claude-3-7-sonnet-20250219", api_key=ANTHROPIC_TOKEN)llm_with_tools = llm.bind_tools(tools_list)# Nodesdef 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 buildergraph_builder = StateGraph(State)# Add nodesgraph_builder.add_node("chatbot_node", chat_model_node)tool_node = ToolNode(tools=tools_list)graph_builder.add_node("tools", tool_node)# Connecto nodesgraph_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 graphgraph = graph_builder.compile(interrupt_before=["chatbot_node"], checkpointer=memory)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
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
InputPython# Inputinitial_input = {"messages": HumanMessage(content="Multiply 2 and 3")}config = {"configurable": {"thread_id": "1"}}# Run the graph until the first interruptionfor 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
{'__interrupt__': ()}
We can see that it hasn't done anything. If we look at the status
InputPythonstate = graph.get_state(config)state.nextCopied
('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
InputPythonstate.valuesCopied
{'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
InputPythongraph.update_state(config,{"messages": [HumanMessage(content="No, actually multiply 3 and 3!")]})Copied
{'configurable': {'thread_id': '1','checkpoint_ns': '','checkpoint_id': '1f027eb6-6c8b-6b6a-8001-bc0f8942566c'}}
We obtain the new state
InputPythonnew_state = graph.get_state(config)new_state.nextCopied
('chatbot_node',)
The following node is still the chatbot's node, but if we now look at the messages
InputPythonnew_state.valuesCopied
{'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
InputPythonfor event in graph.stream(None, config, stream_mode="values"):event['messages'][-1].pretty_print()Copied
================================ 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_01UABhLnEdg5ZqxVQTE5pGUxArgs:a: 3b: 3================================= Tool Message =================================Name: multiply9
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 breakpoints
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
InputPythonfrom typing import Annotatedfrom typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, ENDfrom langgraph.graph.message import add_messagesfrom langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFacefrom langchain_core.messages import SystemMessage, HumanMessage, AIMessagefrom langgraph.checkpoint.memory import MemorySaverfrom langgraph.errors import NodeInterruptfrom huggingface_hub import loginfrom IPython.display import Image, displayimport osimport dotenvdotenv.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 modellogin(token=HUGGINGFACE_TOKEN) # Login to HuggingFace to use the modelMODEL = "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 modelllm = ChatHuggingFace(llm=model)# Nodesdef chatbot_function(state: State):max_len = 15input_message = state["messages"][-1]# Check len messageif len(input_message.content) > max_len:raise NodeInterrupt(f"Received input is longer than {max_len} characters --> {input_message}")# Invoke the LLM with the messagesresponse = llm.invoke(state["messages"])# Return the LLM's response in the correct state formatreturn {"messages": [response]}# Create graph buildergraph_builder = StateGraph(State)# Add nodesgraph_builder.add_node("chatbot_node", chatbot_function)# Connecto nodesgraph_builder.add_edge(START, "chatbot_node")graph_builder.add_edge("chatbot_node", END)# Compile the graphgraph = graph_builder.compile(checkpointer=memory_saver)display(Image(graph.get_graph().draw_mermaid_png()))Copied
<IPython.core.display.Image object>
As can be seen, we have created a break in case the message is long. Let's test it.
InputPythoninitial_input = {"messages": HumanMessage(content="Hello, how are you? My name is Máximo")}config = {"configurable": {"thread_id": "1"}}# Run the graph until the first interruptionfor 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
{'__interrupt__': (Interrupt(value="Received input is longer than 15 characters --> 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
InputPythonstate = graph.get_state(config)state.nextCopied
('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.
InputPythonfor 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
{'__interrupt__': (Interrupt(value="Received input is longer than 15 characters --> 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
InputPythongraph.update_state(config,{"messages": [HumanMessage(content="How are you?")]})Copied
{'configurable': {'thread_id': '1','checkpoint_ns': '','checkpoint_id': '1f027f13-5827-6a18-8001-4209d5a866f0'}}
We look again at the state and its values
InputPythonnew_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
Siguiente nodo: ('chatbot_node',)Valores:Hello, how are you? My name is MáximoHow are you?
The last message is shorter, so we tried to resume the graph execution
InputPythonfor 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
================================== 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**.