From 605fe4ea116fd380dddaec49e9372507514ee2ee Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Thu, 16 Jan 2025 14:20:05 -0500 Subject: [PATCH] simplify hitl, condense part 5 --- docs/docs/tutorials/introduction.ipynb | 1969 ++++++------------------ 1 file changed, 436 insertions(+), 1533 deletions(-) diff --git a/docs/docs/tutorials/introduction.ipynb b/docs/docs/tutorials/introduction.ipynb index f20a29a9b..6f248e5da 100644 --- a/docs/docs/tutorials/introduction.ipynb +++ b/docs/docs/tutorials/introduction.ipynb @@ -466,6 +466,7 @@ "\n", "llm = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", "# Modification: tell the LLM which tools it can call\n", + "# highlight-next-line\n", "llm_with_tools = llm.bind_tools(tools)\n", "\n", "\n", @@ -864,7 +865,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 23, "id": "a06548bf-81fa-4436-b4c1-f68601fb4187", "metadata": {}, "outputs": [], @@ -1173,34 +1174,31 @@ "\n", "LangGraph's [persistence](../../concepts/persistence) layer supports human-in-the-loop workflows, allowing execution to pause and resume based on user feedback. The primary interface to this functionality is the [interrupt](../../concepts/human_in_the_loop/#interrupt) function. Calling `interrupt` inside a node will pause execution. Execution can be resumed, together with new input from a human, by passing in a [Command](../../concepts/human_in_the_loop/#the-command-primitive). `interrupt` is ergonomically similar to Python's built-in `input()`, [with some caveats](../../concepts/human_in_the_loop/#interrupt). We demonstrate an example below.\n", "\n", - "First, start with our existing code from Part 3. We make two changes, highlighted below:\n", - "1. We add a `human_review_node` to handle the interaction with a human reviewer;\n", - "2. We use a different condition in our conditional edge that routes to the `human_review_node`, instead of directly to tools." + "First, start with our existing code from Part 3. We will make one change, which is to add a simple `human_assistance` tool accessible to the chatbot. This tool uses `interrupt` to receive information from a human." ] }, { "cell_type": "code", "execution_count": null, - "id": "5a81608a-373a-4339-b1c6-65b73a92b983", + "id": "08439bb4-91e8-4abb-a57a-2511877abcb7", "metadata": {}, "outputs": [], "source": [ - "from typing import Annotated, Literal\n", + "from typing import Annotated\n", "\n", "from langchain_anthropic import ChatAnthropic\n", "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "from langchain_core.tools import tool\n", "from typing_extensions import TypedDict\n", "\n", "from langgraph.checkpoint.memory import MemorySaver\n", "from langgraph.graph import StateGraph, START, END\n", "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ToolNode\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", "\n", "# highlight-next-line\n", "from langgraph.types import Command, interrupt\n", "\n", - "memory = MemorySaver()\n", - "\n", "\n", "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", @@ -1209,8 +1207,20 @@ "graph_builder = StateGraph(State)\n", "\n", "\n", + "# highlight-next-line\n", + "@tool\n", + "# highlight-next-line\n", + "def human_assistance(query: str) -> str:\n", + " # highlight-next-line\n", + " \"\"\"Request assistance from a human.\"\"\"\n", + " # highlight-next-line\n", + " human_response = interrupt({\"query\": query})\n", + " # highlight-next-line\n", + " return human_response[\"data\"]\n", + "\n", + "\n", "tool = TavilySearchResults(max_results=2)\n", - "tools = [tool]\n", + "tools = [tool, human_assistance]\n", "llm = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", "llm_with_tools = llm.bind_tools(tools)\n", "\n", @@ -1219,61 +1229,14 @@ " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n", "\n", "\n", - "# We add a node to handle the interaction with a human reviewer\n", - "def human_review_node(state: State) -> Command[Literal[\"chatbot\", \"tools\"]]:\n", - " last_message = state[\"messages\"][-1]\n", - " tool_call = last_message.tool_calls[-1]\n", + "graph_builder.add_node(\"chatbot\", chatbot)\n", "\n", - " # this is the value we'll be providing via Command(resume=)\n", - " human_review = interrupt(\n", - " {\n", - " \"question\": \"Is this correct?\",\n", - " # Surface tool calls for review\n", - " \"tool_call\": tool_call,\n", - " }\n", - " )\n", - "\n", - " review_action = human_review[\"action\"]\n", - " review_data = human_review.get(\"data\")\n", - "\n", - " # if approved, call the tool\n", - " if review_action == \"continue\":\n", - " return Command(goto=\"tools\")\n", - "\n", - " elif review_action == \"feedback\":\n", - " # NOTE: we're adding feedback message as a ToolMessage\n", - " # to preserve the correct order in the message history\n", - " # (AI messages with tool calls need to be followed by tool call messages)\n", - " tool_message = {\n", - " \"role\": \"tool\",\n", - " # This is our natural language feedback\n", - " \"content\": review_data,\n", - " \"name\": tool_call[\"name\"],\n", - " \"tool_call_id\": tool_call[\"id\"],\n", - " }\n", - " return Command(goto=\"chatbot\", update={\"messages\": [tool_message]})\n", - "\n", - "\n", - "# Where we used tools_condition before, here we update the condition\n", - "# to route to the human review node first, instead of tools.\n", - "def route_after_llm(state) -> Literal[END, \"human_review_node\"]:\n", - " if len(state[\"messages\"][-1].tool_calls) == 0:\n", - " return END\n", - " else:\n", - " return \"human_review_node\"\n", - "\n", - "\n", - "graph_builder.add_node(chatbot)\n", - "\n", - "tool_node = ToolNode(tools=[tool])\n", + "tool_node = ToolNode(tools=tools)\n", "graph_builder.add_node(\"tools\", tool_node)\n", - "# highlight-next-line\n", - "graph_builder.add_node(human_review_node)\n", "\n", "graph_builder.add_conditional_edges(\n", " \"chatbot\",\n", - " # highlight-next-line\n", - " route_after_llm,\n", + " tools_condition,\n", ")\n", "graph_builder.add_edge(\"tools\", \"chatbot\")\n", "graph_builder.add_edge(START, \"chatbot\")" @@ -1281,14 +1244,14 @@ }, { "cell_type": "markdown", - "id": "813505b2-18c1-46e9-b891-20a34232808b", + "id": "cada4cd2-0316-487b-923e-0d12fa3473c2", "metadata": {}, "source": [ "------\n", "\n", "!!! tip\n", "\n", - " Check out [this guide](../../how-tos/human_in_the_loop/review-tool-calls/) for more detail on human review of tool calls, including how to edit tool calls directly.\n", + " Check out the [Human-in-the-loop section](../../how-tos/#human-in-the-loop) of the How-to Guides for more examples of Human-in-the-loop workflows, including how to [review and edit tool calls](../../how-tos/human_in_the_loop/review-tool-calls/) before they are executed.\n", "\n", "---------\n", "\n", @@ -1297,11 +1260,13 @@ }, { "cell_type": "code", - "execution_count": 5, - "id": "b0883e32-1a39-4ce9-ae32-bbd66708fd84", + "execution_count": 3, + "id": "16cc6b14-f758-4590-beb7-7b76a6841521", "metadata": {}, "outputs": [], "source": [ + "memory = MemorySaver()\n", + "\n", "graph = graph_builder.compile(checkpointer=memory)" ] }, @@ -1310,18 +1275,18 @@ "id": "bc0e84db-925c-4468-b3e0-639e6b25ac3c", "metadata": {}, "source": [ - "Visualizing the graph, we can see that we've added a new node to mediate the interaction between the chatbot and the tools:" + "Visualizing the graph, we recover the same layout as before. We have just added a tool!" ] }, { "cell_type": "code", - "execution_count": 34, - "id": "8b8e61b5-f3c2-4917-a8fc-928ccbcfc6ab", + "execution_count": 4, + "id": "22ff19af-5a5b-4ca5-82d8-6a38445c24af", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "" ] @@ -1340,9 +1305,17 @@ " pass" ] }, + { + "cell_type": "markdown", + "id": "ae4663b8-176d-445d-bd07-56938c044f98", + "metadata": {}, + "source": [ + "Let's now prompt the chatbot with a question that will engage the new `human_assistance` tool:" + ] + }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "9f318020-ab7e-415b-a5e2-eddec6d9f3a6", "metadata": {}, "outputs": [ @@ -1352,22 +1325,22 @@ "text": [ "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", - "I'm learning LangGraph. Could you do some research on it for me?\n", + "I need some expert guidance for building an AI agent. Could you request assistance for me?\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "[{'text': \"Certainly! I'd be happy to research LangGraph for you. To get the most up-to-date and comprehensive information, I'll use the Tavily search engine to look this up. Let me do that for you now.\", 'type': 'text'}, {'id': 'toolu_01UukDzyzhEhxMgTbshetpT4', 'input': {'query': 'LangGraph programming framework'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", + "[{'text': \"Certainly! I'd be happy to request expert assistance for you regarding building an AI agent. To do this, I'll use the human_assistance function to relay your request. Let me do that for you now.\", 'type': 'text'}, {'id': 'toolu_015tcCygn4UdBM24vg9NSycp', 'input': {'query': 'A user is seeking expert guidance for building an AI agent. Can you provide some professional advice or resources on this topic?'}, 'name': 'human_assistance', 'type': 'tool_use'}]\n", "Tool Calls:\n", - " tavily_search_results_json (toolu_01UukDzyzhEhxMgTbshetpT4)\n", - " Call ID: toolu_01UukDzyzhEhxMgTbshetpT4\n", + " human_assistance (toolu_015tcCygn4UdBM24vg9NSycp)\n", + " Call ID: toolu_015tcCygn4UdBM24vg9NSycp\n", " Args:\n", - " query: LangGraph programming framework\n" + " query: A user is seeking expert guidance for building an AI agent. Can you provide some professional advice or resources on this topic?\n" ] } ], "source": [ - "user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\n", + "user_input = \"I need some expert guidance for building an AI agent. Could you request assistance for me?\"\n", "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "# The config is the **second positional argument** to stream() or invoke()!\n", + "\n", "events = graph.stream(\n", " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n", ")\n", @@ -1381,22 +1354,22 @@ "id": "39405637-13b1-40b1-a51e-6d60bf675ff1", "metadata": {}, "source": [ - "Let's inspect the graph state to confirm it worked." + "The chatbot generated a tool call, but then execution has been interrupted! Note that if we inspect the graph state, we see that it stopped at the tools node:" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "9f511371-98b6-4513-b450-9143778f12dc", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "('human_review_node',)" + "('tools',)" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -1408,51 +1381,28 @@ }, { "cell_type": "markdown", - "id": "89326046-2b11-4812-8b6d-8780306ec275", + "id": "4574b267-b0d8-4056-aeea-6e8817e8844e", "metadata": {}, "source": [ - "**Notice** that unlike last time, the \"next\" node is set to `human_review_node`. We've interrupted here! Let's check the tool invocation." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "3facda0a-e6ad-4b28-b627-753ad8c90c15", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'name': 'tavily_search_results_json',\n", - " 'args': {'query': 'LangGraph programming framework'},\n", - " 'id': 'toolu_01UukDzyzhEhxMgTbshetpT4',\n", - " 'type': 'tool_call'}]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "existing_message = snapshot.values[\"messages\"][-1]\n", - "existing_message.tool_calls" - ] - }, - { - "cell_type": "markdown", - "id": "a55a4c70-7226-4be0-8562-391f72bc1f2b", - "metadata": {}, - "source": [ - "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.\n", + "Let's take a closer look at the `human_assistance` tool:\n", "\n", - "To resume execution, we pass a [Command](../../concepts/human_in_the_loop/#the-command-primitive) object. Here, we specify `resume` with a dict containing the information expected by `human_review_node`. This information can be customized based on our needs." + "```python\n", + "@tool\n", + "def human_assistance(query: str) -> str:\n", + " \"\"\"Request assistance from a human.\"\"\"\n", + " human_response = interrupt({\"query\": query})\n", + " return human_response[\"data\"]\n", + "```\n", + "\n", + "Similar to Python's built-in `input()` function, calling `interrupt` inside the tool will pause execution. Progress is persisted based on our choice of [checkpointer](../../concepts/persistence/#checkpointer-libraries)-- so if we are persisting with Postgres, we can resume at any time as long as the database is alive. Here we are persisting with the in-memory checkpointer, so we can resume any time as long as our Python kernel is running.\n", + "\n", + "To resume execution, we pass a [Command](../../concepts/human_in_the_loop/#the-command-primitive) object containing data expected by the tool. The format of this data can be customized based on our needs. Here, we just need a dict with a key `\"data\"`:" ] }, { "cell_type": "code", - "execution_count": 9, - "id": "831d4978-b5ea-4258-b350-8e7dd2bf2b6c", + "execution_count": 7, + "id": "df6d81bb-e674-44ef-a46e-17b1a8d2381a", "metadata": {}, "outputs": [ { @@ -1461,235 +1411,42 @@ "text": [ "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "[{'text': \"Certainly! I'd be happy to research LangGraph for you. To get the most up-to-date and comprehensive information, I'll use the Tavily search engine to look this up. Let me do that for you now.\", 'type': 'text'}, {'id': 'toolu_01UukDzyzhEhxMgTbshetpT4', 'input': {'query': 'LangGraph programming framework'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", + "[{'text': \"Certainly! I'd be happy to request expert assistance for you regarding building an AI agent. To do this, I'll use the human_assistance function to relay your request. Let me do that for you now.\", 'type': 'text'}, {'id': 'toolu_015tcCygn4UdBM24vg9NSycp', 'input': {'query': 'A user is seeking expert guidance for building an AI agent. Can you provide some professional advice or resources on this topic?'}, 'name': 'human_assistance', 'type': 'tool_use'}]\n", "Tool Calls:\n", - " tavily_search_results_json (toolu_01UukDzyzhEhxMgTbshetpT4)\n", - " Call ID: toolu_01UukDzyzhEhxMgTbshetpT4\n", + " human_assistance (toolu_015tcCygn4UdBM24vg9NSycp)\n", + " Call ID: toolu_015tcCygn4UdBM24vg9NSycp\n", " Args:\n", - " query: LangGraph programming framework\n", + " query: A user is seeking expert guidance for building an AI agent. Can you provide some professional advice or resources on this topic?\n", "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "Name: tavily_search_results_json\n", + "Name: human_assistance\n", "\n", - "[{\"url\": \"https://www.langchain.com/langgraph\", \"content\": \"No. LangGraph is an orchestration framework for complex agentic systems and is more low-level and controllable than LangChain agents. LangChain provides a standard interface to interact with models and other components, useful for straight-forward chains and retrieval flows.\"}, {\"url\": \"https://academy.langchain.com/courses/intro-to-langgraph\", \"content\": \"Separate from the LangChain package, LangGraph helps developers add better precision and control into agentic workflows. Lesson 1: Motivation Lesson 2: Simple Graph Lesson 3: LangGraph Studio Lesson 4: Chain Lesson 5: Router Lesson 6: Agent Lesson 7: Agent with Memory Lesson 8: Deployment Lesson 1: State Schema Lesson 2: State Reducers Lesson 3: Multiple Schemas Lesson 1: Streaming Lesson 2: Breakpoints Lesson 3: Editing State and Human Feedback Lesson 4: Dynamic Breakpoints Lesson 1: Parallelization Lesson 2: Sub-graphs Lesson 3: Map-reduce Lesson 4: Research Assistant About this course No. LangGraph is an orchestration framework for complex agentic systems and is more low-level and controllable than LangChain agents. Deploy LangGraph agents at scale with LangGraph Cloud (available for Python).\"}]\n", + "We, the experts are here to help! We'd recommend you check out LangGraph to build your agent. It's much more reliable and extensible than simple autonomous agents.\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Thank you for your patience. I've researched LangGraph for you, and I'm happy to share what I've found. LangGraph is an interesting framework in the field of language AI. Here's a summary of the key points:\n", + "Thank you for your patience. I've received a response from the expert assistance. Here's the advice they provided:\n", "\n", - "1. Purpose:\n", - " LangGraph is an orchestration framework designed for complex agentic systems. It provides more low-level control and precision compared to LangChain agents.\n", + "The experts recommend that you check out LangGraph for building your AI agent. They suggest that LangGraph is a more reliable and extensible option compared to simple autonomous agents.\n", "\n", - "2. Relationship to LangChain:\n", - " - LangGraph is separate from the LangChain package but complements it.\n", - " - While LangChain provides a standard interface for interacting with models and components (useful for straightforward chains and retrieval flows), LangGraph offers more fine-grained control for complex workflows.\n", + "LangGraph is likely a framework or library designed specifically for creating AI agents. It seems to offer advantages in terms of reliability and extensibility, which are crucial factors when developing complex AI systems.\n", "\n", - "3. Key Features:\n", - " - Better precision and control in agentic workflows\n", - " - Allows for the creation of more complex and controllable AI systems\n", + "To follow up on this recommendation, you might want to:\n", "\n", - "4. Learning Resources:\n", - " There's a course available on the LangChain Academy that covers LangGraph in depth. The course structure includes:\n", + "1. Research LangGraph: Look for its official documentation, tutorials, and any community resources.\n", + "2. Compare LangGraph with other agent-building frameworks to understand its unique benefits.\n", + "3. Check if LangGraph aligns with your specific goals and requirements for your AI agent project.\n", + "4. Look for examples or case studies of AI agents built with LangGraph to get a better idea of its capabilities.\n", "\n", - " - Motivation\n", - " - Simple Graph\n", - " - LangGraph Studio\n", - " - Chain\n", - " - Router\n", - " - Agent\n", - " - Agent with Memory\n", - " - Deployment\n", - " - State Schema\n", - " - State Reducers\n", - " - Multiple Schemas\n", - " - Streaming\n", - " - Breakpoints\n", - " - Editing State and Human Feedback\n", - " - Dynamic Breakpoints\n", - " - Parallelization\n", - " - Sub-graphs\n", - " - Map-reduce\n", - " - Research Assistant\n", - "\n", - "5. Deployment:\n", - " LangGraph agents can be deployed at scale using LangGraph Cloud (available for Python).\n", - "\n", - "LangGraph seems to be particularly useful when you need more control over your AI workflows, especially for complex, multi-step processes or when building sophisticated AI agents. If you're already familiar with LangChain, learning LangGraph could be a valuable next step to enhance your ability to create more advanced AI systems.\n", - "\n", - "Is there any specific aspect of LangGraph you'd like to know more about? Or do you have any questions about how it compares to other frameworks you might be familiar with?\n" + "Would you like me to search for more specific information about LangGraph or any other aspect of building AI agents?\n" ] } ], "source": [ - "human_command = Command(resume={\"action\": \"continue\"})\n", - "\n", - "events = graph.stream(human_command, config, stream_mode=\"values\")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "d789d4cf-b498-454b-a088-3ffc03e511b0", - "metadata": {}, - "source": [ - "Review this call's [LangSmith trace](https://smith.langchain.com/public/83aaec86-9dd8-406b-ab03-e66d2464b8ef/r) 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.\n", - "\n", - "Let's demonstrate another example, in which we direct the chatbot to revise its tool calls based on our feedback." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "0ebad510-01f8-4b00-8e80-1b435b0f37fa", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "Could you search the weather in San Francisco?\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'text': \"Certainly! I'd be happy to search for the current weather in San Francisco for you. To get the most up-to-date and accurate information, I'll use the Tavily search engine. Let me do that for you right away.\", 'type': 'text'}, {'id': 'toolu_019rPs39MK72QMVFcnxEyFN4', 'input': {'query': 'current weather in San Francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", - "Tool Calls:\n", - " tavily_search_results_json (toolu_019rPs39MK72QMVFcnxEyFN4)\n", - " Call ID: toolu_019rPs39MK72QMVFcnxEyFN4\n", - " Args:\n", - " query: current weather in San Francisco\n" - ] - } - ], - "source": [ - "user_input = \"Could you search the weather in San Francisco?\"\n", - "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - "\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "ce1e485c-0ddc-4c36-a1c1-b78f6c706fa1", - "metadata": {}, - "source": [ - "This time, we will indicate that the requested action is `\"feedback\"`, and provide feedback in natural language. The chatbot will respond by generating a second, updated tool call:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "b5bbaf35-d1f2-46b3-8451-856e908fd11f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'text': \"Certainly! I'd be happy to search for the current weather in San Francisco for you. To get the most up-to-date and accurate information, I'll use the Tavily search engine. Let me do that for you right away.\", 'type': 'text'}, {'id': 'toolu_019rPs39MK72QMVFcnxEyFN4', 'input': {'query': 'current weather in San Francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", - "Tool Calls:\n", - " tavily_search_results_json (toolu_019rPs39MK72QMVFcnxEyFN4)\n", - " Call ID: toolu_019rPs39MK72QMVFcnxEyFN4\n", - " Args:\n", - " query: current weather in San Francisco\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "Name: tavily_search_results_json\n", - "\n", - "User requested changes: use format for location.\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'text': 'I apologize for the error in my previous attempt. Thank you for the clarification on the format. Let me search again using the correct format.', 'type': 'text'}, {'id': 'toolu_01AfrULohtNjWqGf8oY1FP9v', 'input': {'query': 'current weather in San Francisco, USA'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", - "Tool Calls:\n", - " tavily_search_results_json (toolu_01AfrULohtNjWqGf8oY1FP9v)\n", - " Call ID: toolu_01AfrULohtNjWqGf8oY1FP9v\n", - " Args:\n", - " query: current weather in San Francisco, USA\n" - ] - } - ], - "source": [ - "human_command = Command(\n", - " resume={\n", - " \"action\": \"feedback\",\n", - " \"data\": \"User requested changes: use format for location.\",\n", - " }\n", + "human_response = (\n", + " \"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent.\"\n", + " \" It's much more reliable and extensible than simple autonomous agents.\"\n", ")\n", "\n", - "events = graph.stream(human_command, config, stream_mode=\"values\")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "b56541f9-a5d2-49e0-9389-8d6e116c58b3", - "metadata": {}, - "source": [ - "We can now continue, as before:" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "24ec8471-4438-4c89-aafa-48bb6c72eca8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'text': 'I apologize for the error in my previous attempt. Thank you for the clarification on the format. Let me search again using the correct format.', 'type': 'text'}, {'id': 'toolu_01AfrULohtNjWqGf8oY1FP9v', 'input': {'query': 'current weather in San Francisco, USA'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", - "Tool Calls:\n", - " tavily_search_results_json (toolu_01AfrULohtNjWqGf8oY1FP9v)\n", - " Call ID: toolu_01AfrULohtNjWqGf8oY1FP9v\n", - " Args:\n", - " query: current weather in San Francisco, USA\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "Name: tavily_search_results_json\n", - "\n", - "[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.775, 'lon': -122.4183, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1736966001, 'localtime': '2025-01-15 10:33'}, 'current': {'last_updated_epoch': 1736965800, 'last_updated': '2025-01-15 10:30', 'temp_c': 10.6, 'temp_f': 51.1, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 6.3, 'wind_kph': 10.1, 'wind_degree': 51, 'wind_dir': 'NE', 'pressure_mb': 1026.0, 'pressure_in': 30.3, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 46, 'cloud': 0, 'feelslike_c': 9.3, 'feelslike_f': 48.8, 'windchill_c': 6.6, 'windchill_f': 43.9, 'heatindex_c': 8.6, 'heatindex_f': 47.5, 'dewpoint_c': 4.6, 'dewpoint_f': 40.2, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 1.3, 'gust_mph': 9.4, 'gust_kph': 15.1}}\"}, {\"url\": \"https://www.meteoprog.com/weather/Sanfrancisco/month/january/\", \"content\": \"San Francisco (United States) weather in January 2025 ☀️ Accurate weather forecast for San Francisco in January ⛅ Detailed forecast By month Current temperature \\\"near me\\\" Weather news ⊳ Widget of weather ⊳ Water temperature | METEOPROG. ... 15 January +14 °+7° 16 January +14\"}]\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "Thank you for your patience. I've successfully searched for the current weather in San Francisco, USA. Based on the search results, I can provide you with the following information about the weather in San Francisco:\n", - "\n", - "1. Current Temperature:\n", - " - 10.6°C (51.1°F)\n", - " - Feels like: 9.3°C (48.8°F)\n", - "\n", - "2. Weather Condition: Sunny\n", - "\n", - "3. Wind:\n", - " - Speed: 10.1 km/h (6.3 mph)\n", - " - Direction: Northeast (NE)\n", - "\n", - "4. Humidity: 46%\n", - "\n", - "5. Precipitation: 0 mm (0 inches)\n", - "\n", - "6. Visibility: 16 km (9 miles)\n", - "\n", - "7. Pressure: 1026.0 mb (30.3 inches)\n", - "\n", - "8. UV Index: 1.3 (relatively low)\n", - "\n", - "The data shows that it's a pleasant, sunny day in San Francisco with comfortable temperatures. The weather is clear with no cloud cover, and there's a light breeze from the northeast. The humidity is moderate, and there's no precipitation expected.\n", - "\n", - "Is there any specific aspect of the weather you'd like more information about?\n" - ] - } - ], - "source": [ - "human_command = Command(resume={\"action\": \"continue\"})\n", + "human_command = Command(resume={\"data\": human_response})\n", "\n", "events = graph.stream(human_command, config, stream_mode=\"values\")\n", "for event in events:\n", @@ -1702,33 +1459,31 @@ "id": "21e78a97-474f-4709-b51d-9d5e8323e14c", "metadata": {}, "source": [ - "In the [LangSmith trace](https://smith.langchain.com/public/1e4bf84c-3e9e-41fd-a924-21395f5cd09e/r) for the above run, we can see full sequence of alternating assistant and tool messages representing the interaction with a human reviewer.\n", + "Our input has been received and processed as a tool message. Review this call's [LangSmith trace](https://smith.langchain.com/public/9f0f87e3-56a7-4dde-9c76-b71675624e91/r) to see the exact work that was done in the above call. Notice that the state is loaded in the first step so that our chatbot can continue where it left off.\n", "\n", - "**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.\n", + "**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**, as long as the underlying persistence layer is running, the graph can be paused **indefinitely** and resumed at any time as if nothing had happened.\n", "\n", - "Next, we'll explore how to further customize the bot's behavior using custom state updates.\n", + "Human-in-the-loop workflows enable a variety of new workflows and user experiences. Check out [this section](../../how-tos/#human-in-the-loop) of the How-to Guides for more examples of Human-in-the-loop workflows, including how to [review and edit tool calls](../../how-tos/human_in_the_loop/review-tool-calls/) before they are executed.\n", "\n", - "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.\n", "\n", "
\n", "Full Code\n", "
\n",
     "\n",
     "```python\n",
-    "from typing import Annotated, Literal\n",
+    "from typing import Annotated\n",
     "\n",
     "from langchain_anthropic import ChatAnthropic\n",
     "from langchain_community.tools.tavily_search import TavilySearchResults\n",
+    "from langchain_core.tools import tool\n",
     "from typing_extensions import TypedDict\n",
     "\n",
     "from langgraph.checkpoint.memory import MemorySaver\n",
     "from langgraph.graph import StateGraph, START, END\n",
     "from langgraph.graph.message import add_messages\n",
-    "from langgraph.prebuilt import ToolNode\n",
+    "from langgraph.prebuilt import ToolNode, tools_condition\n",
     "from langgraph.types import Command, interrupt\n",
     "\n",
-    "memory = MemorySaver()\n",
-    "\n",
     "\n",
     "class State(TypedDict):\n",
     "    messages: Annotated[list, add_messages]\n",
@@ -1737,8 +1492,15 @@
     "graph_builder = StateGraph(State)\n",
     "\n",
     "\n",
+    "@tool\n",
+    "def human_assistance(query: str) -> str:\n",
+    "    \"\"\"Request assistance from a human.\"\"\"\n",
+    "    human_response = interrupt({\"query\": query})\n",
+    "    return human_response[\"data\"]\n",
+    "\n",
+    "\n",
     "tool = TavilySearchResults(max_results=2)\n",
-    "tools = [tool]\n",
+    "tools = [tool, human_assistance]\n",
     "llm = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
     "llm_with_tools = llm.bind_tools(tools)\n",
     "\n",
@@ -1747,62 +1509,20 @@
     "    return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
     "\n",
     "\n",
-    "# We add a node to handle the interaction with a human reviewer\n",
-    "def human_review_node(state: State) -> Command[Literal[\"chatbot\", \"tools\"]]:\n",
-    "    last_message = state[\"messages\"][-1]\n",
-    "    tool_call = last_message.tool_calls[-1]\n",
+    "graph_builder.add_node(\"chatbot\", chatbot)\n",
     "\n",
-    "    # this is the value we'll be providing via Command(resume=)\n",
-    "    human_review = interrupt(\n",
-    "        {\n",
-    "            \"question\": \"Is this correct?\",\n",
-    "            # Surface tool calls for review\n",
-    "            \"tool_call\": tool_call,\n",
-    "        }\n",
-    "    )\n",
-    "\n",
-    "    review_action = human_review[\"action\"]\n",
-    "    review_data = human_review.get(\"data\")\n",
-    "\n",
-    "    # if approved, call the tool\n",
-    "    if review_action == \"continue\":\n",
-    "        return Command(goto=\"tools\")\n",
-    "\n",
-    "    elif review_action == \"feedback\":\n",
-    "        # NOTE: we're adding feedback message as a ToolMessage\n",
-    "        # to preserve the correct order in the message history\n",
-    "        # (AI messages with tool calls need to be followed by tool call messages)\n",
-    "        tool_message = {\n",
-    "            \"role\": \"tool\",\n",
-    "            # This is our natural language feedback\n",
-    "            \"content\": review_data,\n",
-    "            \"name\": tool_call[\"name\"],\n",
-    "            \"tool_call_id\": tool_call[\"id\"],\n",
-    "        }\n",
-    "        return Command(goto=\"chatbot\", update={\"messages\": [tool_message]})\n",
-    "\n",
-    "\n",
-    "# Where we used tools_condition before, here we update the condition\n",
-    "# to route to the human review node first, instead of tools.\n",
-    "def route_after_llm(state) -> Literal[END, \"human_review_node\"]:\n",
-    "    if len(state[\"messages\"][-1].tool_calls) == 0:\n",
-    "        return END\n",
-    "    else:\n",
-    "        return \"human_review_node\"\n",
-    "\n",
-    "\n",
-    "graph_builder.add_node(chatbot)\n",
-    "\n",
-    "tool_node = ToolNode(tools=[tool])\n",
+    "tool_node = ToolNode(tools=tools)\n",
     "graph_builder.add_node(\"tools\", tool_node)\n",
-    "graph_builder.add_node(human_review_node)\n",
     "\n",
     "graph_builder.add_conditional_edges(\n",
     "    \"chatbot\",\n",
-    "    route_after_llm,\n",
+    "    tools_condition,\n",
     ")\n",
     "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
     "graph_builder.add_edge(START, \"chatbot\")\n",
+    "\n",
+    "memory = MemorySaver()\n",
+    "graph = graph_builder.compile(checkpointer=memory)\n",
     "```\n",
     "
\n", "
" @@ -1810,49 +1530,121 @@ }, { "cell_type": "markdown", - "id": "6df38bc4-c177-4ccd-9ec2-83d32bf66722", + "id": "0d12578b-ff19-48b5-b1ad-67d9bf7f710e", "metadata": {}, "source": [ - "## Part 5: Manually Updating the State\n", + "## Part 5: Customizing State\n", "\n", - "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 their agent's course, they'll need to have `write` access.\n", - "\n", - "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.\n", - "\n", - "We'll show how to update a checkpointed state below. As before, first, define your graph. We'll reuse the exact same graph as before." + "So far, we've relied on a simple state with one entry-- 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. Here we will demonstrate a new scenario, in which the chatbot is using its search tool to find specific information, and forwarding them to a human for review. Let's have the chatbot research the birthday of an entity. We will add `name` and `birthday` keys to the state:" ] }, { "cell_type": "code", - "execution_count": null, - "id": "cab7f9c2-fc23-4b68-b84a-a7760b5403c9", + "execution_count": 2, + "id": "84627103-bcc8-4645-bd37-b93209fa09dd", "metadata": {}, "outputs": [], "source": [ - "from typing import Annotated, Literal\n", + "from typing import Annotated\n", "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", "from typing_extensions import TypedDict\n", "\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "from langgraph.graph import StateGraph, START, END\n", "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ToolNode\n", - "from langgraph.types import Command, interrupt\n", - "\n", - "memory = MemorySaver()\n", "\n", "\n", "class State(TypedDict):\n", " messages: Annotated[list, add_messages]\n", + " # highlight-next-line\n", + " name: str\n", + " # highlight-next-line\n", + " birthday: str" + ] + }, + { + "cell_type": "markdown", + "id": "c0057133-3dfd-4208-a11f-9cf7c0b82587", + "metadata": {}, + "source": [ + "Adding this information to the state makes it easily accessible by other graph nodes (e.g., a downstream node that stores or processes the information), as well as the graph's persistence layer.\n", + "\n", + "Here, we will populate the state keys inside of our `human_assistance` tool. This allows a human to review the information before it is stored in the state. We will again use `Command`, this time to issue a state update from inside our tool. Read more about use cases for `Command` [here](../../concepts/low_level/#using-inside-tools)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c4b65504-92c7-4c82-a6ee-824885d1a8a4", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.messages import ToolMessage\n", + "from langchain_core.tools import InjectedToolCallId, tool\n", + "\n", + "from langgraph.types import Command, interrupt\n", "\n", "\n", - "graph_builder = StateGraph(State)\n", + "@tool\n", + "# Note that because we are generating a ToolMessage for a state update, we\n", + "# generally require the ID of the corresponding tool call. We can use\n", + "# LangChain's InjectedToolCallId to signal that this argument should not\n", + "# be revealed to the model in the tool's schema.\n", + "def human_assistance(\n", + " name: str, birthday: str, tool_call_id: Annotated[str, InjectedToolCallId]\n", + ") -> str:\n", + " \"\"\"Request assistance from a human.\"\"\"\n", + " human_response = interrupt(\n", + " {\n", + " \"question\": \"Is this correct?\",\n", + " \"name\": name,\n", + " \"birthday\": birthday,\n", + " },\n", + " )\n", + " # If the information is correct, update the state as-is.\n", + " if human_response.get(\"correct\", \"\").lower().startswith(\"y\"):\n", + " verified_name = name\n", + " verified_birthday = birthday\n", + " response = \"Correct\"\n", + " # Otherwise, receive information from the human reviewer.\n", + " else:\n", + " verified_name = human_response.get(\"name\", name)\n", + " verified_birthday = human_response.get(\"birthday\", birthday)\n", + " response = f\"Made a correction: {human_response}\"\n", + "\n", + " # This time we explicitly update the state with a ToolMessage inside\n", + " # the tool.\n", + " state_update = {\n", + " \"name\": verified_name,\n", + " \"birthday\": verified_birthday,\n", + " \"messages\": [ToolMessage(response, tool_call_id=tool_call_id)],\n", + " }\n", + " return Command(update=state_update)" + ] + }, + { + "cell_type": "markdown", + "id": "268757ca-4b72-4fc1-a482-d878dd1bc0d9", + "metadata": {}, + "source": [ + "Otherwise, the rest of our graph is the same:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "e256fa2e-6c13-42dd-a0a5-d206ee4139bf", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_anthropic import ChatAnthropic\n", + "from langchain_community.tools.tavily_search import TavilySearchResults\n", + "\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.graph import StateGraph, START, END\n", + "from langgraph.prebuilt import ToolNode, tools_condition\n", "\n", "\n", "tool = TavilySearchResults(max_results=2)\n", - "tools = [tool]\n", + "tools = [tool, human_assistance]\n", "llm = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", "llm_with_tools = llm.bind_tools(tools)\n", "\n", @@ -1861,86 +1653,35 @@ " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n", "\n", "\n", - "# We add a node to handle the interaction with a human reviewer\n", - "def human_review_node(state: State) -> Command[Literal[\"chatbot\", \"tools\"]]:\n", - " last_message = state[\"messages\"][-1]\n", - " tool_call = last_message.tool_calls[-1]\n", + "graph_builder = StateGraph(State)\n", + "graph_builder.add_node(\"chatbot\", chatbot)\n", "\n", - " # this is the value we'll be providing via Command(resume=)\n", - " human_review = interrupt(\n", - " {\n", - " \"question\": \"Is this correct?\",\n", - " # Surface tool calls for review\n", - " \"tool_call\": tool_call,\n", - " }\n", - " )\n", - "\n", - " review_action = human_review[\"action\"]\n", - " review_data = human_review.get(\"data\")\n", - "\n", - " # if approved, call the tool\n", - " if review_action == \"continue\":\n", - " return Command(goto=\"tools\")\n", - "\n", - " elif review_action == \"feedback\":\n", - " # NOTE: we're adding feedback message as a ToolMessage\n", - " # to preserve the correct order in the message history\n", - " # (AI messages with tool calls need to be followed by tool call messages)\n", - " tool_message = {\n", - " \"role\": \"tool\",\n", - " # This is our natural language feedback\n", - " \"content\": review_data,\n", - " \"name\": tool_call[\"name\"],\n", - " \"tool_call_id\": tool_call[\"id\"],\n", - " }\n", - " return Command(goto=\"chatbot\", update={\"messages\": [tool_message]})\n", - "\n", - "\n", - "# Where we used tools_condition before, here we update the condition\n", - "# to route to the human review node first, instead of tools.\n", - "def route_after_llm(state) -> Literal[END, \"human_review_node\"]:\n", - " if len(state[\"messages\"][-1].tool_calls) == 0:\n", - " return END\n", - " else:\n", - " return \"human_review_node\"\n", - "\n", - "\n", - "graph_builder.add_node(chatbot)\n", - "\n", - "tool_node = ToolNode(tools=[tool])\n", + "tool_node = ToolNode(tools=tools)\n", "graph_builder.add_node(\"tools\", tool_node)\n", - "graph_builder.add_node(human_review_node)\n", "\n", "graph_builder.add_conditional_edges(\n", " \"chatbot\",\n", - " route_after_llm,\n", + " tools_condition,\n", ")\n", "graph_builder.add_edge(\"tools\", \"chatbot\")\n", - "graph_builder.add_edge(START, \"chatbot\")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "64cc981c-38fd-4ea5-8be5-f077be796411", - "metadata": {}, - "outputs": [], - "source": [ + "graph_builder.add_edge(START, \"chatbot\")\n", + "\n", + "memory = MemorySaver()\n", "graph = graph_builder.compile(checkpointer=memory)" ] }, { "cell_type": "markdown", - "id": "00726b4b-7661-414e-a077-5cabba597163", + "id": "e9f77638-f957-4e1b-abcf-e9132c039be5", "metadata": {}, "source": [ - "We first begin a thread:" + "Let's prompt our application to look up the \"birthday\" of the LangGraph library. We will direct the chatbot to reach out to the `human_assistance` tool once it has the required information. Note that setting `name` and `birthday` in the arguments for the tool, we force the chatbot to generate proposals for these fields." ] }, { "cell_type": "code", - "execution_count": 4, - "id": "4124bf18-302a-4593-b485-247bfd36f150", + "execution_count": 5, + "id": "10701e49-d4b4-46db-bb30-f895fdf4411d", "metadata": {}, "outputs": [ { @@ -1949,20 +1690,36 @@ "text": [ "================================\u001b[1m Human Message \u001b[0m=================================\n", "\n", - "I'm learning LangGraph. Could you do some research on it for me?\n", + "Can you look up when LangGraph was released? When you have the answer, use the human_assistance tool for review.\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "[{'text': \"Certainly! I'd be happy to research LangGraph for you. To get the most up-to-date and comprehensive information, I'll use the Tavily search engine to look this up. Let me do that for you now.\", 'type': 'text'}, {'id': 'toolu_01NKHEFSGNmLeVGvBYDyrALi', 'input': {'query': 'LangGraph: what is it, features, and usage in AI development'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", + "[{'text': \"Certainly! I'll start by searching for information about LangGraph's release date using the Tavily search engine. Then, I'll use the human_assistance tool for review. Let's begin with the search.\", 'type': 'text'}, {'id': 'toolu_01APsgm3wwYXyZQcZJNb3rGJ', 'input': {'query': 'LangGraph release date'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", "Tool Calls:\n", - " tavily_search_results_json (toolu_01NKHEFSGNmLeVGvBYDyrALi)\n", - " Call ID: toolu_01NKHEFSGNmLeVGvBYDyrALi\n", + " tavily_search_results_json (toolu_01APsgm3wwYXyZQcZJNb3rGJ)\n", + " Call ID: toolu_01APsgm3wwYXyZQcZJNb3rGJ\n", " Args:\n", - " query: LangGraph: what is it, features, and usage in AI development\n" + " query: LangGraph release date\n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: tavily_search_results_json\n", + "\n", + "[{\"url\": \"https://blog.langchain.dev/langgraph-cloud/\", \"content\": \"We also have a new stable release of LangGraph. By LangChain 6 min read Jun 27, 2024 (Oct '24) Edit: Since the launch of LangGraph Cloud, we now have multiple deployment options alongside LangGraph Studio - which now fall under LangGraph Platform. LangGraph Cloud is synonymous with our Cloud SaaS deployment option.\"}, {\"url\": \"https://changelog.langchain.com/announcements/langgraph-cloud-deploy-at-scale-monitor-carefully-iterate-boldly\", \"content\": \"LangChain - Changelog | ☁ 🚀 LangGraph Cloud: Deploy at scale, monitor LangChain LangSmith LangGraph LangChain LangSmith LangGraph LangChain LangSmith LangGraph LangChain Changelog Sign up for our newsletter to stay up to date DATE: The LangChain Team LangGraph LangGraph Cloud ☁ 🚀 LangGraph Cloud: Deploy at scale, monitor carefully, iterate boldly DATE: June 27, 2024 AUTHOR: The LangChain Team LangGraph Cloud is now in closed beta, offering scalable, fault-tolerant deployment for LangGraph agents. LangGraph Cloud also includes a new playground-like studio for debugging agent failure modes and quick iteration: Join the waitlist today for LangGraph Cloud. And to learn more, read our blog post announcement or check out our docs. Subscribe By clicking subscribe, you accept our privacy policy and terms and conditions.\"}]\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'text': 'Based on the search results, it appears that LangGraph was released before June 27, 2024. The search results mention a \"new stable release of LangGraph\" and \"LangGraph Cloud\" being announced on that date. However, the exact initial release date of LangGraph is not clearly stated in these results.\\n\\nGiven this information, I\\'ll use the human_assistance tool to review and possibly get more precise information about LangGraph\\'s initial release date.', 'type': 'text'}, {'id': 'toolu_01JtZJvDho75gcLbqcHHp4if', 'input': {'name': 'Assistant', 'birthday': '2023-01-01'}, 'name': 'human_assistance', 'type': 'tool_use'}]\n", + "Tool Calls:\n", + " human_assistance (toolu_01JtZJvDho75gcLbqcHHp4if)\n", + " Call ID: toolu_01JtZJvDho75gcLbqcHHp4if\n", + " Args:\n", + " name: Assistant\n", + " birthday: 2023-01-01\n" ] } ], "source": [ - "user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\n", + "user_input = (\n", + " \"Can you look up when LangGraph was released? \"\n", + " \"When you have the answer, use the human_assistance tool for review.\"\n", + ")\n", "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", "\n", "events = graph.stream(\n", @@ -1975,55 +1732,16 @@ }, { "cell_type": "markdown", - "id": "e3f5b9c1-e259-43f3-bfc6-f9a166a1da71", + "id": "99dcaf45-e4d1-4597-b669-13a5330740ac", "metadata": {}, "source": [ - "Note that we can read the existing state from the `graph` object:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "a6b3bcae-dd04-49da-a4ef-e05634657faf", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'text': \"Certainly! I'd be happy to research LangGraph for you. To get the most up-to-date and comprehensive information, I'll use the Tavily search engine to look this up. Let me do that for you now.\", 'type': 'text'}, {'id': 'toolu_01NKHEFSGNmLeVGvBYDyrALi', 'input': {'query': 'LangGraph: what is it, features, and usage in AI development'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", - "Tool Calls:\n", - " tavily_search_results_json (toolu_01NKHEFSGNmLeVGvBYDyrALi)\n", - " Call ID: toolu_01NKHEFSGNmLeVGvBYDyrALi\n", - " Args:\n", - " query: LangGraph: what is it, features, and usage in AI development\n" - ] - } - ], - "source": [ - "snapshot = graph.get_state(config)\n", - "existing_message = snapshot.values[\"messages\"][-1]\n", - "existing_message.pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "3bf55a26-8c12-477a-9e83-5011d36ac4ee", - "metadata": {}, - "source": [ - "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.\n", - "\n", - "But what if the user wants to intercede? What if we think the chat bot doesn't need to use the tool? \n", - "\n", - "Let's directly provide the correct response!" + "We've hit the `interrupt` in the `human_assistance` tool again. In this case, the chatbot failed to identify the correct date, so we can supply it:" ] }, { "cell_type": "code", "execution_count": 6, - "id": "f0fd4cdb-afc4-4617-b0a9-f018232d641c", + "id": "7df33d9e-cc76-4a0f-8307-01e619483b3e", "metadata": {}, "outputs": [ { @@ -2032,71 +1750,63 @@ "text": [ "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "LangGraph is a library for building stateful, multi-actor applications with LLMs.\n", + "[{'text': 'Based on the search results, it appears that LangGraph was released before June 27, 2024. The search results mention a \"new stable release of LangGraph\" and \"LangGraph Cloud\" being announced on that date. However, the exact initial release date of LangGraph is not clearly stated in these results.\\n\\nGiven this information, I\\'ll use the human_assistance tool to review and possibly get more precise information about LangGraph\\'s initial release date.', 'type': 'text'}, {'id': 'toolu_01JtZJvDho75gcLbqcHHp4if', 'input': {'name': 'Assistant', 'birthday': '2023-01-01'}, 'name': 'human_assistance', 'type': 'tool_use'}]\n", + "Tool Calls:\n", + " human_assistance (toolu_01JtZJvDho75gcLbqcHHp4if)\n", + " Call ID: toolu_01JtZJvDho75gcLbqcHHp4if\n", + " Args:\n", + " name: Assistant\n", + " birthday: 2023-01-01\n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: human_assistance\n", "\n", + "Made a correction: {'name': 'LangGraph', 'birthday': 'Jan 17, 2024'}\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "Last 2 messages;\n", - "[ToolMessage(content='LangGraph is a library for building stateful, multi-actor applications with LLMs.', id='12be5dbe-75eb-45d8-8d64-5ab0c2f8555f', tool_call_id='toolu_01NKHEFSGNmLeVGvBYDyrALi'), AIMessage(content='LangGraph is a library for building stateful, multi-actor applications with LLMs.', additional_kwargs={}, response_metadata={}, id='0f389c32-6fdd-4ad9-bd89-671d38983a70')]\n" + "Thank you for the human assistance. I can now provide you with the correct information about LangGraph's release date.\n", + "\n", + "LangGraph was initially released on January 17, 2024. This information comes directly from the human assistance, which provided a correction to my earlier uncertainty.\n", + "\n", + "To summarize:\n", + "- Initial search results were not clear about the exact release date of LangGraph.\n", + "- Human assistance provided the precise release date: January 17, 2024.\n", + "\n", + "This demonstrates the importance of verifying information, especially for newer technologies or products where online search results might not always provide the most up-to-date or accurate information.\n" ] } ], "source": [ - "from langchain_core.messages import AIMessage, ToolMessage\n", - "\n", - "answer = (\n", - " \"LangGraph is a library for building stateful, multi-actor applications with LLMs.\"\n", - ")\n", - "new_messages = [\n", - " # The LLM API expects some ToolMessage to match its tool call. We'll satisfy that here.\n", - " ToolMessage(content=answer, tool_call_id=existing_message.tool_calls[0][\"id\"]),\n", - " # And then directly \"put words in the LLM's mouth\" by populating its response.\n", - " AIMessage(content=answer),\n", - "]\n", - "\n", - "new_messages[-1].pretty_print()\n", - "graph.update_state(\n", - " # Which state to update\n", - " config,\n", - " # The updated values to provide. The messages in our `State` are \"append-only\", meaning this will be appended\n", - " # to the existing state. We will review how to update existing messages in the next section!\n", - " {\"messages\": new_messages},\n", + "human_command = Command(\n", + " resume={\n", + " \"name\": \"LangGraph\",\n", + " \"birthday\": \"Jan 17, 2024\",\n", + " },\n", ")\n", "\n", - "print(\"\\n\\nLast 2 messages;\")\n", - "print(graph.get_state(config).values[\"messages\"][-2:])" + "events = graph.stream(human_command, config, stream_mode=\"values\")\n", + "for event in events:\n", + " if \"messages\" in event:\n", + " event[\"messages\"][-1].pretty_print()" ] }, { "cell_type": "markdown", - "id": "584de971-6b10-4931-986e-cc35f7adbb3d", + "id": "31581965-8d7d-4378-82b2-f5c2a84a54b1", "metadata": {}, "source": [ - "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. Inspect the [LangSmith trace](https://smith.langchain.com/public/e844af02-7f45-43e2-92bc-13cd3af9b62f/r) of the `update_state` call above to see what's going on.\n", - "\n", - "**Notice** that our new messages are _appended_ to the messages already in the state. Remember how we defined the `State` type?\n", - "\n", - "```python\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - "```\n", - "\n", - "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!\n", - "\n", - "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\"." + "Note that these fields are now reflected in the state:" ] }, { "cell_type": "code", "execution_count": 7, - "id": "d16d95c3-b465-42ac-8015-26b669d45d1f", + "id": "b9fe6275-489d-4a6f-b19f-f1000d4133a0", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'configurable': {'thread_id': '1',\n", - " 'checkpoint_ns': '',\n", - " 'checkpoint_id': '1efd377d-87e6-6ab6-8003-7d963c60dd60'}}" + "{'name': 'LangGraph', 'birthday': 'Jan 17, 2024'}" ] }, "execution_count": 7, @@ -2105,893 +1815,172 @@ } ], "source": [ - "graph.update_state(\n", - " config,\n", - " {\"messages\": [AIMessage(content=\"I'm an AI expert!\")]},\n", - " # Which node for this function to act as. It will automatically continue\n", - " # processing as if this node just ran.\n", - " as_node=\"chatbot\",\n", - ")" + "snapshot = graph.get_state(config)\n", + "\n", + "{k: v for k, v in snapshot.values.items() if k in (\"name\", \"birthday\")}" ] }, { "cell_type": "markdown", - "id": "5a1f0056-6b6f-425f-ac1a-0d4b0e9b85cc", + "id": "14593643-aa81-4b40-9c2d-9408bcaf88cb", "metadata": {}, "source": [ - "Check out the [LangSmith trace](https://smith.langchain.com/public/14edbcaf-a230-45e5-bda2-bd24a9f96cd1/r) for this update call at the provided link. **Notice** from the trace that the graph continues into the `route_after_llm` 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 `route_after_llm` edge and then `__end__` since our updated message lacks tool calls." + "This makes them easily accessible to downstream nodes (e.g., a node that further processes or stores the information)." + ] + }, + { + "cell_type": "markdown", + "id": "238c359a-24ca-4fbf-8f6c-28a347fee2f2", + "metadata": {}, + "source": [ + "### Manually updating state\n", + "\n", + "LangGraph gives a high degree of control over the application state. For instance, at any point (including when interrupted), we can manually override a key using `graph.update_state`:" ] }, { "cell_type": "code", "execution_count": 8, - "id": "f4009ba6-dc0b-4216-ab0c-fbb104616f73", + "id": "b206e600-91f0-4f46-9587-ab00c05899de", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", "text/plain": [ - "" + "{'configurable': {'thread_id': '1',\n", + " 'checkpoint_ns': '',\n", + " 'checkpoint_id': '1efd4395-61ae-6f14-8006-fb093210b222'}}" ] }, + "execution_count": 8, "metadata": {}, - "output_type": "display_data" + "output_type": "execute_result" } ], "source": [ - "from IPython.display import Image, display\n", - "\n", - "try:\n", - " display(Image(graph.get_graph().draw_mermaid_png()))\n", - "except Exception:\n", - " # This requires some extra dependencies and is optional\n", - " pass" + "graph.update_state(config, {\"name\": \"LangGraph (library)\"})" ] }, { "cell_type": "markdown", - "id": "96cd4ffa-8fb2-4bd6-bef9-564cbfe7e3ab", + "id": "3dfb8268-8c5a-4022-9189-6edd231436ae", "metadata": {}, "source": [ - "Inspect the current state as before to confirm the checkpoint reflects our manual updates." + "If we call `graph.get_state`, we can see the new value is reflected:" ] }, { "cell_type": "code", "execution_count": 9, - "id": "d420e813-a8c7-415d-ab31-5298d42491e4", + "id": "10adcb89-55ab-4076-bc81-4488eff9e6b3", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "[ToolMessage(content='LangGraph is a library for building stateful, multi-actor applications with LLMs.', id='12be5dbe-75eb-45d8-8d64-5ab0c2f8555f', tool_call_id='toolu_01NKHEFSGNmLeVGvBYDyrALi'), AIMessage(content='LangGraph is a library for building stateful, multi-actor applications with LLMs.', additional_kwargs={}, response_metadata={}, id='0f389c32-6fdd-4ad9-bd89-671d38983a70'), AIMessage(content=\"I'm an AI expert!\", additional_kwargs={}, response_metadata={}, id='8ea38275-255a-4d64-be3c-c4fcd6c6e55a')]\n", - "()\n" - ] + "data": { + "text/plain": [ + "{'name': 'LangGraph (library)', 'birthday': 'Jan 17, 2024'}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ "snapshot = graph.get_state(config)\n", - "print(snapshot.values[\"messages\"][-3:])\n", - "print(snapshot.next)" + "\n", + "{k: v for k, v in snapshot.values.items() if k in (\"name\", \"birthday\")}" ] }, { "cell_type": "markdown", - "id": "380222f4-65fa-4962-afe6-6a715fadb2de", + "id": "ab34ef46-a836-4dbd-b1ae-56c5e8dc75af", "metadata": {}, "source": [ - "**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).\n", + "Manual state updates will even [generate a trace](https://smith.langchain.com/public/7ebb7827-378d-49fe-9f6c-5df0e90086c8/r) in LangSmith. If desired, they can also be used to control human-in-the-loop workflows, as described in [this guide](how-tos/human_in_the_loop/edit-graph-state/). Use of the `interrupt` function is generally recommended instead, as it allows data to be transmitted in a human-in-the-loop interaction independently of state updates.\n", "\n", - "#### What if you want to **overwrite** existing messages? \n", - "\n", - "The [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/?h=add+messages#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`](https://langchain-ai.github.io/langgraph/reference/graphs/?h=add+messages#add_messages) overwrites the existing message with the new content. \n", - "\n", - "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:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "b5aa8029-927d-4597-88b7-c9fadd4f6f60", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "I'm learning LangGraph. Could you do some research on it for me?\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'text': \"Certainly! I'd be happy to research LangGraph for you. To get the most up-to-date and comprehensive information, I'll use the Tavily search function to look this up. Let me do that for you now.\", 'type': 'text'}, {'id': 'toolu_0135SrrAV1dhbeDtvKzUovUH', 'input': {'query': 'LangGraph python library for language models'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", - "Tool Calls:\n", - " tavily_search_results_json (toolu_0135SrrAV1dhbeDtvKzUovUH)\n", - " Call ID: toolu_0135SrrAV1dhbeDtvKzUovUH\n", - " Args:\n", - " query: LangGraph python library for language models\n" - ] - } - ], - "source": [ - "user_input = \"I'm learning LangGraph. Could you do some research on it for me?\"\n", - "config = {\"configurable\": {\"thread_id\": \"2\"}} # we'll use thread_id = 2 here\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "8b019fc6-7826-4291-9178-6cecb5d7b3d0", - "metadata": {}, - "source": [ - "**Next,** let's update the tool invocation for our agent. Maybe we want to search for human-in-the-loop workflows in particular." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "7215533a-b7e2-4b2d-bc1d-5122b1d06b8b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Original\n", - "Message ID run-c6d77fff-fb85-49df-ad84-07ed4e8a352c-0\n", - "{'name': 'tavily_search_results_json', 'args': {'query': 'LangGraph python library for language models'}, 'id': 'toolu_0135SrrAV1dhbeDtvKzUovUH', 'type': 'tool_call'}\n", - "Updated\n", - "{'name': 'tavily_search_results_json', 'args': {'query': 'LangGraph human-in-the-loop workflow'}, 'id': 'toolu_0135SrrAV1dhbeDtvKzUovUH', 'type': 'tool_call'}\n", - "Message ID run-c6d77fff-fb85-49df-ad84-07ed4e8a352c-0\n", - "\n", - "\n", - "Tool calls\n" - ] - }, - { - "data": { - "text/plain": [ - "[{'name': 'tavily_search_results_json',\n", - " 'args': {'query': 'LangGraph human-in-the-loop workflow'},\n", - " 'id': 'toolu_0135SrrAV1dhbeDtvKzUovUH',\n", - " 'type': 'tool_call'}]" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from langchain_core.messages import AIMessage\n", - "\n", - "snapshot = graph.get_state(config)\n", - "existing_message = snapshot.values[\"messages\"][-1]\n", - "print(\"Original\")\n", - "print(\"Message ID\", existing_message.id)\n", - "print(existing_message.tool_calls[0])\n", - "new_tool_call = existing_message.tool_calls[0].copy()\n", - "new_tool_call[\"args\"][\"query\"] = \"LangGraph human-in-the-loop workflow\"\n", - "new_message = AIMessage(\n", - " content=existing_message.content,\n", - " tool_calls=[new_tool_call],\n", - " # Important! The ID is how LangGraph knows to REPLACE the message in the state rather than APPEND this messages\n", - " id=existing_message.id,\n", - ")\n", - "\n", - "print(\"Updated\")\n", - "print(new_message.tool_calls[0])\n", - "print(\"Message ID\", new_message.id)\n", - "graph.update_state(config, {\"messages\": [new_message]})\n", - "\n", - "print(\"\\n\\nTool calls\")\n", - "graph.get_state(config).values[\"messages\"][-1].tool_calls" - ] - }, - { - "cell_type": "markdown", - "id": "680f0ebd-ebce-4de6-8a9b-37d3d4ef0234", - "metadata": {}, - "source": [ - "**Notice** that we've modified the AI's tool invocation to search for \"LangGraph human-in-the-loop workflow\" instead of the simple \"LangGraph\".\n", - "\n", - "Check out the [LangSmith trace](https://smith.langchain.com/public/0a13df28-1509-4974-9f3a-09af34671ec9/r) to see the state update call - you can see our new message has successfully updated the previous AI message.\n", - "\n", - "Resume the graph as before, by passing the appropriate `Command`:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "7e9ebc73-22e8-4af2-9a36-6678873c671f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'text': \"Certainly! I'd be happy to research LangGraph for you. To get the most up-to-date and comprehensive information, I'll use the Tavily search function to look this up. Let me do that for you now.\", 'type': 'text'}, {'id': 'toolu_0135SrrAV1dhbeDtvKzUovUH', 'input': {'query': 'LangGraph python library for language models'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", - "Tool Calls:\n", - " tavily_search_results_json (toolu_0135SrrAV1dhbeDtvKzUovUH)\n", - " Call ID: toolu_0135SrrAV1dhbeDtvKzUovUH\n", - " Args:\n", - " query: LangGraph human-in-the-loop workflow\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "Name: tavily_search_results_json\n", - "\n", - "[{\"url\": \"https://www.youtube.com/watch?v=9BPCV5TYPmg\", \"content\": \"In this video, I'll show you how to handle persistence with LangGraph, enabling a unique Human-in-the-Loop workflow. This approach allows a human to grant an\"}, {\"url\": \"https://blog.langchain.dev/human-in-the-loop-with-opengpts-and-langgraph/\", \"content\": \"TLDR; Today we're launching two \\\"human in the loop\\\" features in OpenGPTs, Interrupt and Authorize, both powered by LangGraph. We've recently launched LangGraph, a library to help developers build multi-actor, multi-step, stateful LLM applications. That's a lot words packed into a short sentence, let's take it one at a time. Multi-actor\"}]\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "Thank you for your patience. I've found some useful information about LangGraph, particularly focusing on its human-in-the-loop workflow capabilities. Let me summarize what I've learned:\n", - "\n", - "1. What is LangGraph:\n", - " LangGraph is a library designed to help developers build complex, stateful LLM (Large Language Model) applications. It's particularly useful for creating multi-actor and multi-step processes.\n", - "\n", - "2. Human-in-the-Loop Workflow:\n", - " One of the key features of LangGraph is its support for human-in-the-loop workflows. This means it allows for human intervention and interaction within AI-driven processes.\n", - "\n", - "3. OpenGPTs Integration:\n", - " LangGraph has been integrated with OpenGPTs, introducing two human-in-the-loop features:\n", - " a) Interrupt: This likely allows a human to pause or intervene in an ongoing AI process.\n", - " b) Authorize: This feature probably enables human approval or authorization at certain steps of the AI workflow.\n", - "\n", - "4. Persistence Handling:\n", - " LangGraph includes functionality for handling persistence. This is important for maintaining state across different steps of a workflow, especially when human interaction is involved.\n", - "\n", - "5. Use Cases:\n", - " The human-in-the-loop capabilities make LangGraph suitable for applications where you need a blend of AI efficiency and human oversight or decision-making. This could be particularly useful in fields like content moderation, complex decision processes, or scenarios where ethical considerations are important.\n", - "\n", - "6. Developer Focus:\n", - " LangGraph seems to be targeted at developers who are looking to create more sophisticated LLM applications that go beyond simple query-response models.\n", - "\n", - "To learn more about LangGraph, you might want to:\n", - "1. Check out the official documentation or GitHub repository for LangGraph.\n", - "2. Watch tutorial videos, like the one mentioned in the search results, which demonstrates how to handle persistence with LangGraph.\n", - "3. Explore the OpenGPTs platform to see practical implementations of LangGraph's human-in-the-loop features.\n", - "4. Try building a simple application using LangGraph to get hands-on experience with its capabilities.\n", - "\n", - "Would you like me to search for more specific information about any aspect of LangGraph, such as its installation process, core components, or example use cases?\n" - ] - } - ], - "source": [ - "human_command = Command(resume={\"action\": \"continue\"})\n", - "\n", - "events = graph.stream(human_command, config, stream_mode=\"values\")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "090b680b-f53f-4af2-a432-45f8c5a10779", - "metadata": {}, - "source": [ - "Check out the [trace](https://smith.langchain.com/public/e5b0b999-b1cd-43cf-bf75-df0b35bbe366/r/1f17ded9-6958-4682-9a0d-9d7eedbf7136) to see the tool call 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!\n", - "\n", - "All of this is reflected in the graph's checkpointed memory, meaning if we continue the conversation, it will recall all the _modified_ state." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "556c71d3-48ac-43d7-833c-c3f884b330a7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "Remember what I'm learning about?\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "Of course! You mentioned that you're learning LangGraph. I apologize for not explicitly referencing that in my response. You're right to bring that back into focus. \n", - "\n", - "Since you're in the process of learning LangGraph, it's important to tailor the information to your learning journey. Based on what we've found, here are some key points that might be particularly relevant for someone learning LangGraph:\n", - "\n", - "1. Core Concept: LangGraph is a library for building stateful, multi-step LLM applications. As you're learning it, focus on understanding how it manages state across different steps of a process.\n", - "\n", - "2. Human-in-the-Loop: One of LangGraph's key features is its support for human-in-the-loop workflows. This could be an interesting area to explore in your learning, as it allows you to create AI systems that can interact with humans during execution.\n", - "\n", - "3. Integration with OpenGPTs: Learning how LangGraph integrates with other tools like OpenGPTs could be valuable. The \"Interrupt\" and \"Authorize\" features might be good practical examples to study.\n", - "\n", - "4. Persistence Handling: As a learner, understanding how LangGraph handles persistence could be crucial. This relates to how the library maintains state across different steps of your AI workflows.\n", - "\n", - "5. Practical Application: The video mentioned in the search results about handling persistence with LangGraph could be a great resource for your learning. It seems to offer a practical demonstration, which is often very helpful when learning a new technology.\n", - "\n", - "To support your learning process, you might want to:\n", - "- Start with simple LangGraph projects and gradually increase complexity\n", - "- Focus on understanding the state management aspects of LangGraph\n", - "- Experiment with the human-in-the-loop features to see how they work in practice\n", - "- Look for tutorials or courses specifically designed for learning LangGraph\n", - "\n", - "Is there a particular aspect of LangGraph that you're finding challenging or especially interesting in your learning journey? Or would you like me to find more beginner-friendly resources for learning LangGraph?\n" - ] - } - ], - "source": [ - "events = graph.stream(\n", - " {\n", - " \"messages\": (\n", - " \"user\",\n", - " \"Remember what I'm learning about?\",\n", - " )\n", - " },\n", - " config,\n", - " stream_mode=\"values\",\n", - ")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "a5166e1b-96a6-4ac0-88a1-bf32a422134a", - "metadata": {}, - "source": [ - "**Congratulations!** You've used `interrupt` 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 persistent 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!" - ] - }, - { - "cell_type": "markdown", - "id": "d88d4c9e-65c8-4093-a6c2-c261475f7c07", - "metadata": {}, - "source": [ - "## Part 6: Customizing State\n", - "\n", - "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.\n", - "\n", - "In the examples above, we involved a human deterministically: the graph __always__ interrupted whenever a tool was invoked. Suppose we wanted our chat bot to have the choice of relying on a human.\n", - "\n", - "One way to do this is route to the `human_review_node` only if the LLM invokes a \"human\" tool. We will include an `ask_human` flag in our graph state that we will flip if the LLM calls this tool. This is not needed for the routing process, but conveniently indicates if a run required human assistance in the output.\n", - "\n", - "Below, define this new graph, with an updated `State`:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "3cf7e042-1718-4625-ae30-a9917f595449", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Annotated, Literal\n", - "\n", - "from langchain_anthropic import ChatAnthropic\n", - "from langchain_community.tools.tavily_search import TavilySearchResults\n", - "from typing_extensions import TypedDict\n", - "\n", - "from langgraph.checkpoint.memory import MemorySaver\n", - "from langgraph.graph import StateGraph, START, END\n", - "from langgraph.graph.message import add_messages\n", - "from langgraph.prebuilt import ToolNode\n", - "from langgraph.types import Command, interrupt\n", - "\n", - "\n", - "class State(TypedDict):\n", - " messages: Annotated[list, add_messages]\n", - " # This flag is new\n", - " # highlight-next-line\n", - " ask_human: bool" - ] - }, - { - "cell_type": "markdown", - "id": "e87f2cb8-c066-4b54-acc4-e8c7399c5f3d", - "metadata": {}, - "source": [ - "Next, define a schema to show the model to let it decide to request assistance." - ] - }, - { - "cell_type": "markdown", - "id": "7bd3d704-5bee-4872-8d12-992bc970c158", - "metadata": {}, - "source": [ - "
\n", - "

Using Pydantic with LangChain

\n", - "

\n", - " This notebook uses Pydantic v2 BaseModel, which requires langchain-core >= 0.3. Using langchain-core < 0.3 will result in errors due to mixing of Pydantic v1 and v2 BaseModels.\n", - "

\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "e5192e54-6a28-42fe-a8a7-62d45d61f994", - "metadata": {}, - "outputs": [], - "source": [ - "from pydantic import BaseModel\n", - "\n", - "\n", - "class RequestAssistance(BaseModel):\n", - " \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n", - "\n", - " To use this function, relay the user's 'request' so the expert can provide the right guidance.\n", - " \"\"\"\n", - "\n", - " request: str" - ] - }, - { - "cell_type": "markdown", - "id": "338c1bf4-8d00-49c3-9809-ecea290fa152", - "metadata": {}, - "source": [ - "We include this as a new tool accessible to the model:" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "75cf03b1-c546-4585-8d2f-4a8be296fd89", - "metadata": {}, - "outputs": [], - "source": [ - "tool = TavilySearchResults(max_results=2)\n", - "tools = [tool]\n", - "llm = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", - "# We can bind the llm to a tool definition, a pydantic model, or a json schema\n", - "llm_with_tools = llm.bind_tools(tools + [RequestAssistance])" - ] - }, - { - "cell_type": "markdown", - "id": "2b19c61b-2087-463b-adf8-96dbc193f41c", - "metadata": {}, - "source": [ - "Otherwise, we just update the `human_review_node` to short-circuit and route directly to `tools` if assistance is not requested. If it is requested, we flip the new flag in the state." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "d7da1212-dc9c-4385-a0a0-c4c3bc30e633", - "metadata": {}, - "outputs": [], - "source": [ - "def chatbot(state: State):\n", - " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n", - "\n", - "\n", - "def human_review_node(state: State) -> Command[Literal[\"chatbot\", \"tools\"]]:\n", - " last_message = state[\"messages\"][-1]\n", - " tool_call = last_message.tool_calls[-1]\n", - "\n", - " # highlight-next-line\n", - " if not tool_call[\"name\"] == RequestAssistance.__name__:\n", - " return Command(goto=\"tools\")\n", - "\n", - " human_review = interrupt(\n", - " {\n", - " \"question\": \"Is this correct?\",\n", - " \"tool_call\": tool_call,\n", - " }\n", - " )\n", - "\n", - " review_action = human_review[\"action\"]\n", - " review_data = human_review.get(\"data\")\n", - "\n", - " if review_action == \"continue\":\n", - " # highlight-next-line\n", - " return Command(goto=\"tools\", update={\"ask_human\": True})\n", - "\n", - " elif review_action == \"feedback\":\n", - " tool_message = {\n", - " \"role\": \"tool\",\n", - " \"content\": review_data,\n", - " \"name\": tool_call[\"name\"],\n", - " \"tool_call_id\": tool_call[\"id\"],\n", - " }\n", - " return Command(\n", - " goto=\"chatbot\",\n", - " # highlight-next-line\n", - " update={\"messages\": [tool_message], \"ask_human\": True},\n", - " )\n", - "\n", - "\n", - "def route_after_llm(state) -> Literal[END, \"human_review_node\"]:\n", - " if len(state[\"messages\"][-1].tool_calls) == 0:\n", - " return END\n", - " else:\n", - " return \"human_review_node\"" - ] - }, - { - "cell_type": "markdown", - "id": "04ca0f57-2519-49c2-9499-888b5a884897", - "metadata": {}, - "source": [ - "Next, create the graph builder and add the chatbot and tools nodes to the graph, same as before." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8db38d3e-f2ff-4d9d-b267-27729be8e060", - "metadata": {}, - "outputs": [], - "source": [ - "graph_builder = StateGraph(State)\n", - "\n", - "graph_builder.add_node(chatbot)\n", - "\n", - "tool_node = ToolNode(tools=[tool])\n", - "graph_builder.add_node(\"tools\", tool_node)\n", - "graph_builder.add_node(human_review_node)\n", - "\n", - "graph_builder.add_conditional_edges(\n", - " \"chatbot\",\n", - " route_after_llm,\n", - ")\n", - "graph_builder.add_edge(\"tools\", \"chatbot\")\n", - "graph_builder.add_edge(START, \"chatbot\")" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "d9d77f37-63cd-4ed3-86c6-ddaac62b39e9", - "metadata": {}, - "outputs": [], - "source": [ - "memory = MemorySaver()\n", - "\n", - "graph = graph_builder.compile(checkpointer=memory)" - ] - }, - { - "cell_type": "markdown", - "id": "a3b73851-810e-4466-89d8-37fba87e8494", - "metadata": {}, - "source": [ - "The chat bot can either request help from a human (chatbot->select->human), invoke the search engine tool (chatbot->select->action), or directly respond (chatbot->select->__end__). Once an action or request has been made, the graph will transition back to the `chatbot` node to continue operations.\n", - "\n", - "Let's see this graph in action. We first ask it a question that requires no human intervention:" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "00a11e0c-937d-4f11-a623-bf16ee1f6d4e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "Can you search for the weather in San Francisco?\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'text': \"Certainly! I'd be happy to search for the weather in San Francisco for you. To get the most up-to-date and accurate information, I'll use the search function to look this up. Let me do that for you right away.\", 'type': 'text'}, {'id': 'toolu_01QvcSUFnMXDrK4dWJ4g34P7', 'input': {'query': 'current weather in San Francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n", - "Tool Calls:\n", - " tavily_search_results_json (toolu_01QvcSUFnMXDrK4dWJ4g34P7)\n", - " Call ID: toolu_01QvcSUFnMXDrK4dWJ4g34P7\n", - " Args:\n", - " query: current weather in San Francisco\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "Name: tavily_search_results_json\n", - "\n", - "[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.775, 'lon': -122.4183, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1736974398, 'localtime': '2025-01-15 12:53'}, 'current': {'last_updated_epoch': 1736973900, 'last_updated': '2025-01-15 12:45', 'temp_c': 11.7, 'temp_f': 53.1, 'is_day': 1, 'condition': {'text': 'Sunny', 'icon': '//cdn.weatherapi.com/weather/64x64/day/113.png', 'code': 1000}, 'wind_mph': 5.8, 'wind_kph': 9.4, 'wind_degree': 45, 'wind_dir': 'NE', 'pressure_mb': 1025.0, 'pressure_in': 30.27, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 41, 'cloud': 0, 'feelslike_c': 10.8, 'feelslike_f': 51.4, 'windchill_c': 9.6, 'windchill_f': 49.3, 'heatindex_c': 11.1, 'heatindex_f': 51.9, 'dewpoint_c': 6.3, 'dewpoint_f': 43.3, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 2.4, 'gust_mph': 7.7, 'gust_kph': 12.4}}\"}, {\"url\": \"https://www.yahoo.com/news/january-15-2025-san-francisco-135157305.html\", \"content\": \"January 15, 2025 San Francisco Bay Area weather forecast Search query Search the web News Finance Sports Manage your account Add or switch accounts Search the web Wildfire damage from space L.A. fires underlying causes Rent hikes amid wildfires Photos: Wildfire devastation L.A. wildfires live updates KRON San Francisco January 15, 2025 San Francisco Bay Area weather forecast KRON San Francisco Wed, January 15, 2025 at 1:51 PM UTC KRON4 Meteorologist Gayle Ong has the latest Bay Area weather outlook. https://www.kron4.com/weather/san-francisco-bay-area-weather-forecast/ Solve the daily Crossword 28,373 people played the daily Crossword recently. Can you solve it faster than others?28,373 people played the daily Crossword recently. Can you solve it faster than others? Crossword Play on Yahoo Yahoo! © 2024 Yahoo.\"}]\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "Thank you for your patience. I've searched for the current weather in San Francisco, and I have the latest information for you. Here's the weather report for San Francisco:\n", - "\n", - "Date and Time: January 15, 2025, 12:45 PM local time\n", - "Temperature: 11.7°C (53.1°F)\n", - "Condition: Sunny\n", - "Wind: 5.8 mph (9.4 km/h), coming from the Northeast\n", - "Humidity: 41%\n", - "Precipitation: 0 mm (0 inches)\n", - "Visibility: 16 km (9 miles)\n", - "UV Index: 2.4 (low)\n", - "\n", - "It's a nice day in San Francisco with clear, sunny skies. The temperature is mild, typical for a winter day in the city. The wind is light, and there's no precipitation expected. It's a good day for outdoor activities, but you might want to bring a light jacket as it's not too warm.\n", - "\n", - "Is there anything specific about the weather you'd like to know more about?\n" - ] - } - ], - "source": [ - "user_input = \"Can you search for the weather in San Francisco?\"\n", - "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", - "\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "0589f4c9-5ad9-42f0-818a-7e1fcad75517", - "metadata": {}, - "source": [ - "This works as expected, and the state indicates that no human assistance was requested:" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "5d6e14ce-4687-48e2-a488-dca48aa9722b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "False" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "graph.get_state(config).values.get(\"ask_human\", False)" - ] - }, - { - "cell_type": "markdown", - "id": "a06c0ce3-dc78-40a2-8ae2-7f4429d4bf9d", - "metadata": {}, - "source": [ - "We next request for expert assistance:" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "b789d334-9222-412c-83f2-0298571ce5c2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "================================\u001b[1m Human Message \u001b[0m=================================\n", - "\n", - "I need some expert guidance for building this AI agent. Could you request assistance for me?\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'text': \"Certainly! I understand that you need expert guidance for building an AI agent. This is a complex topic that would benefit from specialized knowledge. I'll use the RequestAssistance function to escalate your request to an expert who can provide more in-depth support.\", 'type': 'text'}, {'id': 'toolu_01LruhQDWhxozxHJ94qDndEF', 'input': {'request': 'The user needs expert guidance for building an AI agent. They are looking for specialized knowledge and support in this area.'}, 'name': 'RequestAssistance', 'type': 'tool_use'}]\n", - "Tool Calls:\n", - " RequestAssistance (toolu_01LruhQDWhxozxHJ94qDndEF)\n", - " Call ID: toolu_01LruhQDWhxozxHJ94qDndEF\n", - " Args:\n", - " request: The user needs expert guidance for building an AI agent. They are looking for specialized knowledge and support in this area.\n" - ] - } - ], - "source": [ - "user_input = \"I need some expert guidance for building this AI agent. Could you request assistance for me?\"\n", - "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", - "\n", - "events = graph.stream(\n", - " {\"messages\": [(\"user\", user_input)]}, config, stream_mode=\"values\"\n", - ")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "ed2dd02e-f0a6-4f63-a7d6-e49ecf40db21", - "metadata": {}, - "source": [ - "We can respond as before, by constructing an appropriate `Command`:" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "id": "a91f333b-8c4d-44cb-ae6a-0a5e8ed4f23b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "[{'text': \"Certainly! I understand that you need expert guidance for building an AI agent. This is a complex topic that would benefit from specialized knowledge. I'll use the RequestAssistance function to escalate your request to an expert who can provide more in-depth support.\", 'type': 'text'}, {'id': 'toolu_01LruhQDWhxozxHJ94qDndEF', 'input': {'request': 'The user needs expert guidance for building an AI agent. They are looking for specialized knowledge and support in this area.'}, 'name': 'RequestAssistance', 'type': 'tool_use'}]\n", - "Tool Calls:\n", - " RequestAssistance (toolu_01LruhQDWhxozxHJ94qDndEF)\n", - " Call ID: toolu_01LruhQDWhxozxHJ94qDndEF\n", - " Args:\n", - " request: The user needs expert guidance for building an AI agent. They are looking for specialized knowledge and support in this area.\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "Name: RequestAssistance\n", - "\n", - "We, the experts are here to help! We'd recommend you check out LangGraph to build your agent. It's much more reliable and extensible than simple autonomous agents.\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "Thank you for your patience. I've escalated your request, and an expert has provided some initial guidance. Here's what they recommend:\n", - "\n", - "The experts suggest that you look into LangGraph for building your AI agent. They mention that LangGraph is a more reliable and extensible option compared to simple autonomous agents.\n", - "\n", - "LangGraph is likely a framework or library designed specifically for creating advanced AI agents. It seems to offer benefits in terms of reliability and extensibility, which are crucial factors when developing complex AI systems.\n", - "\n", - "To follow up on this recommendation, you might want to:\n", - "\n", - "1. Research LangGraph to understand its features, capabilities, and how it compares to other agent-building frameworks.\n", - "2. Look for documentation, tutorials, or guides on how to get started with LangGraph.\n", - "3. Consider any specific requirements or goals you have for your AI agent and how LangGraph might address them.\n", - "\n", - "Do you have any specific questions about LangGraph or particular aspects of AI agent development that you'd like me to try to find more information about? I'd be happy to help you dig deeper into this topic.\n" - ] - } - ], - "source": [ - "human_response = (\n", - " \"We, the experts are here to help! We'd recommend you check out LangGraph to build your agent.\"\n", - " \" It's much more reliable and extensible than simple autonomous agents.\"\n", - ")\n", - "\n", - "human_command = Command(resume={\"action\": \"feedback\", \"data\": human_response})\n", - "\n", - "events = graph.stream(human_command, config, stream_mode=\"values\")\n", - "for event in events:\n", - " if \"messages\" in event:\n", - " event[\"messages\"][-1].pretty_print()" - ] - }, - { - "cell_type": "markdown", - "id": "79492363-7fc6-4ec7-977d-9030648029bc", - "metadata": {}, - "source": [ - "You can inspect the state to it's been flagged:" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "d2c94b9d-fbbd-4131-bd49-6c95d8c3708b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "graph.get_state(config).values[\"ask_human\"]" - ] - }, - { - "cell_type": "markdown", - "id": "48e0559b-d653-4dab-8928-b001004d14cb", - "metadata": {}, - "source": [ - "**Notice** that the chat bot has incorporated the updated state in its final response. Since **everything** was checkpointed, the \"expert\" human in the loop could perform the update at any time without impacting the graph's execution.\n", + "**Congratulations!** You've added custom keys to the state to facilitate a more complex workflow, and learned how to generate state updates from inside tools.\n", "\n", "We're almost done with the tutorial, but there is one more concept we'd like to review before finishing that connects `checkpointing` and `state updates`. \n", "\n", "This section's code is reproduced below for your reference.\n", "\n", + "\n", "
\n", "Full Code\n", "
\n",
     "\n",
     "```python\n",
-    "from typing import Annotated, Literal\n",
+    "from typing import Annotated\n",
     "\n",
     "from langchain_anthropic import ChatAnthropic\n",
     "from langchain_community.tools.tavily_search import TavilySearchResults\n",
-    "from pydantic import BaseModel\n",
+    "from langchain_core.messages import ToolMessage\n",
+    "from langchain_core.tools import InjectedToolCallId, tool\n",
     "from typing_extensions import TypedDict\n",
     "\n",
     "from langgraph.checkpoint.memory import MemorySaver\n",
     "from langgraph.graph import StateGraph, START, END\n",
     "from langgraph.graph.message import add_messages\n",
-    "from langgraph.prebuilt import ToolNode\n",
+    "from langgraph.prebuilt import ToolNode, tools_condition\n",
     "from langgraph.types import Command, interrupt\n",
     "\n",
     "\n",
+    "\n",
     "class State(TypedDict):\n",
     "    messages: Annotated[list, add_messages]\n",
-    "    # This flag is new\n",
-    "    ask_human: bool\n",
+    "    name: str\n",
+    "    birthday: str\n",
     "\n",
     "\n",
-    "class RequestAssistance(BaseModel):\n",
-    "    \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n",
+    "@tool\n",
+    "def human_assistance(\n",
+    "    name: str, birthday: str, tool_call_id: Annotated[str, InjectedToolCallId]\n",
+    ") -> str:\n",
+    "    \"\"\"Request assistance from a human.\"\"\"\n",
+    "    human_response = interrupt(\n",
+    "        {\n",
+    "            \"question\": \"Is this correct?\",\n",
+    "            \"name\": name,\n",
+    "            \"birthday\": birthday,\n",
+    "        },\n",
+    "    )\n",
+    "    if human_response.get(\"correct\", \"\").lower().startswith(\"y\"):\n",
+    "        verified_name = name\n",
+    "        verified_birthday = birthday\n",
+    "        response = \"Correct\"\n",
+    "    else:\n",
+    "        verified_name = human_response.get(\"name\", name)\n",
+    "        verified_birthday = human_response.get(\"birthday\", birthday)\n",
+    "        response = f\"Made a correction: {human_response}\"\n",
     "\n",
-    "    To use this function, relay the user's 'request' so the expert can provide the right guidance.\n",
-    "    \"\"\"\n",
-    "\n",
-    "    request: str\n",
+    "    state_update = {\n",
+    "        \"name\": verified_name,\n",
+    "        \"birthday\": verified_birthday,\n",
+    "        \"messages\": [ToolMessage(response, tool_call_id=tool_call_id)],\n",
+    "    }\n",
+    "    return Command(update=state_update)\n",
     "\n",
     "\n",
     "tool = TavilySearchResults(max_results=2)\n",
-    "tools = [tool]\n",
+    "tools = [tool, human_assistance]\n",
     "llm = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
-    "llm_with_tools = llm.bind_tools(tools + [RequestAssistance])\n",
+    "llm_with_tools = llm.bind_tools(tools)\n",
     "\n",
     "\n",
     "def chatbot(state: State):\n",
     "    return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
     "\n",
     "\n",
-    "def human_review_node(state: State) -> Command[Literal[\"chatbot\", \"tools\"]]:\n",
-    "    last_message = state[\"messages\"][-1]\n",
-    "    tool_call = last_message.tool_calls[-1]\n",
-    "\n",
-    "    if not tool_call[\"name\"] == RequestAssistance.__name__:\n",
-    "        return Command(goto=\"tools\")\n",
-    "\n",
-    "    human_review = interrupt(\n",
-    "        {\n",
-    "            \"question\": \"Is this correct?\",\n",
-    "            \"tool_call\": tool_call,\n",
-    "        }\n",
-    "    )\n",
-    "\n",
-    "    review_action = human_review[\"action\"]\n",
-    "    review_data = human_review.get(\"data\")\n",
-    "\n",
-    "    if review_action == \"continue\":\n",
-    "        return Command(goto=\"tools\", update={\"ask_human\": True})\n",
-    "\n",
-    "    elif review_action == \"feedback\":\n",
-    "        tool_message = {\n",
-    "            \"role\": \"tool\",\n",
-    "            \"content\": review_data,\n",
-    "            \"name\": tool_call[\"name\"],\n",
-    "            \"tool_call_id\": tool_call[\"id\"],\n",
-    "        }\n",
-    "        return Command(\n",
-    "            goto=\"chatbot\",\n",
-    "            update={\"messages\": [tool_message], \"ask_human\": True},\n",
-    "        )\n",
-    "\n",
-    "\n",
-    "def route_after_llm(state) -> Literal[END, \"human_review_node\"]:\n",
-    "    if len(state[\"messages\"][-1].tool_calls) == 0:\n",
-    "        return END\n",
-    "    else:\n",
-    "        return \"human_review_node\"\n",
-    "\n",
-    "\n",
     "graph_builder = StateGraph(State)\n",
+    "graph_builder.add_node(\"chatbot\", chatbot)\n",
     "\n",
-    "graph_builder.add_node(chatbot)\n",
-    "\n",
-    "tool_node = ToolNode(tools=[tool])\n",
+    "tool_node = ToolNode(tools=tools)\n",
     "graph_builder.add_node(\"tools\", tool_node)\n",
-    "graph_builder.add_node(human_review_node)\n",
     "\n",
     "graph_builder.add_conditional_edges(\n",
     "    \"chatbot\",\n",
-    "    route_after_llm,\n",
+    "    tools_condition,\n",
     ")\n",
     "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
     "graph_builder.add_edge(START, \"chatbot\")\n",
@@ -3010,7 +1999,7 @@
    "source": [
     "## Part 7: Time Travel\n",
     "\n",
-    "In a typical chat bot workflow, the user interacts with the bot 1 or more times to accomplish a task. In the previous sections, we saw how to add memory and a human-in-the-loop to be able to checkpoint our graph state and manually override the state to control future responses.\n",
+    "In a typical chat bot workflow, the user interacts with the bot 1 or more times to accomplish a task. In the previous sections, we saw how to add memory and a human-in-the-loop to be able to checkpoint our graph state and control future responses.\n",
     "\n",
     "But what if you want to let your user start from a previous response and \"branch off\" to explore a separate outcome? Or what if you want users to be able to \"rewind\" your assistant's work to fix some mistakes or try a different strategy (common in applications like autonomous software engineers)?\n",
     "\n",
@@ -3018,106 +2007,54 @@
     "\n",
     "In this section, you will \"rewind\" your graph by fetching a checkpoint using the graph's `get_state_history` method. You can then resume execution at this previous point in time.\n",
     "\n",
-    "First, recall our chatbot graph. We don't need to make **any** changes from before:"
+    "For this, let's use the simple chatbot with tools from [Part 3](#part-3-adding-memory-to-the-chatbot):"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": 10,
    "id": "bb8a02de-a21b-4ef6-a714-7d6e44435e3a",
    "metadata": {},
    "outputs": [],
    "source": [
-    "from typing import Annotated, Literal\n",
+    "from typing import Annotated\n",
     "\n",
     "from langchain_anthropic import ChatAnthropic\n",
     "from langchain_community.tools.tavily_search import TavilySearchResults\n",
-    "from pydantic import BaseModel\n",
+    "from langchain_core.messages import BaseMessage\n",
     "from typing_extensions import TypedDict\n",
     "\n",
     "from langgraph.checkpoint.memory import MemorySaver\n",
     "from langgraph.graph import StateGraph, START, END\n",
     "from langgraph.graph.message import add_messages\n",
-    "from langgraph.prebuilt import ToolNode\n",
-    "from langgraph.types import Command, interrupt\n",
+    "from langgraph.prebuilt import ToolNode, tools_condition\n",
     "\n",
     "\n",
     "class State(TypedDict):\n",
     "    messages: Annotated[list, add_messages]\n",
-    "    # This flag is new\n",
-    "    ask_human: bool\n",
     "\n",
     "\n",
-    "class RequestAssistance(BaseModel):\n",
-    "    \"\"\"Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.\n",
-    "\n",
-    "    To use this function, relay the user's 'request' so the expert can provide the right guidance.\n",
-    "    \"\"\"\n",
-    "\n",
-    "    request: str\n",
+    "graph_builder = StateGraph(State)\n",
     "\n",
     "\n",
     "tool = TavilySearchResults(max_results=2)\n",
     "tools = [tool]\n",
     "llm = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n",
-    "llm_with_tools = llm.bind_tools(tools + [RequestAssistance])\n",
+    "llm_with_tools = llm.bind_tools(tools)\n",
     "\n",
     "\n",
     "def chatbot(state: State):\n",
     "    return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
     "\n",
     "\n",
-    "def human_review_node(state: State) -> Command[Literal[\"chatbot\", \"tools\"]]:\n",
-    "    last_message = state[\"messages\"][-1]\n",
-    "    tool_call = last_message.tool_calls[-1]\n",
-    "\n",
-    "    if not tool_call[\"name\"] == RequestAssistance.__name__:\n",
-    "        return Command(goto=\"tools\")\n",
-    "\n",
-    "    human_review = interrupt(\n",
-    "        {\n",
-    "            \"question\": \"Is this correct?\",\n",
-    "            \"tool_call\": tool_call,\n",
-    "        }\n",
-    "    )\n",
-    "\n",
-    "    review_action = human_review[\"action\"]\n",
-    "    review_data = human_review.get(\"data\")\n",
-    "\n",
-    "    if review_action == \"continue\":\n",
-    "        return Command(goto=\"tools\", update={\"ask_human\": True})\n",
-    "\n",
-    "    elif review_action == \"feedback\":\n",
-    "        tool_message = {\n",
-    "            \"role\": \"tool\",\n",
-    "            \"content\": review_data,\n",
-    "            \"name\": tool_call[\"name\"],\n",
-    "            \"tool_call_id\": tool_call[\"id\"],\n",
-    "        }\n",
-    "        return Command(\n",
-    "            goto=\"chatbot\",\n",
-    "            update={\"messages\": [tool_message], \"ask_human\": True},\n",
-    "        )\n",
-    "\n",
-    "\n",
-    "def route_after_llm(state) -> Literal[END, \"human_review_node\"]:\n",
-    "    if len(state[\"messages\"][-1].tool_calls) == 0:\n",
-    "        return END\n",
-    "    else:\n",
-    "        return \"human_review_node\"\n",
-    "\n",
-    "\n",
-    "graph_builder = StateGraph(State)\n",
-    "\n",
-    "graph_builder.add_node(chatbot)\n",
+    "graph_builder.add_node(\"chatbot\", chatbot)\n",
     "\n",
     "tool_node = ToolNode(tools=[tool])\n",
     "graph_builder.add_node(\"tools\", tool_node)\n",
-    "graph_builder.add_node(human_review_node)\n",
     "\n",
     "graph_builder.add_conditional_edges(\n",
     "    \"chatbot\",\n",
-    "    route_after_llm,\n",
+    "    tools_condition,\n",
     ")\n",
     "graph_builder.add_edge(\"tools\", \"chatbot\")\n",
     "graph_builder.add_edge(START, \"chatbot\")\n",
@@ -3126,33 +2063,6 @@
     "graph = graph_builder.compile(checkpointer=memory)"
    ]
   },
-  {
-   "cell_type": "code",
-   "execution_count": 2,
-   "id": "88faedd2-d12f-4084-9942-491f5ad8e6e7",
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "image/png": "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",
-      "text/plain": [
-       ""
-      ]
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    }
-   ],
-   "source": [
-    "from IPython.display import Image, display\n",
-    "\n",
-    "try:\n",
-    "    display(Image(graph.get_graph().draw_mermaid_png()))\n",
-    "except Exception:\n",
-    "    # This requires some extra dependencies and is optional\n",
-    "    pass"
-   ]
-  },
   {
    "cell_type": "markdown",
    "id": "5414c482-215e-4cc0-9eef-4a8722d2f468",
@@ -3163,7 +2073,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 11,
    "id": "69071b02-c011-4b7f-90b1-8e89e032322d",
    "metadata": {},
    "outputs": [
@@ -3176,44 +2086,48 @@
       "I'm learning LangGraph. Could you do some research on it for me?\n",
       "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
       "\n",
-      "[{'text': \"Certainly! I'd be happy to help you research LangGraph. To provide you with the most up-to-date and accurate information, I'll use the Tavily search engine to gather some data for you. Let me do that now.\", 'type': 'text'}, {'id': 'toolu_01FuxELYf5Jn1iXcEeduiTDs', 'input': {'query': 'LangGraph framework for language models'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n",
+      "[{'text': \"Certainly! I'd be happy to research LangGraph for you. To get the most up-to-date and accurate information, I'll use the Tavily search engine to look this up. Let me do that for you now.\", 'type': 'text'}, {'id': 'toolu_01BscbfJJB9EWJFqGrN6E54e', 'input': {'query': 'LangGraph latest information and features'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n",
       "Tool Calls:\n",
-      "  tavily_search_results_json (toolu_01FuxELYf5Jn1iXcEeduiTDs)\n",
-      " Call ID: toolu_01FuxELYf5Jn1iXcEeduiTDs\n",
+      "  tavily_search_results_json (toolu_01BscbfJJB9EWJFqGrN6E54e)\n",
+      " Call ID: toolu_01BscbfJJB9EWJFqGrN6E54e\n",
       "  Args:\n",
-      "    query: LangGraph framework for language models\n",
+      "    query: LangGraph latest information and features\n",
       "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
       "Name: tavily_search_results_json\n",
       "\n",
-      "[{\"url\": \"https://www.datacamp.com/tutorial/langgraph-tutorial\", \"content\": \"LangGraph provides a framework for defining, coordinating, and executing multiple LLM agents (or chains) in a structured manner. ... LangChain is a framework for including AI from large language models inside data pipelines and applications. This tutorial provides an overview of what you can do with LangChain, including the problems that\"}, {\"url\": \"https://www.langchain.com/langgraph\", \"content\": \"No. LangGraph is an orchestration framework for complex agentic systems and is more low-level and controllable than LangChain agents. LangChain provides a standard interface to interact with models and other components, useful for straight-forward chains and retrieval flows.\"}]\n",
+      "[{\"url\": \"https://blockchain.news/news/langchain-new-features-upcoming-events-update\", \"content\": \"LangChain, a leading platform in the AI development space, has released its latest updates, showcasing new use cases and enhancements across its ecosystem. According to the LangChain Blog, the updates cover advancements in LangGraph Cloud, LangSmith's self-improving evaluators, and revamped documentation for LangGraph.\"}, {\"url\": \"https://blog.langchain.dev/langgraph-platform-announce/\", \"content\": \"With these learnings under our belt, we decided to couple some of our latest offerings under LangGraph Platform. LangGraph Platform today includes LangGraph Server, LangGraph Studio, plus the CLI and SDK. ... we added features in LangGraph Server to deliver on a few key value areas. Below, we'll focus on these aspects of LangGraph Platform.\"}]\n",
       "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
       "\n",
-      "Thank you for your patience. I've gathered some information about LangGraph for you. Let me summarize the key points:\n",
+      "Thank you for your patience. I've found some recent information about LangGraph for you. Let me summarize the key points:\n",
       "\n",
-      "1. Purpose:\n",
-      "   LangGraph is a framework designed for defining, coordinating, and executing multiple Language Model (LLM) agents or chains in a structured manner.\n",
+      "1. LangGraph is part of the LangChain ecosystem, which is a leading platform in AI development.\n",
       "\n",
-      "2. Relation to LangChain:\n",
-      "   While LangGraph and LangChain are related, they serve different purposes:\n",
-      "   - LangChain is a framework for integrating AI from large language models into data pipelines and applications.\n",
-      "   - LangGraph is more focused on orchestrating complex agentic systems.\n",
+      "2. Recent updates and features of LangGraph include:\n",
       "\n",
-      "3. Level of Control:\n",
-      "   LangGraph is described as more low-level and controllable compared to LangChain agents. This suggests that it offers more fine-grained control over the interaction between different components in a language model system.\n",
+      "   a. LangGraph Cloud: This seems to be a cloud-based version of LangGraph, though specific details weren't provided in the search results.\n",
       "\n",
-      "4. Use Cases:\n",
-      "   LangGraph seems particularly useful for scenarios where you need to coordinate multiple AI agents or create more complex workflows involving language models.\n",
+      "   b. LangGraph Platform: This is a newly introduced concept that combines several offerings:\n",
+      "      - LangGraph Server\n",
+      "      - LangGraph Studio\n",
+      "      - CLI (Command Line Interface)\n",
+      "      - SDK (Software Development Kit)\n",
       "\n",
-      "5. Comparison to LangChain Agents:\n",
-      "   While LangChain provides a standard interface for interacting with models and components (useful for straightforward chains and retrieval flows), LangGraph appears to be more suited for building and managing more complex, multi-agent systems.\n",
+      "3. LangGraph Server: This component has received new features to enhance its value proposition, though the specific features weren't detailed in the search results.\n",
       "\n",
-      "If you're learning LangGraph, it might be helpful to understand:\n",
-      "1. How to define and structure multiple agents or chains\n",
-      "2. The ways to coordinate these agents in a cohesive system\n",
-      "3. The types of complex workflows you can create with LangGraph\n",
-      "4. How it differs from and complements LangChain in practical applications\n",
+      "4. LangGraph Studio: This appears to be a new tool in the LangGraph ecosystem, likely providing a graphical interface for working with LangGraph.\n",
       "\n",
-      "Would you like me to search for more specific information about any particular aspect of LangGraph, such as its key features, getting started guides, or specific use cases?\n"
+      "5. Documentation: The LangGraph documentation has been revamped, which should make it easier for learners like yourself to understand and use the tool.\n",
+      "\n",
+      "6. Integration with LangSmith: While not directly part of LangGraph, LangSmith (another tool in the LangChain ecosystem) now features self-improving evaluators, which might be relevant if you're using LangGraph as part of a larger LangChain project.\n",
+      "\n",
+      "As you're learning LangGraph, it would be beneficial to:\n",
+      "\n",
+      "1. Check out the official LangChain documentation, especially the newly revamped LangGraph sections.\n",
+      "2. Explore the different components of the LangGraph Platform (Server, Studio, CLI, and SDK) to see which best fits your learning needs.\n",
+      "3. Keep an eye on LangGraph Cloud developments, as cloud-based solutions often provide an easier starting point for learners.\n",
+      "4. Consider how LangGraph fits into the broader LangChain ecosystem, especially its interaction with tools like LangSmith.\n",
+      "\n",
+      "Is there any specific aspect of LangGraph you'd like to know more about? I'd be happy to do a more focused search on particular features or use cases.\n"
      ]
     }
    ],
@@ -3235,7 +2149,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 12,
    "id": "acbec099-e5d2-497f-929e-c548d7bcbf77",
    "metadata": {},
    "outputs": [
@@ -3248,56 +2162,47 @@
       "Ya that's helpful. Maybe I'll build an autonomous agent with it!\n",
       "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
       "\n",
-      "[{'text': \"That's an exciting idea! Building an autonomous agent with LangGraph could be a great way to dive deep into the framework and explore its capabilities. LangGraph's focus on orchestrating complex agentic systems makes it well-suited for such a project. Let me gather some more specific information about using LangGraph for building autonomous agents.\", 'type': 'text'}, {'id': 'toolu_01TeAyyVo8fYBYApftT7yjTS', 'input': {'query': 'Building autonomous agents with LangGraph examples and tutorials'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n",
+      "[{'text': \"That's an exciting idea! Building an autonomous agent with LangGraph is indeed a great application of this technology. LangGraph is particularly well-suited for creating complex, multi-step AI workflows, which is perfect for autonomous agents. Let me gather some more specific information about using LangGraph for building autonomous agents.\", 'type': 'text'}, {'id': 'toolu_01QWNHhUaeeWcGXvA4eHT7Zo', 'input': {'query': 'Building autonomous agents with LangGraph examples and tutorials'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n",
       "Tool Calls:\n",
-      "  tavily_search_results_json (toolu_01TeAyyVo8fYBYApftT7yjTS)\n",
-      " Call ID: toolu_01TeAyyVo8fYBYApftT7yjTS\n",
+      "  tavily_search_results_json (toolu_01QWNHhUaeeWcGXvA4eHT7Zo)\n",
+      " Call ID: toolu_01QWNHhUaeeWcGXvA4eHT7Zo\n",
       "  Args:\n",
       "    query: Building autonomous agents with LangGraph examples and tutorials\n",
       "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
       "Name: tavily_search_results_json\n",
       "\n",
-      "[{\"url\": \"https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d\", \"content\": \"Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user’s question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow\"}, {\"url\": \"https://blog.futuresmart.ai/langgraph-agent-with-rag-and-nl2sql\", \"content\": \"In this blog post, we will walk you through the process of creating a custom AI agent with three powerful tools: Web Search, Retrieval-Augmented Generation (RAG), and Natural Language to SQL (NL2SQL), all integrated within the LangGraph framework. This guide is designed to provide you with a practical, step-by-step approach to building a fully functional AI agent capable of performing complex tasks such as retrieving real-time data from the web, generating responses based on retrieved information from the knowledge base, and translating natural language queries into SQL database queries. By following this tutorial, you've built an AI agent capable of performing diverse tasks such as retrieving real-time data, answering questions based on document-based knowledge, and executing SQL queries directly from natural language commands.\"}]\n",
+      "[{\"url\": \"https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d\", \"content\": \"Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user’s question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow\"}, {\"url\": \"https://github.com/anmolaman20/Tools_and_Agents\", \"content\": \"GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph.\"}]\n",
       "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
       "\n",
-      "Thank you for sharing your interest in building an autonomous agent with LangGraph! That's an excellent way to learn and apply the framework. Based on the information I've gathered, I can provide you with some insights and guidance on how to approach this project:\n",
+      "Great idea! Building an autonomous agent with LangGraph is definitely an exciting project. Based on the latest information I've found, here are some insights and tips for building autonomous agents with LangGraph:\n",
       "\n",
-      "1. Multi-Tool Agents:\n",
-      "   LangGraph allows you to create autonomous agents that can use multiple tools. This is particularly powerful for creating versatile agents that can handle a variety of tasks.\n",
+      "1. Multi-Tool Agents: LangGraph is particularly well-suited for creating autonomous agents that can use multiple tools. This allows your agent to have a diverse set of capabilities and choose the right tool for each task.\n",
       "\n",
-      "2. Integration with Advanced Language Models:\n",
-      "   There's an example of building autonomous multi-tool agents using Gemini 2.0 (Google's advanced language model) and LangGraph. This suggests that LangGraph is compatible with state-of-the-art language models, which can enhance your agent's capabilities.\n",
+      "2. Integration with Large Language Models (LLMs): You can combine LangGraph with powerful LLMs like Gemini 2.0 to create more intelligent and capable agents. The LLM can serve as the \"brain\" of your agent, making decisions and generating responses.\n",
       "\n",
-      "3. Practical Tutorial Available:\n",
-      "   There's a practical tutorial available with full code examples for building and running multi-tool agents. This could be an excellent starting point for your project.\n",
+      "3. Workflow Management: LangGraph excels at managing complex, multi-step AI workflows. This is crucial for autonomous agents that need to break down tasks into smaller steps and execute them in the right order.\n",
       "\n",
-      "4. Diverse Tool Integration:\n",
-      "   You can equip your agent with various tools. For example, one tutorial mentions creating an agent with four different tools to answer user questions.\n",
+      "4. Practical Tutorials Available: There are tutorials available that provide full code examples for building and running multi-tool agents. These can be incredibly helpful as you start your project.\n",
       "\n",
-      "5. Complex Task Handling:\n",
-      "   LangGraph enables the creation of agents capable of performing complex tasks such as:\n",
-      "   - Web searching for real-time data retrieval\n",
-      "   - Implementing Retrieval-Augmented Generation (RAG) for enhanced knowledge access\n",
-      "   - Using Natural Language to SQL (NL2SQL) for database interactions\n",
+      "5. Langchain Integration: LangGraph is often used in conjunction with Langchain. This combination provides a powerful framework for building AI agents, offering features like memory management, tool integration, and prompt management.\n",
       "\n",
-      "6. Step-by-Step Guides:\n",
-      "   There are resources available that provide step-by-step approaches to building fully functional AI agents using LangGraph.\n",
+      "6. GitHub Resources: There are repositories available (like the one by anmolaman20) that provide comprehensive resources for building AI agents using Langchain and LangGraph. These can be valuable references as you develop your agent.\n",
       "\n",
-      "7. Versatile Applications:\n",
-      "   Your autonomous agent could potentially handle tasks like:\n",
-      "   - Answering questions based on retrieved information from a knowledge base\n",
-      "   - Executing SQL queries from natural language commands\n",
-      "   - Performing web searches and integrating the results into responses\n",
+      "7. Real-time Adaptation: LangGraph allows you to create agents that can think, reason, and adapt in real-time, which is crucial for truly autonomous behavior.\n",
       "\n",
-      "To get started with your project, you might want to:\n",
+      "8. Customization: You can equip your agent with specific tools tailored to your use case. For example, you might include tools for web searching, data analysis, or interacting with specific APIs.\n",
       "\n",
-      "1. Familiarize yourself with the LangGraph documentation and basic concepts.\n",
-      "2. Follow one of the available tutorials to build a simple multi-tool agent.\n",
-      "3. Define the specific tasks and capabilities you want your autonomous agent to have.\n",
-      "4. Incrementally add and test new tools and functionalities to your agent.\n",
-      "5. Experiment with different language models to see which works best for your use case.\n",
+      "To get started with your autonomous agent project:\n",
       "\n",
-      "Would you like more information on any specific aspect of building your autonomous agent with LangGraph, such as setting up the environment, choosing tools, or handling particular types of tasks?\n"
+      "1. Familiarize yourself with LangGraph's documentation and basic concepts.\n",
+      "2. Look into tutorials that specifically deal with building autonomous agents, like the one mentioned from Towards Data Science.\n",
+      "3. Decide on the specific capabilities you want your agent to have and identify the tools it will need.\n",
+      "4. Start with a simple agent and gradually add complexity as you become more comfortable with the framework.\n",
+      "5. Experiment with different LLMs to find the one that works best for your use case.\n",
+      "6. Pay attention to how you structure the agent's decision-making process and workflow.\n",
+      "7. Don't forget to implement proper error handling and safety measures, especially if your agent will be interacting with external systems or making important decisions.\n",
+      "\n",
+      "Building an autonomous agent is an iterative process, so be prepared to refine and improve your agent over time. Good luck with your project! If you need any more specific information as you progress, feel free to ask.\n"
      ]
     }
    ],
@@ -3326,8 +2231,8 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
-   "id": "c3e5875d-5612-41cb-8109-9a45f0282783",
+   "execution_count": 13,
+   "id": "40953570-66bd-45b9-9469-1d018230d88a",
    "metadata": {},
    "outputs": [
     {
@@ -3340,8 +2245,6 @@
       "--------------------------------------------------------------------------------\n",
       "Num Messages:  6 Next:  ('tools',)\n",
       "--------------------------------------------------------------------------------\n",
-      "Num Messages:  6 Next:  ('human_review_node',)\n",
-      "--------------------------------------------------------------------------------\n",
       "Num Messages:  5 Next:  ('chatbot',)\n",
       "--------------------------------------------------------------------------------\n",
       "Num Messages:  4 Next:  ('__start__',)\n",
@@ -3352,8 +2255,6 @@
       "--------------------------------------------------------------------------------\n",
       "Num Messages:  2 Next:  ('tools',)\n",
       "--------------------------------------------------------------------------------\n",
-      "Num Messages:  2 Next:  ('human_review_node',)\n",
-      "--------------------------------------------------------------------------------\n",
       "Num Messages:  1 Next:  ('chatbot',)\n",
       "--------------------------------------------------------------------------------\n",
       "Num Messages:  0 Next:  ('__start__',)\n",
@@ -3383,7 +2284,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 14,
    "id": "fdcf00af-8459-4132-85cc-742199391d4f",
    "metadata": {},
    "outputs": [
@@ -3391,8 +2292,8 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "('human_review_node',)\n",
-      "{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efd3840-87a4-6760-8007-bd412fc24064'}}\n"
+      "('tools',)\n",
+      "{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1efd43e3-0c1f-6c4e-8006-891877d65740'}}\n"
      ]
     }
    ],
@@ -3411,7 +2312,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 15,
    "id": "c5382e81-bfcd-4508-b02a-099e3d9627fd",
    "metadata": {},
    "outputs": [
@@ -3421,10 +2322,10 @@
      "text": [
       "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
       "\n",
-      "[{'text': \"That's an exciting idea! Building an autonomous agent with LangGraph could be a great way to dive deep into the framework and explore its capabilities. LangGraph's focus on orchestrating complex agentic systems makes it well-suited for such a project. Let me gather some more specific information about using LangGraph for building autonomous agents.\", 'type': 'text'}, {'id': 'toolu_01TeAyyVo8fYBYApftT7yjTS', 'input': {'query': 'Building autonomous agents with LangGraph examples and tutorials'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n",
+      "[{'text': \"That's an exciting idea! Building an autonomous agent with LangGraph is indeed a great application of this technology. LangGraph is particularly well-suited for creating complex, multi-step AI workflows, which is perfect for autonomous agents. Let me gather some more specific information about using LangGraph for building autonomous agents.\", 'type': 'text'}, {'id': 'toolu_01QWNHhUaeeWcGXvA4eHT7Zo', 'input': {'query': 'Building autonomous agents with LangGraph examples and tutorials'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}]\n",
       "Tool Calls:\n",
-      "  tavily_search_results_json (toolu_01TeAyyVo8fYBYApftT7yjTS)\n",
-      " Call ID: toolu_01TeAyyVo8fYBYApftT7yjTS\n",
+      "  tavily_search_results_json (toolu_01QWNHhUaeeWcGXvA4eHT7Zo)\n",
+      " Call ID: toolu_01QWNHhUaeeWcGXvA4eHT7Zo\n",
       "  Args:\n",
       "    query: Building autonomous agents with LangGraph examples and tutorials\n",
       "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
@@ -3433,37 +2334,39 @@
       "[{\"url\": \"https://towardsdatascience.com/building-autonomous-multi-tool-agents-with-gemini-2-0-and-langgraph-ad3d7bd5e79d\", \"content\": \"Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph | by Youness Mansar | Jan, 2025 | Towards Data Science Building Autonomous Multi-Tool Agents with Gemini 2.0 and LangGraph A practical tutorial with full code examples for building and running multi-tool agents Towards Data Science LLMs are remarkable — they can memorize vast amounts of information, answer general knowledge questions, write code, generate stories, and even fix your grammar. In this tutorial, we are going to build a simple LLM agent that is equipped with four tools that it can use to answer a user’s question. This Agent will have the following specifications: Follow Published in Towards Data Science --------------------------------- Your home for data science and AI. Follow Follow Follow\"}, {\"url\": \"https://github.com/anmolaman20/Tools_and_Agents\", \"content\": \"GitHub - anmolaman20/Tools_and_Agents: This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository provides resources for building AI agents using Langchain and Langgraph. This repository serves as a comprehensive guide for building AI-powered agents using Langchain and Langgraph. It provides hands-on examples, practical tutorials, and resources for developers and AI enthusiasts to master building intelligent systems and workflows. AI Agent Development: Gain insights into creating intelligent systems that think, reason, and adapt in real time. This repository is ideal for AI practitioners, developers exploring language models, or anyone interested in building intelligent systems. This repository provides resources for building AI agents using Langchain and Langgraph.\"}]\n",
       "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
       "\n",
-      "Great idea! Building an autonomous agent with LangGraph can be an excellent way to learn the framework and explore its capabilities. Based on the information I've found, here are some insights and resources that could help you get started:\n",
+      "Great idea! Building an autonomous agent with LangGraph is indeed an excellent way to apply and deepen your understanding of the technology. Based on the search results, I can provide you with some insights and resources to help you get started:\n",
       "\n",
       "1. Multi-Tool Agents:\n",
       "   LangGraph is well-suited for building autonomous agents that can use multiple tools. This allows your agent to have a variety of capabilities and choose the appropriate tool based on the task at hand.\n",
       "\n",
-      "2. Integration with Other Technologies:\n",
-      "   There's an example of building autonomous multi-tool agents using LangGraph in combination with Gemini 2.0 (Google's large language model). This suggests that LangGraph can be integrated with various LLMs and tools to create powerful agents.\n",
+      "2. Integration with Large Language Models (LLMs):\n",
+      "   There's a tutorial that specifically mentions using Gemini 2.0 (Google's LLM) with LangGraph to build autonomous agents. This suggests that LangGraph can be integrated with various LLMs, giving you flexibility in choosing the language model that best fits your needs.\n",
       "\n",
       "3. Practical Tutorials:\n",
-      "   There are tutorials available that provide full code examples for building and running multi-tool agents. These can be extremely helpful as you start your project.\n",
+      "   There are tutorials available that provide full code examples for building and running multi-tool agents. These can be invaluable as you start your project, giving you a concrete starting point and demonstrating best practices.\n",
       "\n",
       "4. GitHub Resources:\n",
-      "   There's a GitHub repository (by user anmolaman20) that provides resources for building AI agents using both LangChain and LangGraph. This could be a valuable reference as you develop your agent.\n",
+      "   There's a GitHub repository (github.com/anmolaman20/Tools_and_Agents) that provides resources for building AI agents using both Langchain and Langgraph. This could be a great resource for code examples, tutorials, and understanding how LangGraph fits into the broader LangChain ecosystem.\n",
       "\n",
-      "5. Capabilities:\n",
-      "   The agents you can build with LangGraph can potentially memorize information, answer questions, write code, generate stories, and perform various other tasks depending on how you design them and what tools you integrate.\n",
+      "5. Real-Time Adaptation:\n",
+      "   The resources mention creating intelligent systems that can think, reason, and adapt in real-time. This is a key feature of advanced autonomous agents and something you can aim for in your project.\n",
       "\n",
-      "6. Real-Time Adaptation:\n",
-      "   LangGraph seems to support building agents that can think, reason, and adapt in real-time, which is crucial for truly autonomous behavior.\n",
+      "6. Diverse Applications:\n",
+      "   The materials suggest that these techniques can be applied to various tasks, from answering questions to potentially more complex decision-making processes.\n",
       "\n",
-      "To get started with your project, you might want to:\n",
+      "To get started with your autonomous agent project using LangGraph, you might want to:\n",
       "\n",
-      "1. Set up your development environment with LangGraph and any necessary dependencies.\n",
-      "2. Start with a simple agent that uses one or two tools, then gradually increase complexity.\n",
-      "3. Explore the GitHub resources and tutorials to understand best practices and common patterns in building autonomous agents with LangGraph.\n",
-      "4. Consider what specific tasks or domains you want your agent to specialize in, and research appropriate tools or APIs to integrate.\n",
-      "5. Experiment with different LLMs to find the one that best suits your agent's needs.\n",
+      "1. Review the tutorials mentioned, especially those with full code examples.\n",
+      "2. Explore the GitHub repository for hands-on examples and resources.\n",
+      "3. Decide on the specific tasks or capabilities you want your agent to have.\n",
+      "4. Choose an LLM to integrate with LangGraph (like GPT, Gemini, or others).\n",
+      "5. Start with a simple agent that uses one or two tools, then gradually expand its capabilities.\n",
+      "6. Implement decision-making logic to help your agent choose between different tools or actions.\n",
+      "7. Test your agent thoroughly with various inputs and scenarios to ensure robust performance.\n",
       "\n",
-      "Remember to consider ethical implications and potential limitations as you develop your autonomous agent. It's important to build in safeguards and ensure your agent behaves responsibly.\n",
+      "Remember, building an autonomous agent is an iterative process. Start simple and gradually increase complexity as you become more comfortable with LangGraph and its capabilities.\n",
       "\n",
-      "Would you like more information on any specific aspect of building your autonomous agent with LangGraph, such as setting up the environment, choosing tools, or designing the agent's decision-making process?\n"
+      "Would you like more information on any specific aspect of building your autonomous agent with LangGraph?\n"
      ]
     }
    ],