Files
langgraph/examples/introduction.ipynb
T
2024-04-18 09:04:08 -07:00

224 KiB

Introduction to LangGraph

In this tutorial, we will build a support chatbot in LangGraph that can:

  • Answer common questions by searching the web
  • Maintain conversation state across calls
  • Route complex queries to a human for review
  • Use custom state to control its behavior
  • Rewind and explore alternative conversation paths

We'll start with a basic chatbot and progressively add more sophisticated capabilities, introducing key LangGraph concepts along the way.

Setup

First, install the required packages:

In [ ]:
%%capture --no-stderr
%pip install -U langgraph langsmith

# Used for this tutorial; not a requirement for LangGraph
%pip install -U langchain langchain_anthropic

Next, set your API keys:

In [1]:
import getpass
import os


def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")


_set_env("ANTHROPIC_API_KEY")

(Encouraged) LangSmith makes it a lot easier to see what's going on "under the hood."

In [2]:
_set_env("LANGSMITH_API_KEY")
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_PROJECT"] = "LangGraph Tutorial"

Part 1: Build a Basic Chatbot

We'll first create a simple chatbot using LangGraph. This chatbot will respond directly to user messages. Though simple, it will illustrate the core concepts of building with LangGraph. By the end of this section, you will have a built rudimentary chatbot.

Start by creating a StateGraph. A StateGraph object defines the structure of our chatbot as a "state machine". We'll add nodes to represent the llm and functions our chatbot can call and edges to specify how the bot should transition between these functions.

In [3]:
from typing import Annotated

from typing_extensions import TypedDict

from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages


class State(TypedDict):
    # Messages have the type "list". The `add_messages` function
    # in the annotation defines how this state key should be updated
    # (in this case, it appends messages to the list, rather than overwriting them)
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)

Notice that we've defined our State as a TypedDict with a single key: messages. The messages key is annotated with the add_messages function, which tells LangGraph to append new messages to the existing list, rather than overwriting it.

So now our graph knows two things:

  1. Every node we define will receive the current State as input and return a value that updates that state.
  2. messages will be appended to the current list, rather than directly overwritten. This is communicated via the prebuilt add_messages function in the Annotated syntax.

Next, add a "chatbot" node. Nodes represent units of work. They are typically regular python functions.

In [4]:
from langchain_anthropic import ChatAnthropic

llm = ChatAnthropic(model="claude-3-haiku-20240307")


def chatbot(state: State):
    return {"messages": [llm.invoke(state["messages"])]}


# The first argument is the unique node name
# The second argument is the function or object that will be called whenever
# the node is used.
graph_builder.add_node("chatbot", chatbot)

Notice how the chatbot node function takes the current State as input and returns an updated messages list. This is the basic pattern for all LangGraph node functions.

The add_messages function in our State will append the llm's response messages to whatever messages are already in the state.

Next, add an entry point. This tells our graph where to start its work each time we run it.

In [5]:
graph_builder.set_entry_point("chatbot")

Similarly, set a finish point. This instructs the graph "any time this node is run, you can exit."

In [6]:
graph_builder.set_finish_point("chatbot")

Finally, we'll want to be able to run our graph. To do so, call "compile()" on the graph builder. This creates a "CompiledGraph" we can use invoke on our state.

In [7]:
graph = graph_builder.compile()

You can visualize the graph using the get_graph method and one of the "draw" methods, like draw_ascii or draw_png. The draw methods each require additional dependencies.

In [8]:
from IPython.display import Image, display

try:
    display(Image(graph.get_graph().draw_mermaid_png()))
except:
    # This requires some extra dependencies and is optional
    pass

Now let's run the chatbot!

Tip: You can exit the chat loop at any time by typing "quit", "exit", or "q".

In [9]:
while True:
    user_input = input("User: ")
    if user_input.lower() in ["quit", "exit", "q"]:
        print("Goodbye!")
        break
    for event in graph.stream({"messages": ("user", user_input)}):
        for value in event.values():
            print("Assistant:", value["messages"][-1].content)
User:  Hi there, I'm Will!
Assistant: It's nice to meet you, Will! I'm Claude, an AI assistant created by Anthropic. I'm here to help with any questions or tasks you might have. Please let me know if there's anything I can assist you with.
User:  What's my name?
Assistant: I'm afraid I don't actually know your name. As an AI assistant, I don't have specific information about you or other individual users. I can only respond based on the context provided to me during our conversation.
User:  q
Goodbye!

Congratulations! You've built your first chatbot using LangGraph. This bot can engage in basic conversation by taking user input and generating responses using an LLM. You can inspect a LangSmith Trace for the call above at the provided link.

However, you may have noticed that the bot's knowledge is limited to what's in its training data. In the next part, we'll add a web search tool to expand the bot's knowledge and make it more capable.

Below is the full code for this section for your reference:

In [10]:
from typing import Annotated

from langchain_anthropic import ChatAnthropic
from typing_extensions import TypedDict

from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages


class State(TypedDict):
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)


llm = ChatAnthropic(model="claude-3-haiku-20240307")


def chatbot(state: State):
    return {"messages": [llm.invoke(state["messages"])]}


# The first argument is the unique node name
# The second argument is the function or object that will be called whenever
# the node is used.
graph_builder.add_node("chatbot", chatbot)
graph_builder.set_entry_point("chatbot")
graph_builder.set_finish_point("chatbot")
graph = graph_builder.compile()

Part 2: Enhancing the Chatbot with Tools

To handle queries our chatbot can't answer "from memory", we'll integrate a web search tool. Our bot can use this tool to find relevant information and provide better responses.

Requirements

Before we start, make sure you have the necessary packages installed and API keys set up:

First, install the requirements to use the Tavily Search Engine, and set your TAVILY_API_KEY.

In [ ]:
%%capture --no-stderr
%pip install -U tavily-python
In [11]:
_set_env("TAVILY_API_KEY")

Next, define the tool:

In [12]:
from langchain_community.tools.tavily_search import TavilySearchResults

tool = TavilySearchResults(max_results=2)
tools = [tool]
tool.invoke("What's a 'node' in LangGraph?")
Out [12]:
[{'url': 'https://medium.com/@cplog/introduction-to-langgraph-a-beginners-guide-14f9be027141',
  'content': 'Nodes: Nodes are the building blocks of your LangGraph. Each node represents a function or a computation step. You define nodes to perform specific tasks, such as processing input, making ...'},
 {'url': 'https://www.analyticsvidhya.com/blog/2024/03/build-an-ai-coding-agent-with-langgraph-by-langchain/',
  'content': 'LangGraph is an extension of LangChain, which allows us to build cyclic, stateful, multi-actor agent systems. It implements a graph structure with nodes and edges. The nodes are functions or tools, and the edges are the connections between nodes. Edges are of two types: conditional and normal.'}]

The results are page summaries our chat bot can use to answer questions.

Next, we'll start defining our graph. The following is all the same as in Part 1, except we have added bind_tools on our LLM. This lets the LLM know the correct JSON format to use if it wants to use our search engine.

In [13]:
from typing import Annotated

from langchain_anthropic import ChatAnthropic
from typing_extensions import TypedDict

from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages


class State(TypedDict):
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)


llm = ChatAnthropic(model="claude-3-haiku-20240307")
# Modification: tell the LLM which tools it can call
llm_with_tools = llm.bind_tools(tools)


def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}


graph_builder.add_node("chatbot", chatbot)
/Users/wfh/.pyenv/versions/3.11.2/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: The function `bind_tools` is in beta. It is actively being worked on, so the API may change.
  warn_beta(

Now we'll add the tools to a new node. The ToolNode is a helper class in LangGraph that invoke's the selected tools whenever the LLM responds with 1 or more tool calls. It relies on tool_calling support, which is available in Anthropic, OpenAI, Google Gemini, and a number of other LLM providers.

In [14]:
from langgraph.prebuilt import ToolNode

tool_node = ToolNode(tools=[tool])
graph_builder.add_node("action", tool_node)

With the tool node added, we can define the conditional_edges.

Recall that edges route the control flow from one node to the next. Conditional edges usually contain "if" statements to route to different nodes depending on the current graph state. These functions receive the current graph state and return a string or list of strings indicating which node(s) to call next.

Below, call add_conditional_edges to route from the chatbot node to either the action or "__end__" using the prebuilt tools_condition function. Then, create an edge from the action/tools node back to the chat bot to let it observe the response from our search engine and decide whether it has enough information to answer the user's question.

In [15]:
from langgraph.prebuilt import tools_condition

# The `tools_condition` function returns "action" if the chatbot asks to use a tool, and "__end__" if
# it is fine directly responding. This conditional routing defines the main agent loop.
graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
    # The following dictionary lets you tell the graph to interpret the condition's outputs as a specific node
    # It defaults to the identity function, but if you
    # want to use a node named something else apart from "action",
    # You can update the value of the dictionary to something else
    # e.g., "action": "my_tools"
    {"action": "action", "__end__": "__end__"},
)
# Any time a tool is called, we return to the chatbot to decide the next step
graph_builder.add_edge("action", "chatbot")
graph_builder.set_entry_point("chatbot")
graph = graph_builder.compile()

Notice that conditional edges start from a single node. This tells the graph "any time the 'chatbot' node runs, either go to 'action' if it calls a tool, or end the loop if it responds directly.

The prebuilt tools_condition returns the "__end__" string if no tool calls are made. When the graph transitions to __end__, it has no more tasks to complete and ceases execution. Because the condition can return __end__, we don't need to explicitly set a finish_point this time. Our graph already has a way to finish!

Let's visualize the graph we've built. The following function has some additional dependencies to run that are unimportant for this tutorial.

In [16]:
try:
    display(Image(graph.get_graph().draw_mermaid_png()))
except:
    # This requires some extra dependencies and is optional
    pass

Now we can ask the bot questions outside its training data.

In [18]:
from langchain_core.messages import BaseMessage

while True:
    user_input = input("User: ")
    if user_input.lower() in ["quit", "exit", "q"]:
        print("Goodbye!")
        break
    for event in graph.stream({"messages": [("user", user_input)]}):
        for value in event.values():
            if isinstance(value["messages"][-1], BaseMessage):
                print("Assistant:", value["messages"][-1].content)
User:  What sets langgraph apart?
Assistant: [{'id': 'toolu_01H54JZhgQMGzbKq54PoNQL6', 'input': {'query': 'langgraph'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
Assistant: [{"url": "https://github.com/langchain-ai/langgraph/blob/main/README.md", "content": "Define the nodes\nWe now need to define a few different nodes in our graph.\\nIn langgraph, a node can be either a function or a runnable.\\nThere are two main nodes we need for this:\nWe will also need to define some edges.\\nSome of these edges may be conditional.\\nThe reason they are conditional is that based on the output of a node, one of several paths may be taken.\\nThe path that is taken is not known until that node is run (the LLM decides).\n LangChain.\\nIt extends the LangChain Expression Language with the ability to coordinate multiple chains (or actors) across multiple steps of computation in a cyclic manner.\\nIt is inspired by Pregel and Apache Beam.\\nThe current interface exposed is one inspired by NetworkX.\nThe main use is for adding cycles to your LLM application.\\nCrucially, this is NOT a DAG framework.\\nIf you want to build a DAG, you should use just use LangChain Expression Language.\n This is a special node representing the end of the graph.\\nThis means that anything passed to this node will be the final output of the graph.\\nIt can be used in two places:\nWhen to Use\nWhen should you use this versus LangChain Expression Language?\n This method adds a node to the graph.\\nIt takes two arguments:\n.add_edge\nCreates an edge from one node to the next.\\nThis means that output of the first node will be passed to the next node.\\nIt takes two arguments.\n Assuming you have done the above Quick Start, you can build off it like:\nHere, we manually define the first tool call that we will make.\\nNotice that it does that same thing as agent would have done (adds the agent_outcome key).\\nThis is so that we can easily plug it in.\n"}, {"url": "https://blog.langchain.dev/langgraph-multi-agent-workflows/", "content": "As a part of the launch, we highlighted two simple runtimes: one that is the equivalent of the AgentExecutor in langchain, and a second that was a version of that aimed at message passing and chat models.\n It's important to note that these three examples are only a few of the possible examples we could highlight - there are almost assuredly other examples out there and we look forward to seeing what the community comes up with!\n LangGraph: Multi-Agent Workflows\nLinks\nLast week we highlighted LangGraph - a new package (available in both Python and JS) to better enable creation of LLM workflows containing cycles, which are a critical component of most agent runtimes. \"\nAnother key difference between Autogen and LangGraph is that LangGraph is fully integrated into the LangChain ecosystem, meaning you take fully advantage of all the LangChain integrations and LangSmith observability.\n As part of this launch, we're also excited to highlight a few applications built on top of LangGraph that utilize the concept of multiple agents.\n"}]
Assistant: Based on the search results, here are the key things that set LangGraph apart:

1. LangGraph extends the LangChain Expression Language by enabling the coordination of multiple "chains" or "actors" across multiple steps of computation in a cyclic manner. This allows for the creation of more complex workflows that involve cycles, rather than just directed acyclic graphs (DAGs).

2. LangGraph is inspired by frameworks like Pregel and Apache Beam, which support cyclic computations. In contrast, LangChain Expression Language is more focused on building DAG-based workflows.

3. The main use case for LangGraph is to add cycles to LLM applications, where the path through the workflow is not known until runtime, as the LLM decides which path to take.

4. LangGraph provides a custom interface inspired by NetworkX for defining nodes (functions or runnables) and edges (including conditional edges) in the workflow graph.

5. LangGraph is fully integrated with the LangChain ecosystem, allowing it to leverage all the integrations and observability features provided by LangChain.

In summary, LangGraph extends the capabilities of LangChain by enabling the creation of more complex, cyclic workflows involving multiple agents or components, rather than being limited to directed acyclic graphs.
User:  q
Goodbye!

Congrats! You've created a conversational agent in langgraph that can use a search engine to retrieve updated information when needed. Now it can handle a wider range of user queries. To inspect all the steps your agent just took, check out this LangSmith trace.

Our chatbot still can't remember past interactions on its own, limiting its ability to have coherent, multi-turn conversations. In the next part, we'll add memory to address this.

The full code for the graph we've created in this section is reproduced below:

In [19]:
from typing import Annotated, Union

from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict

from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode


class State(TypedDict):
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)


tool = TavilySearchResults(max_results=2)
tools = [tool]
llm = ChatAnthropic(model="claude-3-haiku-20240307")
llm_with_tools = llm.bind_tools(tools)


def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}


graph_builder.add_node("chatbot", chatbot)

tool_node = ToolNode(tools=[tool])
graph_builder.add_node("action", tool_node)

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
    {"action": "action", "__end__": "__end__"},
)
# Any time a tool is called, we return to the chatbot to decide the next step
graph_builder.add_edge("action", "chatbot")
graph_builder.set_entry_point("chatbot")
graph = graph_builder.compile()

Part 3: Adding Memory to the Chatbot

Our chatbot can now use tools to answer user questions, but it doesn't remember the context of previous interactions. This limits its ability to have coherent, multi-turn conversations.

LangGraph solves this problem through persistent checkpointing. If you provide a checkpointer when compiling the graph and a thread_id when calling your graph, LangGraph automatically saves the state after each step. When you invoke the graph again using the same thread_id, the graph loads its saved state, allowing the chatbot to pick up where it left off.

We will see later that checkpointing is much more powerful than simple chat memory - it lets you save and resume complex state at any time for error recovery, human-in-the-loop workflows, time travel interactions, and more. But before we get too ahead of ourselves, let's add checkpointing to enable multi-turn conversations.

To get started, create a SqliteSaver checkpointer.

In [20]:
from langgraph.checkpoint.sqlite import SqliteSaver

memory = SqliteSaver.from_conn_string(":memory:")

Notice that we've specified :memory as the Sqlite DB path. This is convenient for our tutorial (it saves it all in-memory). In a production application, you would likely change this to connect to your own DB and/or use one of the other checkpointer classes.

Next define the graph. The following is all copied from Part 2.

In [21]:
from typing import Annotated, Union

from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict

from langgraph.graph import StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode


class State(TypedDict):
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)


tool = TavilySearchResults(max_results=2)
tools = [tool]
llm = ChatAnthropic(model="claude-3-haiku-20240307")
llm_with_tools = llm.bind_tools(tools)


def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}


graph_builder.add_node("chatbot", chatbot)

tool_node = ToolNode(tools=[tool])
graph_builder.add_node("action", tool_node)

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
    {"action": "action", "__end__": "__end__"},
)
# Any time a tool is called, we return to the chatbot to decide the next step
graph_builder.add_edge("action", "chatbot")
graph_builder.set_entry_point("chatbot")

Finally, compile the graph with the provided checkpointer.

In [22]:
graph = graph_builder.compile(checkpointer=memory)

Notice the connectivity of the graph hasn't changed since Part 2. All we are doing is checkpointing the State as the graph works through each node.

In [23]:
try:
    display(Image(graph.get_graph().draw_mermaid_png()))
except:
    # This requires some extra dependencies and is optional
    pass

Now you can interact with your bot! First, pick a thread to use as the key for this conversation.

In [24]:
config = {"configurable": {"thread_id": "1"}}

Next, call your chat bot.

In [25]:
user_input = "Hi there! My name is Will."

# The config is the **second positional argument** to stream() or invoke()!
events = graph.stream({"messages": [("user", user_input)]}, config)
for event in events:
    for value in event.values():
        if isinstance(value, BaseMessage):
            print("Assistant:", value.content)

Note: The config was provided as the second positional argument when calling our graph. It importantly is not nested within the graph inputs ({'messages': []}).

Let's ask a followup: see if it remembers your name.

In [26]:
user_input = "Remember my name?"

# The config is the **second positional argument** to stream() or invoke()!
events = graph.stream({"messages": [("user", user_input)]}, config)
for event in events:
    for value in event.values():
        if isinstance(value["messages"][-1], BaseMessage):
            print("Assistant:", value["messages"][-1].content)
Assistant: Yes, I remember your name is Will. It's nice to meet you, Will!

Notice that we are't the memory using an external list: it's all handled by the checkpointer! You can inspect the full execution in this LangSmith trace to see what's going on.

Don't believe me? Try this using a different config.

In [27]:
# The only difference is we change the `thread_id` here to "2" instead of "1"
events = graph.stream(
    {"messages": [("user", user_input)]}, {"configurable": {"thread_id": "2"}}
)
for event in events:
    for value in event.values():
        if isinstance(value["messages"][-1], BaseMessage):
            print("Assistant:", value["messages"][-1].content)
Assistant: I'm afraid I don't actually have any information about your name stored. As an AI assistant created by Anthropic, I don't have a persistent memory of previous conversations or personal details about users. I'm happy to try to assist you, but I don't have the capability to "remember" your name from previous interactions. Could you please let me know your name again so I can refer to you appropriately?

Notice that the only change we've made is to modify the thread_id in the config. See this call's LangSmith trace for comparison.

By now, we have made a few checkpoints across two different threads. But what goes into a checkpoint? To inspect a graph's state for a given config at any time, call get_state(config).

In [28]:
snapshot = graph.get_state(config)
snapshot
Out [28]:
StateSnapshot(values={'messages': [HumanMessage(content='Hi there! My name is Will.', id='4de66d5f-2bcf-451e-8913-8d0939bdf3aa'), AIMessage(content="It's nice to meet you, Will! I'm an AI assistant created by Anthropic to be helpful, harmless, and honest. I'm happy to chat with you about anything you'd like - feel free to ask me questions or let me know if there's anything I can assist with.", response_metadata={'id': 'msg_01NK3eHWMzScBWg4aPhxHgBe', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 375, 'output_tokens': 64}}, id='run-f6ad0a33-e5dd-4f09-8947-23e48805e483-0'), HumanMessage(content='Remember my name?', id='eb778872-1114-4974-a0c8-37c8d8d8eaa3'), AIMessage(content="Yes, I remember your name is Will. It's nice to meet you, Will!", response_metadata={'id': 'msg_01Q9FjFhHs63kgEytVmZeova', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 446, 'output_tokens': 21}}, id='run-7e4976a1-5a21-4b7d-ba4d-55da1c33fa72-0')]}, next=(), config={'configurable': {'thread_id': '1', 'thread_ts': '2024-04-18T07:28:57.722029+00:00'}}, parent_config=None)
In [29]:
snapshot.next  # (since the graph ended this turn, `next` is empty. If you fetch a state from within a graph invocation, next tells which node will execute next)
Out [29]:
()

The snapshot above contains the current state values, corresponding config, and the next node to process. In our case, the graph has reached an __end__ state, so next is empty.

Congratulations! Your chatbot can now maintain conversation state across sessions thanks to LangGraph's checkpointing system. This opens up exciting possibilities for more natural, contextual interactions. LangGraph's checkpointing even handles arbitrary complex graph states, which is much more expressive and powerful than simple chat memory.

In the next part, we'll introduce human oversight to our bot to handle situations where it may need guidance or verification before proceeding.

Check out the code snippet below to review our graph from this section.

In [30]:
from typing import Annotated, Union

from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict

from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import MessageGraph, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode


class State(TypedDict):
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)


tool = TavilySearchResults(max_results=2)
tools = [tool]
llm = ChatAnthropic(model="claude-3-haiku-20240307")
llm_with_tools = llm.bind_tools(tools)


def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}


graph_builder.add_node("chatbot", chatbot)

tool_node = ToolNode(tools=[tool])
graph_builder.add_node("action", tool_node)

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
    {"action": "action", "__end__": "__end__"},
)
graph_builder.add_edge("action", "chatbot")
graph_builder.set_entry_point("chatbot")
graph = graph_builder.compile(checkpointer=memory)

Part 4: Human-in-the-loop

Agents can be unreliable and may need human input to successfully accomplish tasks. Similarly, for some actions, you may want to require human approval before running to ensure that everything is running as intended.

LangGraph supports human-in-the-loop workflows in a number of ways. In this section, we will use LangGraph's interrupt_before functionality to always break the tool node.

First, start from our existing code. The following is copied from Part 3.

In [31]:
from typing import Annotated, Union

from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict

from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import MessageGraph, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode

memory = SqliteSaver.from_conn_string(":memory:")


class State(TypedDict):
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)


tool = TavilySearchResults(max_results=2)
tools = [tool]
llm = ChatAnthropic(model="claude-3-haiku-20240307")
llm_with_tools = llm.bind_tools(tools)


def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}


graph_builder.add_node("chatbot", chatbot)

tool_node = ToolNode(tools=[tool])
graph_builder.add_node("action", tool_node)

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
    {"action": "action", "__end__": "__end__"},
)
graph_builder.add_edge("action", "chatbot")
graph_builder.set_entry_point("chatbot")

Now, compile the graph, specifying to interrupt_before the action node.

In [32]:
graph = graph_builder.compile(
    checkpointer=memory,
    # This is new!
    interrupt_before=["action"],
    # Note: can also interrupt __after__ actions, if desired.
    # interrupt_after=["action"]
)
In [33]:
user_input = "I'm learning LangGraph. Could you do some research on it for me?"
config = {"configurable": {"thread_id": "1"}}
# The config is the **second positional argument** to stream() or invoke()!
events = graph.stream({"messages": [("user", user_input)]}, config)
for event in events:
    for value in event.values():
        if isinstance(value["messages"][-1], BaseMessage):
            print("Assistant:", value["messages"][-1].content)
Assistant: [{'text': "Okay, let's look into LangGraph for you. Here is a summary of the key information I was able to find:", 'type': 'text'}, {'id': 'toolu_01XayS5zo1ZHaQMYpWX5x8fQ', 'input': {'query': 'LangGraph'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]

Let's inspect the graph state to confirm it worked.

In [34]:
snapshot = graph.get_state(config)
snapshot.next
Out [34]:
('action',)

Notice that unlike last time, the "next" node is set to 'action'. We've interrupted here! Let's check the tool invocation.

In [35]:
existing_message = snapshot.values["messages"][-1]
existing_message.tool_calls
Out [35]:
[{'name': 'tavily_search_results_json',
  'args': {'query': 'LangGraph'},
  'id': 'toolu_01XayS5zo1ZHaQMYpWX5x8fQ'}]

This query seems reasonable. Nothing to filter here. The simplest thing the human can do is just let the graph continue executing. Let's do that below.

Next, continue the graph! Passing in None will just let the graph continue where it left off, without adding anything new to the state.

In [36]:
# `None` will append nothing new to the current state, letting it resume as if it had never been interrupted
events = graph.stream(None, config)
for event in events:
    for value in event.values():
        if isinstance(value["messages"][-1], BaseMessage):
            print("Assistant:", value["messages"][-1].content)
Assistant: [{"url": "https://blog.langchain.dev/langgraph/", "content": "Some of the things we are looking to implement in the near future:\nIf any of these resonate with you, please feel free to add an example notebook in the LangGraph repo, or reach out to us at hello@langchain.dev for more involved collaboration!\n See this notebook for how to get started\nModifications\nOne of the big benefits of LangGraph is that it exposes the logic of AgentExecutor in a far more natural and modifiable way. An example of this could be that after a model is called we either exit the graph and return to the user, or we call a tool - depending on what a user decides! This function/LCEL should accept a dictionary in the same form as the State object as input, and output a dictionary with keys of the State object to update.\n In this case, it's often ideal if the LLM can reason that the results returned from the retriever are poor, and maybe issue a second (more refined) query to the retriever, and use those results instead."}, {"url": "https://blog.langchain.dev/langgraph-multi-agent-workflows/", "content": "As a part of the launch, we highlighted two simple runtimes: one that is the equivalent of the AgentExecutor in langchain, and a second that was a version of that aimed at message passing and chat models.\n It's important to note that these three examples are only a few of the possible examples we could highlight - there are almost assuredly other examples out there and we look forward to seeing what the community comes up with!\n LangGraph: Multi-Agent Workflows\nLinks\nLast week we highlighted LangGraph - a new package (available in both Python and JS) to better enable creation of LLM workflows containing cycles, which are a critical component of most agent runtimes. \"\nAnother key difference between Autogen and LangGraph is that LangGraph is fully integrated into the LangChain ecosystem, meaning you take fully advantage of all the LangChain integrations and LangSmith observability.\n As part of this launch, we're also excited to highlight a few applications built on top of LangGraph that utilize the concept of multiple agents.\n"}]
Assistant: Based on the search results, LangGraph seems to be a new package developed as part of the LangChain ecosystem. It is designed to enable the creation of more advanced LLM workflows that can contain cycles, which is an important feature for agent-based models.

Some key points about LangGraph:

- It exposes the logic of the AgentExecutor in LangChain in a more modifiable way, allowing for customization of behavior after a model is called.
- It supports multi-agent workflows, enabling the creation of applications with multiple interacting agents.
- It is integrated with the broader LangChain ecosystem, allowing it to take advantage of existing LangChain integrations and observability tools.

The search results indicate that LangGraph is a relatively new development, and the blog post mentions there are plans to implement additional features in the near future. Overall, it seems like an interesting tool for building more sophisticated LLM-powered applications, especially those involving agent-based models or multi-agent interactions.

Let me know if you have any other questions! I'm happy to provide more details on LangGraph or do additional research if needed.

Review this call's LangSmith trace to see the exact work that was done in the above call. Notice that the state is loaded in the first step so that your chatbot can continue where it left off.

Congrats! You've used an interrupt to add human-in-the-loop execution to your chatbot, allowing for human oversight and intervention when needed. This opens up the potential UIs you can create with your AI systems. Since we have already added a checkpointer, the graph can be paused indefinitely and resumed at any time as if nothing had happened.

Next, we'll explore how to further customize the bot's behavior using custom state updates.

Below is a copy of the code you used in this section. The only difference between this and the previous parts is the addition of the interrupt_before argument.

In [37]:
from typing import Annotated, Union

from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict

from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import MessageGraph, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode


class State(TypedDict):
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)


tool = TavilySearchResults(max_results=2)
tools = [tool]
llm = ChatAnthropic(model="claude-3-haiku-20240307")
llm_with_tools = llm.bind_tools(tools)


def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}


graph_builder.add_node("chatbot", chatbot)

tool_node = ToolNode(tools=[tool])
graph_builder.add_node("action", tool_node)

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
    {"action": "action", "__end__": "__end__"},
)
graph_builder.add_edge("action", "chatbot")
graph_builder.set_entry_point("chatbot")

memory = SqliteSaver.from_conn_string(":memory:")
graph = graph_builder.compile(
    checkpointer=memory,
    # This is new!
    interrupt_before=["action"],
    # Note: can also interrupt __after__ actions, if desired.
    # interrupt_after=["action"]
)

Part 5: Manually Updating the State

In the previous section, we showed how to interrupt a graph so that a human could inspect its actions. This lets the human read the state, but if they want to change they agent's course, they'll need to have write access.

Thankfully, LangGraph lets you manually update state! Updating the state lets you control the agent's trajectory by modifying its actions (even modifying the past!). This capability is particularly useful when you want to correct the agent's mistakes, explore alternative paths, or guide the agent towards a specific goal.

We'll show how to update a checkpointed state below. As before, first, define your graph. We'll re-use the exact same graph as before.

In [38]:
from typing import Annotated, Union

from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict

from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import MessageGraph, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode


class State(TypedDict):
    messages: Annotated[list, add_messages]


graph_builder = StateGraph(State)


tool = TavilySearchResults(max_results=2)
tools = [tool]
llm = ChatAnthropic(model="claude-3-haiku-20240307")
llm_with_tools = llm.bind_tools(tools)


def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}


graph_builder.add_node("chatbot", chatbot)

tool_node = ToolNode(tools=[tool])
graph_builder.add_node("action", tool_node)

graph_builder.add_conditional_edges(
    "chatbot",
    tools_condition,
    {"action": "action", "__end__": "__end__"},
)
graph_builder.add_edge("action", "chatbot")
graph_builder.set_entry_point("chatbot")
memory = SqliteSaver.from_conn_string(":memory:")
graph = graph_builder.compile(
    checkpointer=memory,
    # This is new!
    interrupt_before=["action"],
    # Note: can also interrupt __after__ actions, if desired.
    # interrupt_after=["action"]
)

user_input = "I'm learning LangGraph. Could you do some research on it for me?"
config = {"configurable": {"thread_id": "1"}}
# The config is the **second positional argument** to stream() or invoke()!
events = graph.stream({"messages": [("user", user_input)]}, config)
for event in events:
    for value in event.values():
        if isinstance(value["messages"][-1], BaseMessage):
            print("Assistant:", value["messages"][-1].content)
Assistant: [{'text': "Okay, let's look up some information on LangGraph:", 'type': 'text'}, {'id': 'toolu_016HGEqpk5m2wiAD98c3Gwcx', 'input': {'query': 'LangGraph'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]
In [40]:
snapshot = graph.get_state(config)
existing_message = snapshot.values["messages"][-1]
existing_message.pretty_print()
================================== Ai Message ==================================

[{'text': "Okay, let's look up some information on LangGraph:", 'type': 'text'}, {'id': 'toolu_016HGEqpk5m2wiAD98c3Gwcx', 'input': {'query': 'LangGraph'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]

So far, all of this is an exact repeat of the previous section. The LLM just requested to use the search engine tool and our graph was interrupted. If we proceed as before, the tool will be called to search the web.

But what if the user wants to intercede? What if we think the chat bot doesn't need to use the tool?

Let's directly provide the correct response!

In [41]:
from langchain_core.messages import AIMessage, ToolMessage

new_message = AIMessage(
    content="LangGraph is a library for building stateful, multi-actor applications with LLMs."
)

new_message.pretty_print()
graph.update_state(
    # Which state to update
    config,
    # The updated values to provide. The messages in our `State` are "append-only", meaning this will be appended
    # to the existing state. We will review how to update existing messages in the next section!
    {"messages": [new_message]},
)

print("\n\nLast 2 messages;")
print(graph.get_state(config).values["messages"][-2:])
================================== Ai Message ==================================

LangGraph is a library for building stateful, multi-actor applications with LLMs.


Last 2 messages;
[AIMessage(content=[{'text': "Okay, let's look up some information on LangGraph:", 'type': 'text'}, {'id': 'toolu_016HGEqpk5m2wiAD98c3Gwcx', 'input': {'query': 'LangGraph'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}], response_metadata={'id': 'msg_01WgZjuqwL2gR1JnwJ2Ugxg8', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 384, 'output_tokens': 75}}, id='run-ba7aa4d5-6269-4682-888e-5f505fc7ec23-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'LangGraph'}, 'id': 'toolu_016HGEqpk5m2wiAD98c3Gwcx'}]), AIMessage(content='LangGraph is a library for building stateful, multi-actor applications with LLMs.', id='e58607fd-43c4-416e-99dc-4c4575eddcdf')]

Now the graph is complete, since we've provided the final response message! Since state updates simulate a graph step, they even generate corresponding traces. Inspec the LangSmith trace of the update_state call above to see what's going on.

Notice that our new messages is appended to the messages already in the state. Remember how we defined the State type?

class State(TypedDict):
    messages: Annotated[list, add_messages]

We annotated messages with the pre-built add_messages function. This instructs the graph to always append values to the existing list, rather than overwriting the list directly. The same logic is applied here, so the messages we passed to update_state were appended in the same way!

The update_state function operates as if it were one of the nodes in your graph! By default, the update operation uses the node that was last executed, but you can manually specify it below. Let's add an update and tell the graph to treat it as if it came from the "chatbot".

In [42]:
graph.update_state(
    config,
    {"messages": [AIMessage(content="I'm an AI expert!")]},
    # Which node for this function to act as. It will automatically continue
    # processing as if this node just ran.
    as_node="chatbot",
)
Out [42]:
{'configurable': {'thread_id': '1',
  'thread_ts': '2024-04-18T07:45:58.218035+00:00'}}

Check out the LangSmith trace for this update call at the provided link. Notice from the trace that the graph continues into the tools_condition edge. We just told the graph to treat the update as_node="chatbot". If we follow the diagram below and start from the chatbot node, we naturally end up in the tools_condition edge and then __end__ since our updated message lacks tool calls.

In [43]:
try:
    display(Image(graph.get_graph().draw_mermaid_png()))
except:
    # This requires some extra dependencies and is optional
    pass

Inspect the current state as before to confirm the checkpoint reflects our manual updates.

In [44]:
snapshot = graph.get_state(config)
print(snapshot.values["messages"][-3:])
print(snapshot.next)
[AIMessage(content=[{'text': "Okay, let's look up some information on LangGraph:", 'type': 'text'}, {'id': 'toolu_016HGEqpk5m2wiAD98c3Gwcx', 'input': {'query': 'LangGraph'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}], response_metadata={'id': 'msg_01WgZjuqwL2gR1JnwJ2Ugxg8', 'model': 'claude-3-haiku-20240307', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 384, 'output_tokens': 75}}, id='run-ba7aa4d5-6269-4682-888e-5f505fc7ec23-0', tool_calls=[{'name': 'tavily_search_results_json', 'args': {'query': 'LangGraph'}, 'id': 'toolu_016HGEqpk5m2wiAD98c3Gwcx'}]), AIMessage(content='LangGraph is a library for building stateful, multi-actor applications with LLMs.', id='e58607fd-43c4-416e-99dc-4c4575eddcdf'), AIMessage(content="I'm an AI expert!", id='82bbd2aa-b3c7-48cf-abeb-33f7938e98ae')]
()

Notice: that we've continued to add AI messages to the state. Since we are acting as the chatbot and responding with an AIMessage that doesn't contain tool_calls, the graph knows that it has entered a finished state (next is empty).

What if you want to overwrite existing messages?

The add_messages function we used to annotate our graph's State above controls how updates are made to the messages key. This function looks at any message IDs in the new messages list. If the ID matches a message in the existing state, add_messages overwrites the existing message with the new content.

As an example, let's update the tool invocation to make sure we get good results from our search engine! First, start a new thread:

In [45]:
user_input = "I'm learning LangGraph. Could you do some research on it for me?"
config = {"configurable": {"thread_id": "2"}}  # we'll use thread_id = 2 here
events = graph.stream({"messages": [("user", user_input)]}, config)
for event in events:
    for value in event.values():
        if isinstance(value["messages"][-1], BaseMessage):
            print("Assistant:", value["messages"][-1].content)
Assistant: [{'id': 'toolu_01YQjNFesoHt4yFzg53xSLgb', 'input': {'query': 'LangGraph'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]

Next, let's update the tool invocation for our agent. Maybe we want to search for human-in-the-loop workflows in particular.

In [46]:
from langchain_core.messages import AIMessage

snapshot = graph.get_state(config)
existing_message = snapshot.values["messages"][-1]
print("Original")
print(existing_message.tool_calls[0])
new_tool_call = existing_message.tool_calls[0].copy()
new_tool_call["args"]["query"] = "LangGraph human-in-the-loop workflow"
new_message = AIMessage(
    content=existing_message.content,
    tool_calls=[new_tool_call],
    # Important! The ID is how LangGraph knows to REPLACE the message in the state rather than APPEND this messages
    id=existing_message.id,
)

print("Updated")
print(new_message.tool_calls[0])
print("Message ID", new_message.id)
graph.update_state(config, {"messages": [new_message]})

print("\n\nTool calls")
graph.get_state(config).values["messages"][-1].tool_calls
Out [46]:
Original
{'name': 'tavily_search_results_json', 'args': {'query': 'LangGraph'}, 'id': 'toolu_01YQjNFesoHt4yFzg53xSLgb'}
Updated
{'name': 'tavily_search_results_json', 'args': {'query': 'LangGraph human-in-the-loop workflow'}, 'id': 'toolu_01YQjNFesoHt4yFzg53xSLgb'}
Message ID run-4507ef72-6c8e-4fc3-9521-6e9e856c62a8-0


Tool calls
[{'name': 'tavily_search_results_json',
  'args': {'query': 'LangGraph human-in-the-loop workflow'},
  'id': 'toolu_01YQjNFesoHt4yFzg53xSLgb'}]

Notice that we've modified the AI's tool invocation to search for "LangGraph human-in-the-loop workflow" instead of the simple "LangGraph".

Check out the LangSmith trace to see the state update call - you can see our new message has successfully updated the previous AI message.

Resume the graph by streaming with an input of None and the existing config.

In [47]:
events = graph.stream(None, config)
for event in events:
    for value in event.values():
        if isinstance(value["messages"][-1], BaseMessage):
            print("Assistant:", value["messages"][-1].content)
Assistant: [{"url": "https://langchain-ai.github.io/langgraph/how-tos/human-in-the-loop/", "content": "Human-in-the-loop\u00b6 When creating LangGraph agents, it is often nice to add a human in the loop component. This can be helpful when giving them access to tools. ... from langgraph.graph import MessageGraph, END # Define a new graph workflow = MessageGraph # Define the two nodes we will cycle between workflow. add_node (\"agent\", call_model) ..."}, {"url": "https://langchain-ai.github.io/langgraph/how-tos/agent_executor/human-in-the-loop/", "content": "Human in the Loop\u00b6 In this notebook we will go over how to add a human-in-the-loop workflow to the base agent executor. We will use the human to approve ... In langgraph, a node can be either a function or a runnable. There are two main nodes we need for this: The agent: responsible for deciding what (if any) actions to take. ..."}]
Assistant: Based on the search results, LangGraph appears to be a framework for building AI agents that can interact with humans in a loop. Some key points:

- LangGraph allows you to define "nodes" that represent different components of an AI agent, such as a language model, a tool executor, or a human-in-the-loop component.
- The human-in-the-loop feature lets you incorporate human feedback and approval into the agent's decision-making process. This can be helpful for sensitive applications where you want a human to review the agent's proposed actions.
- LangGraph provides examples of how to set up a workflow with an agent node and a human-in-the-loop node, allowing the agent to cycle between making decisions and getting human approval.

Overall, LangGraph seems to be a flexible framework for building AI agents that can interact with humans in a controlled and transparent way. It could be useful for applications where you want to leverage large language models while maintaining human oversight and control.

Let me know if you have any other questions! I'm happy to dig deeper into the LangGraph framework.

Check out the trace to see the toolc all and later LLM response. Notice that now the graph queries the search engine using our updated query term - we were able to manually override the LLM's search here!

All of this is reflected in the graph's checkpointed memory, meaning if we continue the conversation, it will recall all the modified state.

In [48]:
events = graph.stream(
    {"messages": ("user", "Guess what I'm learning about these days?")}, config
)
for event in events:
    for value in event.values():
        if isinstance(value, BaseMessage):
            print("Assistant:", value.content)

Congratulations! You've used interrupt_before and update_state to manually modify the state as a part of a human-in-the-loop workflow. Interruptions and state modifications let you control how the agent behaves. Combined with persistant checkpointing, it means you can pause an action and resume at any point. Your user doesn't have to be available when the graph interrupts!

The graph code for this section is identical to previous ones. The key snippets to remember are to add .compile(..., interrupt_before=[...]) (or interrupt_after) if you want to explicitly pause the graph whenever it reaches a node. Then you can use update_state to modify the checkpoint and control how the graph should proceed.

Part 6: Customizing State

So far, we've relied on a simple state (it's just a list of messages!). You can go far with this simple state, but if you want to define complex behavior without relying on the message list, you can add additional fields to the state. In this section, we will extend our chat bot with a new node to illustrate this.

In the examples above, we involved a human deterministically: the graph always interrupted whenever an tool was invoked. Suppose we wanted our chat bot to have the choice of relying on a human.

One way to do this is to create a passthrough "human" node, before which the graph will always stop. We will only execute this node if the LLM invokes a "human" tool. For our convenience, we will include an "ask_human" flag in our graph state that we will flip if the LLM calls this tool.

Below, define this new graph, with an updated State

In [49]:
from typing import Annotated, Union

from langchain_anthropic import ChatAnthropic
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.messages import BaseMessage
from typing_extensions import TypedDict

from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import MessageGraph, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode


class State(TypedDict):
    messages: Annotated[list, add_messages]
    # This flag is new
    ask_human: bool

Next, define a schema to show the model to let it decide to request assistance.

In [50]:
from langchain_core.pydantic_v1 import BaseModel


class RequestAssistance(BaseModel):
    """Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.

    To use this function, relay the user's 'request' so the expert can provide the right guidance.
    """

    request: str

Next, define the chatbot node. The primary modification here is flip the ask_human flag if we see that the chat bot has invoked the RequestAssistance flag.

In [51]:
tool = TavilySearchResults(max_results=2)
tools = [tool]
llm = ChatAnthropic(model="claude-3-haiku-20240307")
# We can bind the llm to a tool definition, a pydantic model, or a json schema
llm_with_tools = llm.bind_tools(tools + [RequestAssistance])


def chatbot(state: State):
    response = llm_with_tools.invoke(state["messages"])
    ask_human = False
    if (
        response.tool_calls
        and response.tool_calls[0]["name"] == RequestAssistance.__name__
    ):
        ask_human = True
    return {"messages": [response], "ask_human": ask_human}

Next, create the graph builder and add the chatbot and tools nodes to the graph, same as before.

Warning:
Output truncated. This notebook contains too many cells to display efficiently.