Streaming¶
Review¶
In module 2, covered a few ways to customize graph state and memory.
We built up to a Chatbot with external memory that can sustain long-running conversations.
Goals¶
This module will dive into human-in-the-loop, which builds on memory and allows users to interact directly with graphs in various ways.
To set the stage for human-in-the-loop, we'll first dive into streaming, which provides several ways to visualize graph output (e.g., node state or chat model tokens) over the course of execution.
from dotenv import load_dotenv
load_dotenv()
%%capture --no-stderr
%pip install --quiet -U langgraph langchain_deepseek langgraph_sdk
Streaming¶
LangGraph is built with first class support for streaming.
Let's set up our Chatbot from Module 2, and show various way to stream outputs from the graph during execution.
Note that we use RunnableConfig with call_model to enable token-wise streaming. This is only needed with python < 3.11. We include in case you are running this notebook in CoLab, which will use python 3.x.
from IPython.display import Image, display
from typing import Literal
from langchain_deepseek import ChatDeepSeek
from langchain_core.messages import SystemMessage, HumanMessage, RemoveMessage
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.graph import MessagesState
# LLM
model = ChatDeepSeek(model="deepseek-v4-pro", temperature=0)
# State
class State(MessagesState):
summary: str
# Define the logic to call the model
def call_model(state: State, config: RunnableConfig):
# Get summary if it exists
summary = state.get("summary", "")
# If there is summary, then we add it
if summary:
# Add summary to system message
system_message = f"Summary of conversation earlier: {summary}"
# Append summary to any newer messages
messages = [SystemMessage(content=system_message)] + state["messages"]
else:
messages = state["messages"]
response = model.invoke(messages, config)
return {"messages": response}
def summarize_conversation(state: State):
# First, we get any existing summary
summary = state.get("summary", "")
# Create our summarization prompt
if summary:
# A summary already exists
summary_message = (
f"This is summary of the conversation to date: {summary}\n\n"
"Extend the summary by taking into account the new messages above:"
)
else:
summary_message = "Create a summary of the conversation above:"
# Add prompt to our history
messages = state["messages"] + [HumanMessage(content=summary_message)]
response = model.invoke(messages)
# Delete all but the 2 most recent messages
delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {"summary": response.content, "messages": delete_messages}
# Determine whether to end or summarize the conversation
def should_continue(state: State)-> Literal ["summarize_conversation",END]:
"""Return the next node to execute."""
messages = state["messages"]
# If there are more than six messages, then we summarize the conversation
if len(messages) > 6:
return "summarize_conversation"
# Otherwise we can just end
return END
# Define a new graph
workflow = StateGraph(State)
workflow.add_node("conversation", call_model)
workflow.add_node(summarize_conversation)
# Set the entrypoint as conversation
workflow.add_edge(START, "conversation")
workflow.add_conditional_edges("conversation", should_continue)
workflow.add_edge("summarize_conversation", END)
# Compile
memory = MemorySaver()
graph = workflow.compile(checkpointer=memory)
display(Image(graph.get_graph().draw_mermaid_png()))
Streaming full state¶
Now, let's talk about ways to stream our graph state.
.stream and .astream are sync and async methods for streaming back results.
LangGraph supports a few different streaming modes for graph state.
values: This streams the full state of the graph after each node is called.updates: This streams updates to the state of the graph after each node is called.

Let's look at stream_mode="updates".
Because we stream with updates, we only see updates to the state after node in the graph is run.
Each chunk is a dict with node_name as the key and the updated state as the value.
# Create a thread
config = {"configurable": {"thread_id": "1"}}
# Start conversation
for chunk in graph.stream({"messages": [HumanMessage(content="hi! I'm Lance")]}, config, stream_mode="updates"):
print(chunk)
Let's now just print the state update.
# Start conversation
for chunk in graph.stream({"messages": [HumanMessage(content="hi! I'm Lance")]}, config, stream_mode="updates"):
chunk['conversation']["messages"].pretty_print()
================================== Ai Message ==================================
Hi Lance! How's it going? What can I do for you today?
Now, we can see stream_mode="values".
This is the full state of the graph after the conversation node is called.
# Start conversation, again
config = {"configurable": {"thread_id": "2"}}
# Start conversation
input_message = HumanMessage(content="hi! I'm Lance")
for event in graph.stream({"messages": [input_message]}, config, stream_mode="values"):
for m in event['messages']:
m.pretty_print()
print("---"*25)
================================ Human Message ================================= hi! I'm Lance --------------------------------------------------------------------------- ================================ Human Message ================================= hi! I'm Lance ================================== Ai Message ================================== Hello, Lance! How can I assist you today? ---------------------------------------------------------------------------
Streaming tokens¶
We often want to stream more than graph state.
In particular, with chat model calls it is common to stream the tokens as they are generated.
We can do this using the .astream_events method, which streams back events as they happen inside nodes!
Each event is a dict with a few keys:
event: This is the type of event that is being emitted.name: This is the name of event.data: This is the data associated with the event.metadata: Containslanggraph_node, the node emitting the event.
Let's have a look.
config = {"configurable": {"thread_id": "3"}}
input_message = HumanMessage(content="Tell me about the 49ers NFL team")
async for event in graph.astream_events({"messages": [input_message]}, config, version="v2"):
print(f"Node: {event['metadata'].get('langgraph_node','')}. Type: {event['event']}. Name: {event['name']}")
The central point is that tokens from chat models within your graph have the on_chat_model_stream type.
We can use event['metadata']['langgraph_node'] to select the node to stream from.
And we can use event['data'] to get the actual data for each event, which in this case is an AIMessageChunk.
node_to_stream = 'conversation'
config = {"configurable": {"thread_id": "4"}}
input_message = HumanMessage(content="Tell me about the 49ers NFL team")
async for event in graph.astream_events({"messages": [input_message]}, config, version="v2"):
# Get chat model tokens from a particular node
if event["event"] == "on_chat_model_stream" and event['metadata'].get('langgraph_node','') == node_to_stream:
print(event["data"])
As you see above, just use the chunk key to get the AIMessageChunk.
config = {"configurable": {"thread_id": "5"}}
input_message = HumanMessage(content="Tell me about the 49ers NFL team")
async for event in graph.astream_events({"messages": [input_message]}, config, version="v2"):
# Get chat model tokens from a particular node
if event["event"] == "on_chat_model_stream" and event['metadata'].get('langgraph_node','') == node_to_stream:
data = event["data"]
print(data["chunk"].content, end="|")
|The| San| Francisco| |49|ers| are| a| professional| American| football| team| based| in| the| San| Francisco| Bay| Area|.| They| compete| in| the| National| Football| League| (|NFL|)| as| a| member| club| of| the| league|'s| National| Football| Conference| (|N|FC|)| West| division|.| The| team| was| founded| in| |194|6| as| a| charter| member| of| the| All|-Amer|ica| Football| Conference| (|AA|FC|)| and| joined| the| NFL| in| |194|9| when| the| leagues| merged|. |The| |49|ers| have| a| rich| history| and| are| one| of| the| most| successful| teams| in| NFL| history|.| They| have| won| five| Super| Bowl| championships|,| with| victories| in| Super| Bow|ls| XVI|,| XIX|,| XX|III|,| XX|IV|,| and| XX|IX|.| The| team| has| also| won| numerous| division| titles| and| made| several| playoff| appearances|. |The| |49|ers| are| known| for| their| iconic| players| and| coaches|,| including| Hall| of| Fam|ers| like| Joe| Montana|,| Jerry| Rice|,| Steve| Young|,| Ronnie| L|ott|,| and| coach| Bill| Walsh|,| who| is| credited| with| popular|izing| the| West| Coast| offense|.| The| team's| colors| are| red| and| gold|,| and| they| play| their| home| games| at| Levi|'s| Stadium| in| Santa| Clara|,| California|,| which| they| moved| to| in| |201|4| after| previously| playing| at| Cand|lestick| Park| in| San| Francisco|. |The| |49|ers| have| a| passionate| fan| base| and| a| stor|ied| rivalry| with| teams| like| the| Dallas| Cowboys|,| Green| Bay| Packers|,| and| Seattle| Seahawks|.| The| team's| success| in| the| |198|0|s| and| |199|0|s|,| particularly| under| the| leadership| of| Bill| Walsh| and| George| Se|if|ert|,| helped| establish| them| as| one| of| the| premier| franchises| in| the| NFL|.||||
Streaming with LangGraph API¶
⚠️ 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")
from langgraph_sdk import get_client
# This is the URL of the local development server
URL = "http://127.0.0.1:2024"
client = get_client(url=URL)
# Search all hosted graphs
assistants = await client.assistants.search()
Let's stream values, like before.
# Create a new thread
thread = await client.threads.create()
# Input message
input_message = HumanMessage(content="Multiply 2 and 3")
async for event in client.runs.stream(thread["thread_id"],
assistant_id="agent",
input={"messages": [input_message]},
stream_mode="values"):
print(event)
The streamed objects have:
event: Typedata: State
from langchain_core.messages import convert_to_messages
thread = await client.threads.create()
input_message = HumanMessage(content="Multiply 2 and 3")
async for event in client.runs.stream(thread["thread_id"], assistant_id="agent", input={"messages": [input_message]}, stream_mode="values"):
messages = event.data.get('messages',None)
if messages:
print(convert_to_messages(messages)[-1])
print('='*25)
There are some new streaming mode that are only supported via the API.
For example, we can use messages mode to better handle the above case!
This mode currently assumes that you have a messages key in your graph, which is a list of messages.
All events emitted using messages mode have two attributes:
event: This is the name of the eventdata: This is data associated with the event
thread = await client.threads.create()
input_message = HumanMessage(content="Multiply 2 and 3")
async for event in client.runs.stream(thread["thread_id"],
assistant_id="agent",
input={"messages": [input_message]},
stream_mode="messages"):
print(event.event)
metadata messages/metadata messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/metadata messages/complete messages/metadata messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial messages/partial
We can see a few events:
metadata: metadata about the runmessages/complete: fully formed messagemessages/partial: chat model tokens
Now, let's show how to stream these messages.
We'll define a helper function for better formatting of the tool calls in messages.
thread = await client.threads.create()
input_message = HumanMessage(content="Multiply 2 and 3")
def format_tool_calls(tool_calls):
"""
Format a list of tool calls into a readable string.
Args:
tool_calls (list): A list of dictionaries, each representing a tool call.
Each dictionary should have 'id', 'name', and 'args' keys.
Returns:
str: A formatted string of tool calls, or "No tool calls" if the list is empty.
"""
if tool_calls:
formatted_calls = []
for call in tool_calls:
formatted_calls.append(
f"Tool Call ID: {call['id']}, Function: {call['name']}, Arguments: {call['args']}"
)
return "\n".join(formatted_calls)
return "No tool calls"
async for event in client.runs.stream(
thread["thread_id"],
assistant_id="agent",
input={"messages": [input_message]},
stream_mode="messages",):
# Handle metadata events
if event.event == "metadata":
print(f"Metadata: Run ID - {event.data['run_id']}")
print("-" * 50)
# Handle partial message events
elif event.event == "messages/partial":
for data_item in event.data:
# Process user messages
if "role" in data_item and data_item["role"] == "user":
print(f"Human: {data_item['content']}")
else:
# Extract relevant data from the event
tool_calls = data_item.get("tool_calls", [])
invalid_tool_calls = data_item.get("invalid_tool_calls", [])
content = data_item.get("content", "")
response_metadata = data_item.get("response_metadata", {})
if content:
print(f"AI: {content}")
if tool_calls:
print("Tool Calls:")
print(format_tool_calls(tool_calls))
if invalid_tool_calls:
print("Invalid Tool Calls:")
print(format_tool_calls(invalid_tool_calls))
if response_metadata and response_metadata.get("finish_reason"):
print(f"Response Metadata: Finish Reason - {response_metadata['finish_reason']}")
print("-" * 50)
Metadata: Run ID - 019a0358-57dc-76f9-bc63-633eee467a86
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {}
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {}
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {}
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {}
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {'a': 2}
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {'a': 2}
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {'a': 2}
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {'a': 2}
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {'a': 2, 'b': 3}
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {'a': 2, 'b': 3}
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {'a': 2, 'b': 3}
Response Metadata: Finish Reason - tool_calls
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {'a': 2, 'b': 3}
Response Metadata: Finish Reason - tool_calls
--------------------------------------------------
Tool Calls:
Tool Call ID: call_mDtKBiuzkpN5ITWykhEFN0XU, Function: multiply, Arguments: {'a': 2, 'b': 3}
Response Metadata: Finish Reason - tool_calls
--------------------------------------------------
--------------------------------------------------
AI: The
--------------------------------------------------
AI: The result
--------------------------------------------------
AI: The result of
--------------------------------------------------
AI: The result of multiplying
--------------------------------------------------
AI: The result of multiplying
--------------------------------------------------
AI: The result of multiplying 2
--------------------------------------------------
AI: The result of multiplying 2 and
--------------------------------------------------
AI: The result of multiplying 2 and
--------------------------------------------------
AI: The result of multiplying 2 and 3
--------------------------------------------------
AI: The result of multiplying 2 and 3 is
--------------------------------------------------
AI: The result of multiplying 2 and 3 is
--------------------------------------------------
AI: The result of multiplying 2 and 3 is 6
--------------------------------------------------
AI: The result of multiplying 2 and 3 is 6.
--------------------------------------------------
AI: The result of multiplying 2 and 3 is 6.
Response Metadata: Finish Reason - stop
--------------------------------------------------
AI: The result of multiplying 2 and 3 is 6.
Response Metadata: Finish Reason - stop
--------------------------------------------------
AI: The result of multiplying 2 and 3 is 6.
Response Metadata: Finish Reason - stop
--------------------------------------------------