How to migrate from legacy LangChain agents to LangGraph
This guide assumes familiarity with the following concepts:
Here we focus on how to move from legacy LangChain agents to more flexible LangGraph agents. LangChain agents (the AgentExecutor in particular) have multiple configuration parameters. In this notebook we will show how those parameters map to the LangGraph react agent executor using the create_react_agent prebuilt helper method.
Prerequisitesβ
This how-to guide uses OpenAI as the LLM. Install the dependencies to run.
%%capture --no-stderr
%pip install -U langgraph langchain langchain-openai
Then, set your OpenAI API key.
import getpass
import os
if "OPENAI_API_KEY" not in os.environ:
os.environ["OPENAI_API_KEY"] = getpass.getpass("OpenAI API key:\n")
Basic Usageβ
For basic creation and usage of a tool-calling ReAct-style agent, the functionality is the same. First, let's define a model and tool(s), then we'll use those to create an agent.
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")
@tool
def magic_function(input: int) -> int:
"""Applies a magic function to an input."""
return input + 2
tools = [magic_function]
query = "what is the value of magic_function(3)?"
For the LangChain AgentExecutor, we define a prompt with a placeholder for the agent's scratchpad. The agent can be invoked as follows:
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful assistant"),
("human", "{input}"),
# Placeholders fill up a **list** of messages
("placeholder", "{agent_scratchpad}"),
]
)
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools)
agent_executor.invoke({"input": query})
{'input': 'what is the value of magic_function(3)?',
'output': 'The value of `magic_function(3)` is 5.'}
LangGraph's react agent executor manages a state that is defined by a list of messages. It will continue to process the list until there are no tool calls in the agent's output. To kick it off, we input a list of messages. The output will contain the entire state of the graph-- in this case, the conversation history.
from langgraph.prebuilt import create_react_agent
app = create_react_agent(model, tools)
messages = app.invoke({"messages": [("human", query)]})
{
"input": query,
"output": messages["messages"][-1].content,
}
{'input': 'what is the value of magic_function(3)?',
'output': 'The value of `magic_function(3)` is 5.'}
message_history = messages["messages"]
new_query = "Pardon?"
messages = app.invoke({"messages": message_history + [("human", new_query)]})
{
"input": new_query,
"output": messages["messages"][-1].content,
}
{'input': 'Pardon?',
'output': 'The value returned by `magic_function` when the input is 3 is 5.'}
Prompt Templatesβ
With legacy LangChain agents you have to pass in a prompt template. You can use this to control the agent.
With LangGraph react agent executor, by default there is no prompt. You can achieve similar control over the agent in a few ways:
- Pass in a system message as input
- Initialize the agent with a system message
- Initialize the agent with a function to transform messages before passing to the model.
Let's take a look at all of these below. We will pass in custom instructions to get the agent to respond in Spanish.
First up, using AgentExecutor
:
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful assistant. Respond only in Spanish."),
("human", "{input}"),
# Placeholders fill up a **list** of messages
("placeholder", "{agent_scratchpad}"),
]
)
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools)
agent_executor.invoke({"input": query})
{'input': 'what is the value of magic_function(3)?',
'output': 'El valor de `magic_function(3)` es 5.'}
Now, let's pass a custom system message to react agent executor.
LangGraph's prebuilt create_react_agent
does not take a prompt template directly as a parameter, but instead takes a state_modifier
parameter. This modifies the graph state before the llm is called, and can be one of four values:
- A
SystemMessage
, which is added to the beginning of the list of messages. - A
string
, which is converted to aSystemMessage
and added to the beginning of the list of messages. - A
Callable
, which should take in full graph state. The output is then passed to the language model. - Or a
Runnable
, which should take in full graph state. The output is then passed to the language model.
Here's how it looks in action:
from langchain_core.messages import SystemMessage
from langgraph.prebuilt import create_react_agent
system_message = "You are a helpful assistant. Respond only in Spanish."
# This could also be a SystemMessage object
# system_message = SystemMessage(content="You are a helpful assistant. Respond only in Spanish.")
app = create_react_agent(model, tools, state_modifier=system_message)
messages = app.invoke({"messages": [("user", query)]})
We can also pass in an arbitrary function. This function should take in a list of messages and output a list of messages. We can do all types of arbitrary formatting of messages here. In this cases, let's just add a SystemMessage to the start of the list of messages.
from langgraph.prebuilt import create_react_agent
from langgraph.prebuilt.chat_agent_executor import AgentState
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful assistant. Respond only in Spanish."),
("placeholder", "{messages}"),
]
)
def _modify_state_messages(state: AgentState):
return prompt.invoke({"messages": state["messages"]}).to_messages() + [
("user", "Also say 'Pandamonium!' after the answer.")
]
app = create_react_agent(model, tools, state_modifier=_modify_state_messages)
messages = app.invoke({"messages": [("human", query)]})
print(
{
"input": query,
"output": messages["messages"][-1].content,
}
)
{'input': 'what is the value of magic_function(3)?', 'output': 'The value of magic_function(3) is 5. Β‘Pandamonium!'}
Memoryβ
In LangChainβ
With LangChain's AgentExecutor, you could add chat Memory so it can engage in a multi-turn conversation.
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")
memory = InMemoryChatMessageHistory(session_id="test-session")
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful assistant."),
# First put the history
("placeholder", "{chat_history}"),
# Then the new input
("human", "{input}"),
# Finally the scratchpad
("placeholder", "{agent_scratchpad}"),
]
)
@tool
def magic_function(input: int) -> int:
"""Applies a magic function to an input."""
return input + 2
tools = [magic_function]
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools)
agent_with_chat_history = RunnableWithMessageHistory(
agent_executor,
# This is needed because in most real world scenarios, a session id is needed
# It isn't really used here because we are using a simple in memory ChatMessageHistory
lambda session_id: memory,
input_messages_key="input",
history_messages_key="chat_history",
)
config = {"configurable": {"session_id": "test-session"}}
print(
agent_with_chat_history.invoke(
{"input": "Hi, I'm polly! What's the output of magic_function of 3?"}, config
)["output"]
)
print("---")
print(agent_with_chat_history.invoke({"input": "Remember my name?"}, config)["output"])
print("---")
print(
agent_with_chat_history.invoke({"input": "what was that output again?"}, config)[
"output"
]
)
Hi Polly! The output of applying the magic function to the input 3 is 5.
---
Yes, you mentioned your name is Polly.
---
The output of applying the magic function to the input 3 is 5.
In LangGraphβ
Memory is just persistence, aka checkpointing.
Add a checkpointer
to the agent and you get chat memory for free.
from langgraph.checkpoint.memory import MemorySaver # an in-memory checkpointer
from langgraph.prebuilt import create_react_agent
system_message = "You are a helpful assistant."
# This could also be a SystemMessage object
# system_message = SystemMessage(content="You are a helpful assistant. Respond only in Spanish.")
memory = MemorySaver()
app = create_react_agent(
model, tools, state_modifier=system_message, checkpointer=memory
)
config = {"configurable": {"thread_id": "test-thread"}}
print(
app.invoke(
{
"messages": [
("user", "Hi, I'm polly! What's the output of magic_function of 3?")
]
},
config,
)["messages"][-1].content
)
print("---")
print(
app.invoke({"messages": [("user", "Remember my name?")]}, config)["messages"][
-1
].content
)
print("---")
print(
app.invoke({"messages": [("user", "what was that output again?")]}, config)[
"messages"
][-1].content
)
Hi Polly! The output of applying the magic function to the input 3 is 5.
---
Yes, your name is Polly!
---
The output of applying the magic function to the input 3 was 5.