Editing graph state¶
Review¶
We discussed motivations for human-in-the-loop:
(1) Approval - We can interrupt our agent, surface state to a user, and allow the user to accept an action
(2) Debugging - We can rewind the graph to reproduce or avoid issues
(3) Editing - You can modify the state
We showed how breakpoints support user approval, but don't yet know how to modify our graph state once our graph is interrupted!
Goals¶
Now, let's show how to directly edit the graph state and insert human feedback.
from dotenv import load_dotenv
load_dotenv()
%%capture --no-stderr
%pip install --quiet -U langgraph langchain_deepseek langgraph_sdk langgraph-prebuilt
Editing state¶
Previously, we introduced breakpoints.
We used them to interrupt the graph and await user approval before executing the next node.
But breakpoints are also opportunities to modify the graph state.
Let's set up our agent with a breakpoint before the assistant node.
from langchain_deepseek import ChatDeepSeek
def multiply(a: int, b: int) -> int:
"""Multiply a and b.
Args:
a: first int
b: second int
"""
return a * b
# This will be a tool
def add(a: int, b: int) -> int:
"""Adds a and b.
Args:
a: first int
b: second int
"""
return a + b
def divide(a: int, b: int) -> float:
"""Divide a by b.
Args:
a: first int
b: second int
"""
return a / b
tools = [add, multiply, divide]
llm = ChatDeepSeek(model="deepseek-v4-pro")
llm_with_tools = llm.bind_tools(tools)
from IPython.display import Image, display
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import MessagesState
from langgraph.graph import START, StateGraph
from langgraph.prebuilt import tools_condition, ToolNode
from langchain_core.messages import HumanMessage, SystemMessage
# System message
sys_msg = SystemMessage(content="You are a helpful assistant tasked with performing arithmetic on a set of inputs.")
# Node
def assistant(state: MessagesState):
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
# Graph
builder = StateGraph(MessagesState)
# Define nodes: these do the work
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
# Define edges: these determine the control flow
builder.add_edge(START, "assistant")
builder.add_conditional_edges(
"assistant",
# If the latest message (result) from assistant is a tool call -> tools_condition routes to tools
# If the latest message (result) from assistant is a not a tool call -> tools_condition routes to END
tools_condition,
)
builder.add_edge("tools", "assistant")
memory = MemorySaver()
graph = builder.compile(interrupt_before=["assistant"], checkpointer=memory)
# Show
display(Image(graph.get_graph(xray=True).draw_mermaid_png()))
Let's run!
We can see the graph is interrupted before the chat model responds.
# Input
initial_input = {"messages": "Multiply 2 and 3"}
# Thread
thread = {"configurable": {"thread_id": "1"}}
# Run the graph until the first interruption
for event in graph.stream(initial_input, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
================================ Human Message =================================
Multiply 2 and 3
state = graph.get_state(thread)
state
StateSnapshot(values={'messages': [HumanMessage(content='Multiply 2 and 3', id='e7edcaba-bfed-4113-a85b-25cc39d6b5a7')]}, next=('assistant',), config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6a412-5b2d-601a-8000-4af760ea1d0d'}}, metadata={'source': 'loop', 'writes': None, 'step': 0, 'parents': {}}, created_at='2024-09-03T22:09:10.966883+00:00', parent_config={'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1ef6a412-5b28-6ace-bfff-55d7a2c719ae'}}, tasks=(PregelTask(id='dbee122a-db69-51a7-b05b-a21fab160696', name='assistant', error=None, interrupts=(), state=None),))
Now, we can directly apply a state update.
Remember, updates to the messages key will use the add_messages reducer:
- If we want to over-write the existing message, we can supply the message
id. - If we simply want to append to our list of messages, then we can pass a message without an
idspecified, as shown below.
graph.update_state(
thread,
{"messages": [HumanMessage(content="No, actually multiply 3 and 3!")]},
)
{'configurable': {'thread_id': '1',
'checkpoint_ns': '',
'checkpoint_id': '1ef6a414-f419-6182-8001-b9e899eca7e5'}}
Let's have a look.
We called update_state with a new message.
The add_messages reducer appends it to our state key, messages.
new_state = graph.get_state(thread).values
for m in new_state['messages']:
m.pretty_print()
================================ Human Message ================================= Multiply 2 and 3 ================================ Human Message ================================= No, actually multiply 3 and 3!
Now, let's proceed with our agent, simply by passing None and allowing it proceed from the current state.
We emit the current and then proceed to execute the remaining nodes.
for event in graph.stream(None, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
================================ Human Message ================================= No, actually multiply 3 and 3! ================================== Ai Message ================================== Tool Calls: multiply (call_Mbu8MfA0krQh8rkZZALYiQMk) Call ID: call_Mbu8MfA0krQh8rkZZALYiQMk Args: a: 3 b: 3 ================================= Tool Message ================================= Name: multiply 9
Now, we're back at the assistant, which has our breakpoint.
We can again pass None to proceed.
for event in graph.stream(None, thread, stream_mode="values"):
event['messages'][-1].pretty_print()
================================= Tool Message ================================= Name: multiply 9 ================================== Ai Message ================================== 3 multiplied by 3 equals 9.
Editing graph state in Studio¶
⚠️ Notice
Since filming these videos, we've updated Studio so that it can now be run locally and accessed through your browser. This is the preferred way to run Studio instead of using the Desktop App shown in the video. It is now called LangSmith Studio instead of LangGraph Studio. Detailed setup instructions are available in the "Getting Setup" guide at the start of the course. You can find a description of Studio here, and specific details for local deployment here.
To start the local development server, run the following command in your terminal in the /studio directory in this module:
langgraph dev
You should see the following output:
- 🚀 API: http://127.0.0.1:2024
- 🎨 Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
- 📚 API Docs: http://127.0.0.1:2024/docs
Open your browser and navigate to the Studio UI URL shown above.
The LangGraph API supports editing graph state.
if 'google.colab' in str(get_ipython()):
raise Exception("Unfortunately LangGraph Studio is currently not supported on Google Colab")
# This is the URL of the local development server
from langgraph_sdk import get_client
client = get_client(url="http://127.0.0.1:2024")
Our agent is defined in studio/agent.py.
If you look at the code, you'll see that it does not have a breakpoint!
Of course, we can add it to agent.py, but one very nice feature of the API is that we can pass in a breakpoint!
Here, we pass a interrupt_before=["assistant"].
initial_input = {"messages": "Multiply 2 and 3"}
thread = await client.threads.create()
async for chunk in client.runs.stream(
thread["thread_id"],
"agent",
input=initial_input,
stream_mode="values",
interrupt_before=["assistant"],
):
print(f"Receiving new event of type: {chunk.event}...")
messages = chunk.data.get('messages', [])
if messages:
print(messages[-1])
print("-" * 50)
Receiving new event of type: metadata...
--------------------------------------------------
Receiving new event of type: values...
{'content': 'Multiply 2 and 3', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': '882dabe4-b877-4d71-bd09-c34cb97c4f46', 'example': False}
--------------------------------------------------
We can get the current state
current_state = await client.threads.get_state(thread['thread_id'])
current_state
{'values': {'messages': [{'content': 'Multiply 2 and 3',
'additional_kwargs': {},
'response_metadata': {},
'type': 'human',
'name': None,
'id': '882dabe4-b877-4d71-bd09-c34cb97c4f46',
'example': False}]},
'next': ['assistant'],
'tasks': [{'id': 'a71c0b80-a679-57cb-aa59-a1655b763480',
'name': 'assistant',
'error': None,
'interrupts': [],
'state': None}],
'metadata': {'step': 0,
'run_id': '1ef6a41c-ea63-663f-b3e8-4f001bf0bf53',
'source': 'loop',
'writes': None,
'parents': {},
'user_id': '',
'graph_id': 'agent',
'thread_id': 'a95ffa54-2435-4a47-a9da-e886369ca8ee',
'created_by': 'system',
'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'},
'created_at': '2024-09-03T22:13:54.466695+00:00',
'checkpoint_id': '1ef6a41c-ead7-637b-8000-8c6a7b98379e',
'parent_checkpoint_id': '1ef6a41c-ead3-637d-bfff-397ebdb4f2ea'}
We can look at the last message in state.
last_message = current_state['values']['messages'][-1]
last_message
{'content': 'Multiply 2 and 3',
'additional_kwargs': {},
'response_metadata': {},
'type': 'human',
'name': None,
'id': '882dabe4-b877-4d71-bd09-c34cb97c4f46',
'example': False}
We can edit it!
last_message['content'] = "No, actually multiply 3 and 3!"
last_message
{'content': 'No, actually multiply 3 and 3!',
'additional_kwargs': {},
'response_metadata': {},
'type': 'human',
'name': None,
'id': '882dabe4-b877-4d71-bd09-c34cb97c4f46',
'example': False}
last_message
{'content': 'No, actually multiply 3 and 3!',
'additional_kwargs': {},
'response_metadata': {},
'type': 'human',
'name': None,
'id': '882dabe4-b877-4d71-bd09-c34cb97c4f46',
'example': False}
Remember, as we said before, updates to the messages key will use the same add_messages reducer.
If we want to over-write the existing message, then we can supply the message id.
Here, we did that. We only modified the message content, as shown above.
await client.threads.update_state(thread['thread_id'], {"messages": last_message})
{'configurable': {'thread_id': 'a95ffa54-2435-4a47-a9da-e886369ca8ee',
'checkpoint_ns': '',
'checkpoint_id': '1ef6a41d-cc8e-6979-8001-8c7c283b636c'},
'checkpoint_id': '1ef6a41d-cc8e-6979-8001-8c7c283b636c'}
Now, we resume by passing None.
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id="agent",
input=None,
stream_mode="values",
interrupt_before=["assistant"],
):
print(f"Receiving new event of type: {chunk.event}...")
messages = chunk.data.get('messages', [])
if messages:
print(messages[-1])
print("-" * 50)
We get the result of the tool call as 9, as expected.
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id="agent",
input=None,
stream_mode="values",
interrupt_before=["assistant"],
):
print(f"Receiving new event of type: {chunk.event}...")
messages = chunk.data.get('messages', [])
if messages:
print(messages[-1])
print("-" * 50)
Awaiting user input¶
So, it's clear that we can edit our agent state after a breakpoint.
Now, what if we want to allow for human feedback to perform this state update?
We'll add a node that serves as a placeholder for human feedback within our agent.
This human_feedback node allow the user to add feedback directly to state.
We specify the breakpoint using interrupt_before our human_feedback node.
We set up a checkpointer to save the state of the graph up until this node.
# System message
sys_msg = SystemMessage(content="You are a helpful assistant tasked with performing arithmetic on a set of inputs.")
# no-op node that should be interrupted on
def human_feedback(state: MessagesState):
pass
# Assistant node
def assistant(state: MessagesState):
return {"messages": [llm_with_tools.invoke([sys_msg] + state["messages"])]}
# Graph
builder = StateGraph(MessagesState)
# Define nodes: these do the work
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
builder.add_node("human_feedback", human_feedback)
# Define edges: these determine the control flow
builder.add_edge(START, "human_feedback")
builder.add_edge("human_feedback", "assistant")
builder.add_conditional_edges(
"assistant",
# If the latest message (result) from assistant is a tool call -> tools_condition routes to tools
# If the latest message (result) from assistant is a not a tool call -> tools_condition routes to END
tools_condition,
)
builder.add_edge("tools", "human_feedback")
memory = MemorySaver()
graph = builder.compile(interrupt_before=["human_feedback"], checkpointer=memory)
display(Image(graph.get_graph().draw_mermaid_png()))
We will get feedback from the user.
We use .update_state to update the state of the graph with the human response we get, as before.
We use the as_node="human_feedback" parameter to apply this state update as the specified node, human_feedback.
# Input
initial_input = {"messages": "Multiply 2 and 3"}
# Thread
thread = {"configurable": {"thread_id": "5"}}
# Run the graph until the first interruption
for event in graph.stream(initial_input, thread, stream_mode="values"):
event["messages"][-1].pretty_print()
# Get user input
user_input = input("Tell me how you want to update the state: ")
# We now update the state as if we are the human_feedback node
graph.update_state(thread, {"messages": user_input}, as_node="human_feedback")
# Continue the graph execution
for event in graph.stream(None, thread, stream_mode="values"):
event["messages"][-1].pretty_print()
================================ Human Message ================================= Multiply 2 and 3 ================================ Human Message ================================= no, multiply 3 and 3 ================================== Ai Message ================================== Tool Calls: multiply (call_sewrDyCrAJBQQecusUoT6OJ6) Call ID: call_sewrDyCrAJBQQecusUoT6OJ6 Args: a: 3 b: 3 ================================= Tool Message ================================= Name: multiply 9
# Continue the graph execution
for event in graph.stream(None, thread, stream_mode="values"):
event["messages"][-1].pretty_print()
================================= Tool Message ================================= Name: multiply 9 ================================== Ai Message ================================== The result of multiplying 3 and 3 is 9.