diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 90790392d..9cb247f96 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -16,9 +16,7 @@ _MANUAL = { "async.ipynb", "streaming-tokens.ipynb", "streaming-content.ipynb", - "human-in-the-loop.ipynb", "persistence.ipynb", - "time-travel.ipynb", "visualization.ipynb", "state-model.ipynb", "subgraph.ipynb", @@ -35,6 +33,10 @@ _MANUAL = { "create-react-agent-system-prompt.ipynb", "create-react-agent-memory.ipynb", "create-react-agent-hitl.ipynb", + "human_in_the_loop/breakpoints.ipynb", + "human_in_the_loop/time-travel.ipynb", + "human_in_the_loop/edit-graph-state.ipynb", + "human_in_the_loop/wait-user-input.ipynb", ], "tutorials": [ "introduction.ipynb", diff --git a/docs/docs/concepts/low_level.md b/docs/docs/concepts/low_level.md index e8aa73f6c..145d5d369 100644 --- a/docs/docs/concepts/low_level.md +++ b/docs/docs/concepts/low_level.md @@ -269,6 +269,11 @@ When you use a checkpointer with a graph, you can interact with the state of tha This usually done when enabling different human-in-the-loop interaction patterns. When interacting with the checkpointer state, you must specify [thread identifiers](#threads) +Each checkpoint has two properties: + +-**values**: This is the value of the state at this point in time. +-**next**: This is a tuple of the nodes to execute next in the graph. + ### Get state You can get the state of a checkpointer by calling `graph.get_state(config)`. The config should contain `thread_id`, and the state will be fetched for that thread. diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 126542350..59d50a0cc 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -22,8 +22,10 @@ One of LangGraph's main benefits is that it makes human-in-the-loop workflows ea These guides cover common examples of that. - [How to add persistence ("memory") to your graph](persistence.ipynb) -- [How to view and update graph state](time-travel.ipynb) -- [How to add human-in-the-loop](human-in-the-loop.ipynb) +- [How to add breakpoints](human_in_the_loop/breakpoints.ipynb) +- [How to edit graph state](human_in_the_loop/edit-graph-state.ipynb) +- [How to wait for user input](human_in_the_loop/wait-user-input.ipynb) +- [How to view and update past graph state](human_in_the_loop/time-travel.ipynb) ## Streaming diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 9cda1e842..577c2aa82 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -140,8 +140,10 @@ nav: - Create map-reduce branches for parallel execution: how-tos/map-reduce.ipynb - Human-in-the-loop: - Add persistence ("memory"): how-tos/persistence.ipynb - - View and update graph state: how-tos/time-travel.ipynb - - Add human-in-the-loop: how-tos/human-in-the-loop.ipynb + - Add breakpoints: how-tos/human_in_the_loop/breakpoints.ipynb + - Wait for user input: how-tos/human_in_the_loop/wait-user-input.ipynb + - View and update past graph state: how-tos/human_in_the_loop/time-travel.ipynb + - Edit graph state: how-tos/human_in_the_loop/edit-graph-state.ipynb - Streaming: - Stream LLM tokens: how-tos/streaming-tokens.ipynb - Stream Arbitrarily Nested Content: how-tos/streaming-content.ipynb diff --git a/examples/human-in-the-loop.ipynb b/examples/human-in-the-loop.ipynb index 1e4444402..b83629d2a 100644 --- a/examples/human-in-the-loop.ipynb +++ b/examples/human-in-the-loop.ipynb @@ -972,7 +972,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.11.1" } }, "nbformat": 4, diff --git a/examples/human_in_the_loop/breakpoints.ipynb b/examples/human_in_the_loop/breakpoints.ipynb new file mode 100644 index 000000000..750c3126d --- /dev/null +++ b/examples/human_in_the_loop/breakpoints.ipynb @@ -0,0 +1,339 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# How to add breakpoints\n", + "\n", + "When creating LangGraph agents, it is often nice to add a human-in-the-loop component.\n", + "This can be helpful when giving them access to tools.\n", + "Often in these situations you may want to manually approve an action before taking.\n", + "\n", + "This can be in several ways, but the primary supported way is to add an \"interrupt\" before a node is executed.\n", + "This interrupts execution at that node.\n", + "You can then resume from that spot to continue. " + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain_anthropic" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for Anthropic (the LLM we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"ANTHROPIC_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "3333b771", + "metadata": {}, + "source": [ + "## Build the agent\n", + "\n", + "We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and a fake tool (just for demo purposes)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6098e5cb", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up the state\n", + "from langgraph.graph import MessagesState\n", + "\n", + "# Set up the tool\n", + "from langchain_core.tools import tool\n", + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "@tool\n", + "def search(query: str):\n", + " \"\"\"Call to surf the web.\"\"\"\n", + " # This is a placeholder for the actual implementation\n", + " # Don't let the LLM know this though 😊\n", + " return [\n", + " \"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", + " ]\n", + "\n", + "\n", + "tools = [search]\n", + "tool_executor = ToolExecutor(tools)\n", + "\n", + "# Set up the model\n", + "from langchain_anthropic import ChatAnthropic\n", + "\n", + "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", + "model = model.bind_tools(tools)\n", + "\n", + "# Define nodes and conditional edges\n", + "\n", + "from langchain_core.messages import ToolMessage\n", + "\n", + "from langgraph.prebuilt import ToolInvocation\n", + "\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if not last_message.tool_calls:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state[\"messages\"]\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state[\"messages\"]\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " tool_call = last_message.tool_calls[0]\n", + " action = ToolInvocation(\n", + " tool=tool_call[\"name\"],\n", + " tool_input=tool_call[\"args\"],\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a ToolMessage\n", + " tool_message = ToolMessage(\n", + " content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n", + " )\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [tool_message]}\n", + "\n", + "# Build the graph\n", + "\n", + "from langgraph.graph import END, StateGraph\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(MessagesState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END,\n", + " },\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")\n", + "\n", + "# Set up memory\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "memory = MemorySaver()\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "\n", + "# We add in `interrupt_before=[\"action\"]`\n", + "# This will add a breakpoint before the `action` node is called\n", + "app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])" + ] + }, + { + "cell_type": "markdown", + "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", + "metadata": {}, + "source": [ + "## Interacting with the Agent\n", + "\n", + "We can now interact with the agent and see that it stops before calling a tool.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "cfd140f0-a5a6-4697-8115-322242f197b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "search for the weather in sf now\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'text': \"Certainly! I'll search for the current weather in San Francisco for you. Let me use the search function to find this information.\", 'type': 'text'}, {'id': 'toolu_017HamT7ubS5RXGCL7CS3t7F', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}]\n", + "Tool Calls:\n", + " search (toolu_017HamT7ubS5RXGCL7CS3t7F)\n", + " Call ID: toolu_017HamT7ubS5RXGCL7CS3t7F\n", + " Args:\n", + " query: current weather in San Francisco\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "thread = {\"configurable\": {\"thread_id\": \"3\"}}\n", + "inputs = [HumanMessage(content=\"search for the weather in sf now\")]\n", + "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "1bca3814-db08-4b0b-8c0c-95b6c5440c81", + "metadata": {}, + "source": [ + "**Resume**\n", + "\n", + "We can now call the agent again with no inputs to continue, ie. run the tool as requested.\n", + "\n", + "Running an interrupted graph with `None` in the inputs means to \"proceed as if the interruption didn't occur.\"" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "51923913-20f7-4ee1-b9ba-d01f5fb2869b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: search\n", + "\n", + "[\"It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"]\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Based on the search results, I can provide you with information about the current weather in San Francisco:\n", + "\n", + "The weather in San Francisco right now is sunny. It's a beautiful day in the city!\n", + "\n", + "It's worth noting that the search result included an unusual comment about Gemini, which isn't directly related to the weather. This appears to be an unrelated piece of information or possibly part of a horoscope that was included in the search results.\n", + "\n", + "If you'd like more specific details about the temperature, wind conditions, or forecast for the coming days, please let me know, and I'd be happy to search for that additional information for you.\n" + ] + } + ], + "source": [ + "for event in app.stream(None, thread, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/human_in_the_loop/edit-graph-state.ipynb b/examples/human_in_the_loop/edit-graph-state.ipynb new file mode 100644 index 000000000..b0b1aeae4 --- /dev/null +++ b/examples/human_in_the_loop/edit-graph-state.ipynb @@ -0,0 +1,425 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# How to edit graph state\n", + "\n", + "When creating LangGraph agents, it is often nice to add a human-in-the-loop component.\n", + "This can be helpful when giving them access to tools.\n", + "Often in these situations you may want to edit the graph state before continuing (for example, to edit what tool is being called, or how it is being called).\n", + "\n", + "This can be in several ways, but the primary supported way is to add an \"interrupt\" before a node is executed.\n", + "This interrupts execution at that node.\n", + "You can then use `update_state` to update the state, and then resume from that spot to continue. " + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain_anthropic" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for Anthropic (the LLM we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"ANTHROPIC_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "3333b771", + "metadata": {}, + "source": [ + "## Build the agent\n", + "\n", + "We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and a fake tool (just for demo purposes)." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "6098e5cb", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up the state\n", + "from langgraph.graph import MessagesState\n", + "\n", + "# Set up the tool\n", + "from langchain_core.tools import tool\n", + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "@tool\n", + "def search(query: str):\n", + " \"\"\"Call to surf the web.\"\"\"\n", + " # This is a placeholder for the actual implementation\n", + " # Don't let the LLM know this though 😊\n", + " return [\n", + " f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", + " ]\n", + "\n", + "\n", + "tools = [search]\n", + "tool_executor = ToolExecutor(tools)\n", + "\n", + "# Set up the model\n", + "from langchain_anthropic import ChatAnthropic\n", + "\n", + "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", + "model = model.bind_tools(tools)\n", + "\n", + "# Define nodes and conditional edges\n", + "\n", + "from langchain_core.messages import ToolMessage\n", + "\n", + "from langgraph.prebuilt import ToolInvocation\n", + "\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if not last_message.tool_calls:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state[\"messages\"]\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state[\"messages\"]\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " tool_call = last_message.tool_calls[0]\n", + " action = ToolInvocation(\n", + " tool=tool_call[\"name\"],\n", + " tool_input=tool_call[\"args\"],\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a ToolMessage\n", + " tool_message = ToolMessage(\n", + " content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n", + " )\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [tool_message]}\n", + "\n", + "# Build the graph\n", + "\n", + "from langgraph.graph import END, StateGraph\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(MessagesState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END,\n", + " },\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")\n", + "\n", + "# Set up memory\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "memory = MemorySaver()\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "\n", + "# We add in `interrupt_before=[\"action\"]`\n", + "# This will add a breakpoint before the `action` node is called\n", + "app = workflow.compile(checkpointer=memory, interrupt_before=[\"action\"])" + ] + }, + { + "cell_type": "markdown", + "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", + "metadata": {}, + "source": [ + "## Interacting with the Agent\n", + "\n", + "We can now interact with the agent and see that it stops before calling a tool.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "cfd140f0-a5a6-4697-8115-322242f197b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "search for the weather in sf now\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'text': \"Certainly! I'll search for the current weather in San Francisco for you. Let me use the search function to find this information.\", 'type': 'text'}, {'id': 'toolu_011s1G2cKjKkkkJTcWb3ze5m', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}]\n", + "Tool Calls:\n", + " search (toolu_011s1G2cKjKkkkJTcWb3ze5m)\n", + " Call ID: toolu_011s1G2cKjKkkkJTcWb3ze5m\n", + " Args:\n", + " query: current weather in San Francisco\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "thread = {\"configurable\": {\"thread_id\": \"3\"}}\n", + "inputs = [HumanMessage(content=\"search for the weather in sf now\")]\n", + "for event in app.stream({\"messages\": inputs}, thread, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "78e3f5b9-9700-42b1-863f-c404861f8620", + "metadata": {}, + "source": [ + "**Edit**\n", + "\n", + "We can now update the state accordingly. Let's modify the tool call to have the query `\"current weather in SF\"`." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "1aa7b1b9-9322-4815-bc0d-eb083870ac15", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'configurable': {'thread_id': '3',\n", + " 'thread_ts': '1ef3102f-e345-627e-8002-bfdc440474fe'}}" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# First, lets get the current state\n", + "current_state = app.get_state(thread)\n", + "\n", + "# Let's now get the last message in the state\n", + "# This is the one with the tool calls that we want to update\n", + "last_message = current_state.values['messages'][-1]\n", + "\n", + "# Let's now update the args for that tool call\n", + "last_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n", + "\n", + "# Let's now call `update_state` to pass in this message in the `messages` key\n", + "# This will get treated as any other update to the state\n", + "# It will get passed to the reducer function for the `messages` key\n", + "# That reducer function will use the ID of the message to update it\n", + "# It's important that it has the right ID! Otherwise it would get appended\n", + "# as a new message\n", + "app.update_state(thread, {\"messages\": last_message})" + ] + }, + { + "cell_type": "markdown", + "id": "0dcc5457-1ba1-4cba-ac41-da5c67cc67e5", + "metadata": {}, + "source": [ + "Let's now check the current state of the app to make sure it got updated accordingly" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "a3fcf2bd-f881-49fe-b20e-ad16e6819bc6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'name': 'search',\n", + " 'args': {'query': 'current weather in SF'},\n", + " 'id': 'toolu_011s1G2cKjKkkkJTcWb3ze5m'}]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "current_state = app.get_state(thread).values['messages'][-1].tool_calls\n", + "current_state" + ] + }, + { + "cell_type": "markdown", + "id": "1bca3814-db08-4b0b-8c0c-95b6c5440c81", + "metadata": {}, + "source": [ + "**Resume**\n", + "\n", + "We can now call the agent again with no inputs to continue, ie. run the tool as requested. We can see from the logs that it passes in the update args to the tool." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "51923913-20f7-4ee1-b9ba-d01f5fb2869b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: search\n", + "\n", + "[\"I looked up: current weather in SF. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"]\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Based on the search results, I can provide you with information about the current weather in San Francisco:\n", + "\n", + "The weather in San Francisco is currently sunny. This means it's a clear day with plenty of sunshine, which is quite typical for San Francisco, especially during certain times of the year.\n", + "\n", + "It's worth noting that San Francisco's weather can be quite variable, even within the city itself, due to its unique microclimate and geography. While it's sunny now, it's always a good idea to be prepared for potential changes in weather, as the city is known for its foggy conditions that can roll in quickly, especially near the coast.\n", + "\n", + "The search result also included an unusual astrological reference about Geminis, but that's not relevant to the weather information you requested. If you need any more specific details about the weather, such as temperature, wind speed, or forecast for the coming days, please let me know, and I'd be happy to search for that information as well.\n" + ] + } + ], + "source": [ + "for event in app.stream(None, thread, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78780afe-409d-46cd-a734-e82538cdd8de", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/human_in_the_loop/time-travel.ipynb b/examples/human_in_the_loop/time-travel.ipynb new file mode 100644 index 000000000..c0ad709bc --- /dev/null +++ b/examples/human_in_the_loop/time-travel.ipynb @@ -0,0 +1,619 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# How to view and update past graph state\n", + "\n", + "Once you start [checkpointing](./persistence.ipynb) your graphs, you can easily **get** or **update** the state of the agent at any point in time. This permits a few things:\n", + "\n", + "1. You can surface a state during an interrupt to a user to let them accept an action.\n", + "2. You can **rewind** the graph to reproduce or avoid issues.\n", + "3. You can **modify** the state to embed your agent into a larger system, or to let the user better control its actions.\n", + "\n", + "The key methods used for this functionality are:\n", + "\n", + "- [get_state](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.graph.CompiledGraph.get_state): fetch the values from the target config\n", + "- [update_state](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.graph.CompiledGraph.update_state): apply the given values to the target state\n", + "\n", + "**Note:** this requires passing in a checkpointer.\n", + "\n", + "Below is a quick example." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain_anthropic" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for Anthropic (the LLM we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"ANTHROPIC_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "e36f89e5", + "metadata": {}, + "source": [ + "## Build the agent\n", + "\n", + "We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and a fake tool (just for demo purposes)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f5319e01", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/harrisonchase/.pyenv/versions/3.11.1/envs/permchain/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: The method `ChatAnthropic.bind_tools` is in beta. It is actively being worked on, so the API may change.\n", + " warn_beta(\n" + ] + } + ], + "source": [ + "# Set up the state\n", + "from langgraph.graph import MessagesState\n", + "\n", + "# Set up the tool\n", + "from langchain_core.tools import tool\n", + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "@tool\n", + "def search(query: str):\n", + " \"\"\"Call to surf the web.\"\"\"\n", + " # This is a placeholder for the actual implementation\n", + " # Don't let the LLM know this though 😊\n", + " return [\n", + " f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", + " ]\n", + "\n", + "\n", + "tools = [search]\n", + "tool_executor = ToolExecutor(tools)\n", + "\n", + "# Set up the model\n", + "from langchain_anthropic import ChatAnthropic\n", + "\n", + "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", + "model = model.bind_tools(tools)\n", + "\n", + "# Define nodes and conditional edges\n", + "\n", + "from langchain_core.messages import ToolMessage\n", + "\n", + "from langgraph.prebuilt import ToolInvocation\n", + "\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if not last_message.tool_calls:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state[\"messages\"]\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state[\"messages\"]\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " tool_call = last_message.tool_calls[0]\n", + " action = ToolInvocation(\n", + " tool=tool_call[\"name\"],\n", + " tool_input=tool_call[\"args\"],\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a ToolMessage\n", + " tool_message = ToolMessage(\n", + " content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n", + " )\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [tool_message]}\n", + "\n", + "# Build the graph\n", + "\n", + "from langgraph.graph import END, StateGraph\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(MessagesState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END,\n", + " },\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")\n", + "\n", + "# Set up memory\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "memory = MemorySaver()\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile(checkpointer=memory)" + ] + }, + { + "cell_type": "markdown", + "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", + "metadata": {}, + "source": [ + "## Interacting with the Agent\n", + "\n", + "We can now interact with the agent. Let's ask it for the weather in SF.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cfd140f0-a5a6-4697-8115-322242f197b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "Use the search tool to look up the weather in SF\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}]\n", + "Tool Calls:\n", + " search (toolu_01M1JGmPF1wjJRxY2SYsCYS7)\n", + " Call ID: toolu_01M1JGmPF1wjJRxY2SYsCYS7\n", + " Args:\n", + " query: weather in San Francisco\n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: search\n", + "\n", + "[\"I looked up: weather in San Francisco. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"]\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Based on the search results, I can provide you with information about the weather in San Francisco:\n", + "\n", + "The current weather in San Francisco is sunny. This means it's a clear day with plenty of sunshine, which is quite common for San Francisco, especially during certain times of the year.\n", + "\n", + "However, there's an interesting additional note in the search result that seems to be unrelated to the weather itself. It mentions something about Geminis, which appears to be a reference to astrology. This part of the result doesn't provide any relevant weather information, so we'll focus on the actual weather report.\n", + "\n", + "To summarize:\n", + "- Current weather in San Francisco: Sunny\n", + "- Sky conditions: Clear\n", + "\n", + "Keep in mind that San Francisco's weather can change quickly due to its unique microclimate, influenced by the bay and ocean. Even on sunny days, it's always a good idea to be prepared for potential fog or cooler temperatures, especially near the coast or in the evenings.\n", + "\n", + "Is there any specific information about the San Francisco weather you'd like to know more about, such as temperature, wind conditions, or forecast for the coming days?\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "config = {\"configurable\": {\"thread_id\": \"1\"}}\n", + "input_message = HumanMessage(content=\"Use the search tool to look up the weather in SF\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "1c38c505-6cee-427f-9dcd-493a2ade7ebb", + "metadata": {}, + "source": [ + "## Checking history\n", + "\n", + "Let's browse the history of this thread, from start to finish." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "8578a66d-6489-4e03-8c23-fd0530278455", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "StateSnapshot(values={'messages': []}, next=('__start__',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef3103d-f96d-65f2-bfff-f32837888e44'}}, metadata={'source': 'input', 'step': -1, 'writes': {'messages': [HumanMessage(content='Use the search tool to look up the weather in SF')]}}, created_at='2024-06-23T01:56:57.764168+00:00', parent_config=None)\n", + "--\n", + "StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef3103d-f974-6b0e-8000-99038b849969'}}, metadata={'source': 'loop', 'step': 0, 'writes': None}, created_at='2024-06-23T01:56:57.767174+00:00', parent_config=None)\n", + "--\n", + "StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18'), AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01JGRvXQBPCGbXxHHHj5mSQs', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 82}}, id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7'}])]}, next=('action',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef3103e-072f-68f2-8001-29ebe76a1f5e'}}, metadata={'source': 'loop', 'step': 1, 'writes': {'agent': {'messages': [AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01JGRvXQBPCGbXxHHHj5mSQs', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 82}}, id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7'}])]}}}, created_at='2024-06-23T01:56:59.206868+00:00', parent_config=None)\n", + "--\n", + "StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18'), AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01JGRvXQBPCGbXxHHHj5mSQs', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 82}}, id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7'}]), ToolMessage(content='[\"I looked up: weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', name='search', id='0e86ac06-d5a2-47f6-b6ff-6f221eac9f68', tool_call_id='toolu_01M1JGmPF1wjJRxY2SYsCYS7')]}, next=('agent',), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef3103e-0739-6762-8002-1ff85304da93'}}, metadata={'source': 'loop', 'step': 2, 'writes': {'action': {'messages': [ToolMessage(content='[\"I looked up: weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', name='search', id='0e86ac06-d5a2-47f6-b6ff-6f221eac9f68', tool_call_id='toolu_01M1JGmPF1wjJRxY2SYsCYS7')]}}}, created_at='2024-06-23T01:56:59.210922+00:00', parent_config=None)\n", + "--\n", + "StateSnapshot(values={'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18'), AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01JGRvXQBPCGbXxHHHj5mSQs', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 82}}, id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7'}]), ToolMessage(content='[\"I looked up: weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', name='search', id='0e86ac06-d5a2-47f6-b6ff-6f221eac9f68', tool_call_id='toolu_01M1JGmPF1wjJRxY2SYsCYS7'), AIMessage(content=\"Based on the search results, I can provide you with information about the weather in San Francisco:\\n\\nThe current weather in San Francisco is sunny. This means it's a clear day with plenty of sunshine, which is quite common for San Francisco, especially during certain times of the year.\\n\\nHowever, there's an interesting additional note in the search result that seems to be unrelated to the weather itself. It mentions something about Geminis, which appears to be a reference to astrology. This part of the result doesn't provide any relevant weather information, so we'll focus on the actual weather report.\\n\\nTo summarize:\\n- Current weather in San Francisco: Sunny\\n- Sky conditions: Clear\\n\\nKeep in mind that San Francisco's weather can change quickly due to its unique microclimate, influenced by the bay and ocean. Even on sunny days, it's always a good idea to be prepared for potential fog or cooler temperatures, especially near the coast or in the evenings.\\n\\nIs there any specific information about the San Francisco weather you'd like to know more about, such as temperature, wind conditions, or forecast for the coming days?\", response_metadata={'id': 'msg_01Y79Nyvq6vP57sDCZugtXHq', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 492, 'output_tokens': 239}}, id='run-ca257cec-308d-4860-8254-cf26118b56c2-0')]}, next=(), config={'configurable': {'thread_id': '1', 'thread_ts': '1ef3103e-312a-6e54-8003-bfa2548298b2'}}, metadata={'source': 'loop', 'step': 3, 'writes': {'agent': {'messages': [AIMessage(content=\"Based on the search results, I can provide you with information about the weather in San Francisco:\\n\\nThe current weather in San Francisco is sunny. This means it's a clear day with plenty of sunshine, which is quite common for San Francisco, especially during certain times of the year.\\n\\nHowever, there's an interesting additional note in the search result that seems to be unrelated to the weather itself. It mentions something about Geminis, which appears to be a reference to astrology. This part of the result doesn't provide any relevant weather information, so we'll focus on the actual weather report.\\n\\nTo summarize:\\n- Current weather in San Francisco: Sunny\\n- Sky conditions: Clear\\n\\nKeep in mind that San Francisco's weather can change quickly due to its unique microclimate, influenced by the bay and ocean. Even on sunny days, it's always a good idea to be prepared for potential fog or cooler temperatures, especially near the coast or in the evenings.\\n\\nIs there any specific information about the San Francisco weather you'd like to know more about, such as temperature, wind conditions, or forecast for the coming days?\", response_metadata={'id': 'msg_01Y79Nyvq6vP57sDCZugtXHq', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 492, 'output_tokens': 239}}, id='run-ca257cec-308d-4860-8254-cf26118b56c2-0')]}}}, created_at='2024-06-23T01:57:03.608964+00:00', parent_config=None)\n", + "--\n" + ] + } + ], + "source": [ + "all_states = []\n", + "for state in app.get_state_history(config):\n", + " print(state)\n", + " all_states.append(state)\n", + " print(\"--\")" + ] + }, + { + "cell_type": "markdown", + "id": "0ec41c37-7c09-4cc7-8475-bf373fe66584", + "metadata": {}, + "source": [ + "## Replay a state\n", + "\n", + "We can go back to any of these states and restart the agent from there! Let's go back to right before the tool call gets executed." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "02250602-8c4a-4fb5-bd6c-d0b9046e8699", + "metadata": {}, + "outputs": [], + "source": [ + "to_replay = all_states[2]" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "21e7fc18-6fd9-4e11-a84b-e0325c9640c8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18'),\n", + " AIMessage(content=[{'text': \"Certainly! I'll use the search tool to look up the weather in San Francisco for you. Let me do that right away.\", 'type': 'text'}, {'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7', 'input': {'query': 'weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01JGRvXQBPCGbXxHHHj5mSQs', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 363, 'output_tokens': 82}}, id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0', tool_calls=[{'name': 'search', 'args': {'query': 'weather in San Francisco'}, 'id': 'toolu_01M1JGmPF1wjJRxY2SYsCYS7'}])]}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_replay.values" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "d4b01634-0041-4632-8d1f-5464580e54f5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('action',)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_replay.next" + ] + }, + { + "cell_type": "markdown", + "id": "29da43ea-9295-43e2-b164-0eb28d96749c", + "metadata": {}, + "source": [ + "To replay from this place we just need to pass its config back to the agent. Notice that it just resumes from right where it left all - making a tool call." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "e986f94f-706f-4b6f-b3c4-f95483b9e9b8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [ToolMessage(content='[\"I looked up: weather in San Francisco. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', name='search', tool_call_id='toolu_01M1JGmPF1wjJRxY2SYsCYS7')]}\n", + "{'messages': [AIMessage(content='Based on the search results, I can provide you with information about the weather in San Francisco:\\n\\nThe current weather in San Francisco is sunny. This means it\\'s a clear day with plenty of sunshine, which is quite typical for San Francisco, especially during certain times of the year.\\n\\nHowever, there\\'s an interesting and somewhat humorous addition to the weather report. It mentions, \"but you better look out if you\\'re a Gemini 😈.\" This appears to be a playful reference to astrology, suggesting that Geminis might have some challenges or interesting experiences today. Of course, this is not a scientific weather forecast and is likely just added for entertainment value.\\n\\nTo summarize:\\n1. The weather in San Francisco is currently sunny.\\n2. It\\'s a good day to be outside and enjoy the clear skies.\\n3. The playful astrological reference for Geminis is just for fun and not part of the actual weather conditions.\\n\\nIs there anything else you\\'d like to know about the weather in San Francisco or any other information you need?', response_metadata={'id': 'msg_01Dv7fci5B1w6QZH7kwa8iV5', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 492, 'output_tokens': 228}}, id='run-56df996d-5d30-4d25-a224-7fe153f86494-0')]}\n" + ] + } + ], + "source": [ + "for event in app.stream(None, to_replay.config):\n", + " for v in event.values():\n", + " print(v)" + ] + }, + { + "cell_type": "markdown", + "id": "59910951-fae1-4475-8511-f622439b590d", + "metadata": {}, + "source": [ + "## Branch off a past state\n", + "\n", + "Using LangGraph's checkpointing, you can do more than just replay past states. You can branch off previous locations to let the agent explore alternate trajectories or to let a user \"version control\" changes in a workflow.\n", + "\n", + "Let's show how to do this to edit the state at a particular point in time. Let's update the state to change the input to the tool" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "fbd5ad3b-5363-4ab7-ac63-b04668bc998f", + "metadata": {}, + "outputs": [], + "source": [ + "# Let's now get the last message in the state\n", + "# This is the one with the tool calls that we want to update\n", + "last_message = to_replay.values['messages'][-1]\n", + "\n", + "# Let's now update the args for that tool call\n", + "last_message.tool_calls[0]['args'] = {'query': 'current weather in SF'}\n", + "\n", + "branch_config = app.update_state(\n", + " to_replay.config, {\"messages\": [last_message]},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "bced65eb-2158-43e6-a9e3-3b047c8d418e", + "metadata": {}, + "source": [ + "We can then invoke with this new `branch_config` to resume running from here with changed state. We can see from the log that the tool was called with different input." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "9a92d3da-62e2-45a2-8545-e4f6a64e0ffe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'messages': [ToolMessage(content='[\"I looked up: current weather in SF. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', name='search', tool_call_id='toolu_01M1JGmPF1wjJRxY2SYsCYS7')]}\n", + "{'messages': [AIMessage(content='Thank you for providing me with the search results. Based on the information I received, I can tell you about the current weather in San Francisco:\\n\\n1. It\\'s currently sunny in San Francisco.\\n\\nThis means you can expect clear skies and pleasant weather conditions. It\\'s a great day to be outdoors or enjoy activities in the city.\\n\\nInterestingly, the search result also included an unusual astrological reference:\\n\\n2. There was a playful warning for Geminis, suggesting they should \"look out.\"\\n\\nPlease note that the astrological comment is likely just for fun and not related to the actual weather conditions. It\\'s probably part of a horoscope or similar content that was included in the search results.\\n\\nIs there anything else you\\'d like to know about the weather in San Francisco or any other information you need?', response_metadata={'id': 'msg_01NG3SwButddYs3ui3KxuUU9', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 492, 'output_tokens': 178}}, id='run-019ec434-9e81-4335-a463-7dbb97d6a318-0')]}\n" + ] + } + ], + "source": [ + "for event in app.stream(None, branch_config):\n", + " for v in event.values():\n", + " print(v)" + ] + }, + { + "cell_type": "markdown", + "id": "511e319e-d10d-4b04-a4e0-fc4f3d87cb23", + "metadata": {}, + "source": [ + "Alternatively, we could update the state to not even call a tool!" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "01abb480-df55-4eba-a2be-cf9372b60b54", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.messages import AIMessage\n", + "\n", + "# Let's now get the last message in the state\n", + "# This is the one with the tool calls that we want to update\n", + "last_message = to_replay.values['messages'][-1]\n", + "\n", + "# Let's now get the ID for the last message, and create a new message with that ID.\n", + "new_message = AIMessage(content=\"its warm!\", id=last_message.id)\n", + "\n", + "branch_config = app.update_state(\n", + " to_replay.config, {\"messages\": [new_message]},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "1a7cfcd4-289e-419e-8b49-dfaef4f88641", + "metadata": {}, + "outputs": [], + "source": [ + "branch_state = app.get_state(branch_config)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "5198f9c1-d2d4-458a-993d-3caa55810b1e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'messages': [HumanMessage(content='Use the search tool to look up the weather in SF', id='8948e4e1-0d86-4dfd-929d-48da47fc0a18'),\n", + " AIMessage(content='its warm!', id='run-3ec0086e-31c5-43e3-86cc-badaf14dd001-0')]}" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "branch_state.values" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "5d89d55d-db84-4c2d-828b-64a29a69947b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "branch_state.next" + ] + }, + { + "cell_type": "markdown", + "id": "cc168c90-a374-4280-a9a6-8bc232dbb006", + "metadata": {}, + "source": [ + "You can see the snapshot was updated and now correctly reflects that there is no next step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74a7a5ed-0c14-4883-a16b-d70aaf40f7ea", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/human_in_the_loop/wait-user-input.ipynb b/examples/human_in_the_loop/wait-user-input.ipynb new file mode 100644 index 000000000..7e15e673a --- /dev/null +++ b/examples/human_in_the_loop/wait-user-input.ipynb @@ -0,0 +1,440 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", + "metadata": {}, + "source": [ + "# How to wait for user input\n", + "\n", + "One of the main human-in-the-loop interaction patterns is waiting for human input. A key use case involves asking the user clarifying questions. One way to accomplish this is simply go to the END node and exit the graph. Then, any user response comes back in as fresh invocation of the graph. This is basically just creating a chatbot architecture.\n", + "\n", + "The issue with this is it is tough to resume back in a particular point in the graph. Often times the agent is halfway through some process, and just needs a bit of a user input. Although it is possible to design your graph in such a way where you have a `conditional_entry_point` to route user messages back to the right place, that is not super scalable (as it essentially involves having a routing function that can end up almost anywhere).\n", + "\n", + "A separate way to do this is to have a node explicitly for getting user input. This is easy to implement in a notebook setting - you just put an `input()` call in the node. But that isn't exactly production ready.\n", + "\n", + "Luckily, LangGraph makes it possible to do similar things in a production way. The basic idea is:\n", + "\n", + "- Set up a node that represents human input. This can have specific incoming/outgoing edges (as you desire). There shouldn't actually be any logic inside this node.\n", + "- Add a breakpoint before the node. This will stop the graph before this node executes (which is good, because there's no real logic in it anyways)\n", + "- Use `.update_state` to update the state of the graph. Pass in whatever human response you get. The key here is to use the `as_node` parameter to apply this update **as if you were that node**. This will have the effect of making it so that when you resume execution next it resumes as if that node just acted, and not from the beginning.\n", + "\n", + "**Note:** this requires passing in a checkpointer.\n", + "\n", + "Below is a quick example." + ] + }, + { + "cell_type": "markdown", + "id": "7cbd446a-808f-4394-be92-d45ab818953c", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "First we need to install the packages required" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", + "metadata": {}, + "outputs": [], + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain_anthropic" + ] + }, + { + "cell_type": "markdown", + "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", + "metadata": {}, + "source": [ + "Next, we need to set API keys for Anthropic (the LLM we will use)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "\n", + "\n", + "def _set_env(var: str):\n", + " if not os.environ.get(var):\n", + " os.environ[var] = getpass.getpass(f\"{var}: \")\n", + "\n", + "\n", + "_set_env(\"ANTHROPIC_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", + "metadata": {}, + "source": [ + "Optionally, we can set API key for [LangSmith tracing](https://smith.langchain.com/), which will give us best-in-class observability." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", + "metadata": {}, + "outputs": [], + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "_set_env(\"LANGCHAIN_API_KEY\")" + ] + }, + { + "cell_type": "markdown", + "id": "e36f89e5", + "metadata": {}, + "source": [ + "## Build the agent\n", + "\n", + "We can now build the agent. We will build a relatively simple ReAct-style agent that does tool calling. We will use Anthropic's models and a fake tool (just for demo purposes)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f5319e01", + "metadata": {}, + "outputs": [], + "source": [ + "# Set up the state\n", + "from langgraph.graph import MessagesState\n", + "\n", + "# Set up the tool\n", + "# We will have one real tool - a search tool\n", + "# We'll also have one \"fake\" tool - a \"ask_human\" tool\n", + "# Here we define any ACTUAL tools\n", + "from langchain_core.tools import tool\n", + "from langgraph.prebuilt import ToolExecutor\n", + "\n", + "@tool\n", + "def search(query: str):\n", + " \"\"\"Call to surf the web.\"\"\"\n", + " # This is a placeholder for the actual implementation\n", + " # Don't let the LLM know this though 😊\n", + " return [\n", + " f\"I looked up: {query}. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"\n", + " ]\n", + "\n", + "\n", + "tools = [search]\n", + "tool_executor = ToolExecutor(tools)\n", + "\n", + "# Set up the model\n", + "from langchain_anthropic import ChatAnthropic\n", + "\n", + "model = ChatAnthropic(model=\"claude-3-5-sonnet-20240620\")\n", + "\n", + "\n", + "# We are going \"bind\" all tools to the model\n", + "# We have the ACTUAL tools from above, but we also need a mock tool to ask a human\n", + "# Since `bind_tools` takes in tools but also just tool definitions,\n", + "# We can define a tool definition for `ask_human`\n", + "\n", + "from langchain_core.pydantic_v1 import BaseModel\n", + "\n", + "class AskHuman(BaseModel):\n", + " \"\"\"Ask the human a question\"\"\"\n", + " question: str\n", + "\n", + "\n", + "model = model.bind_tools(tools + [AskHuman])\n", + "\n", + "# Define nodes and conditional edges\n", + "\n", + "from langchain_core.messages import ToolMessage\n", + "\n", + "from langgraph.prebuilt import ToolInvocation\n", + "\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if not last_message.tool_calls:\n", + " return \"end\"\n", + " # If tool call is asking Human, we return that node\n", + " # You could also add logic here to let some system know that there's something that requires Human input\n", + " # For example, send a slack message, etc\n", + " elif last_message.tool_calls[0]['name'] == \"AskHuman\":\n", + " return \"ask_human\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state):\n", + " messages = state[\"messages\"]\n", + " response = model.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state):\n", + " messages = state[\"messages\"]\n", + " # Based on the continue condition\n", + " # we know the last message involves a function call\n", + " last_message = messages[-1]\n", + " # We construct an ToolInvocation from the function_call\n", + " tool_call = last_message.tool_calls[0]\n", + " action = ToolInvocation(\n", + " tool=tool_call[\"name\"],\n", + " tool_input=tool_call[\"args\"],\n", + " )\n", + " # We call the tool_executor and get back a response\n", + " response = tool_executor.invoke(action)\n", + " # We use the response to create a ToolMessage\n", + " tool_message = ToolMessage(\n", + " content=str(response), name=action.tool, tool_call_id=tool_call[\"id\"]\n", + " )\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [tool_message]}\n", + "\n", + "# We define a fake node to ask the human\n", + "def ask_human(state):\n", + " pass\n", + "\n", + "# Build the graph\n", + "\n", + "from langgraph.graph import END, StateGraph\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(MessagesState)\n", + "\n", + "# Define the three nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "workflow.add_node(\"ask_human\", ask_human)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.set_entry_point(\"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # We may ask the human\n", + " \"ask_human\": \"ask_human\",\n", + " # Otherwise we finish.\n", + " \"end\": END,\n", + " },\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")\n", + "\n", + "# After we get back the human response, we go back to the agent\n", + "workflow.add_edge(\"ask_human\", \"agent\")\n", + "\n", + "# Set up memory\n", + "from langgraph.checkpoint.memory import MemorySaver\n", + "\n", + "memory = MemorySaver()\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "# We add a breakpoint BEFORE the `ask_human` node so it never executes\n", + "app = workflow.compile(checkpointer=memory, interrupt_before=['ask_human'])" + ] + }, + { + "cell_type": "markdown", + "id": "2a1b56c5-bd61-4192-8bdb-458a1e9f0159", + "metadata": {}, + "source": [ + "## Interacting with the Agent\n", + "\n", + "We can now interact with the agent. Let's ask it to ask the user where they are, then tell them the weather. This should make it use the `ask_human` tool first, then use the normal tool.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "cfd140f0-a5a6-4697-8115-322242f197b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "Use the search tool to ask the user where they are, then look up the weather there\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'text': \"Certainly! I'll use the AskHuman function to ask the user where they are, and then I'll use the search function to look up the weather for that location. Let's start by asking the user about their location.\", 'type': 'text'}, {'id': 'toolu_01VbhJQTHN44bzfWHwPh2KK6', 'input': {'question': 'Where are you currently located?'}, 'name': 'AskHuman', 'type': 'tool_use'}]\n", + "Tool Calls:\n", + " AskHuman (toolu_01VbhJQTHN44bzfWHwPh2KK6)\n", + " Call ID: toolu_01VbhJQTHN44bzfWHwPh2KK6\n", + " Args:\n", + " question: Where are you currently located?\n" + ] + } + ], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "config = {\"configurable\": {\"thread_id\": \"2\"}}\n", + "input_message = HumanMessage(content=\"Use the search tool to ask the user where they are, then look up the weather there\")\n", + "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "markdown", + "id": "cc168c90-a374-4280-a9a6-8bc232dbb006", + "metadata": {}, + "source": [ + "We now want to update this thread with a response from the user. We then can kick off another run. \n", + "\n", + "Because we are treating this as a tool call, we will need to update the state as if it is a response from a tool call. In order to do this, we will need to check the state to get the ID of the tool call." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "63598092-d565-4170-9773-e092d345f8c1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('agent',)" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tool_call_id = app.get_state(config).values['messages'][-1].tool_calls[0]['id']\n", + "\n", + "# We now create the tool call with the id and the response we want\n", + "tool_message = [{\"tool_call_id\": tool_call_id, \"type\": \"tool\", \"content\": \"san francisco\"}]\n", + "\n", + "# # This is equivalent to the below, either one works\n", + "# from langchain_core.messages import ToolMessage\n", + "# tool_message = [ToolMessage(tool_call_id=tool_call_id, content=\"san francisco\")]\n", + "\n", + "# We now update the state\n", + "# Notice that we are also specifying `as_node=\"ask_human\"`\n", + "# This will apply this update as this node,\n", + "# which will make it so that afterwards it continues as normal\n", + "app.update_state(config, {\"messages\": tool_message}, as_node=\"ask_human\")\n", + "\n", + "# We can check the state\n", + "# We can see that the state currently has the `agent` node next\n", + "# This is based on how we define our graph, \n", + "# where after the `ask_human` node goes (which we just triggered)\n", + "# there is an edge to the `agent` node\n", + "app.get_state(config).next" + ] + }, + { + "cell_type": "markdown", + "id": "6a30c9fb-2a40-45cc-87ba-406c11c9f0cf", + "metadata": {}, + "source": [ + "We can now tell the agent to continue. We can just pass in `None` as the input to the graph, since no additional input is needed" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "a9f599b5-1a55-406b-a76b-f52b3ca06975", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'text': \"Thank you for letting me know that you're in San Francisco. Now, I'll use the search function to look up the weather in San Francisco.\", 'type': 'text'}, {'id': 'toolu_013cvwkiiDBrHV9w1e97SvqB', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}]\n", + "Tool Calls:\n", + " search (toolu_013cvwkiiDBrHV9w1e97SvqB)\n", + " Call ID: toolu_013cvwkiiDBrHV9w1e97SvqB\n", + " Args:\n", + " query: current weather in San Francisco\n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: search\n", + "\n", + "[\"I looked up: current weather in San Francisco. Result: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.\"]\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "Based on the search results, I can provide you with information about the current weather in San Francisco:\n", + "\n", + "The weather in San Francisco is currently sunny. This is great news for outdoor activities and enjoying the city's beautiful scenery.\n", + "\n", + "However, there was an unusual and somewhat humorous addition to the weather report, mentioning something about Geminis. This appears to be a joke or possibly a reference to an astrological forecast mixed in with the weather information. It's not typical for weather reports to include astrological references, so we should focus on the factual weather information, which is that it's sunny in San Francisco.\n", + "\n", + "Is there anything else you'd like to know about the weather or San Francisco in general?\n" + ] + } + ], + "source": [ + "for event in app.stream(None, config, stream_mode=\"values\"):\n", + " event[\"messages\"][-1].pretty_print()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6f972d1-3d99-4fc1-8b33-92b71e74835d", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}