diff --git a/docs/docs/how-tos/human_in_the_loop/review-tool-calls.ipynb b/docs/docs/how-tos/human_in_the_loop/review-tool-calls.ipynb index bdffd2733..c1653f0e1 100644 --- a/docs/docs/how-tos/human_in_the_loop/review-tool-calls.ipynb +++ b/docs/docs/how-tos/human_in_the_loop/review-tool-calls.ipynb @@ -12,12 +12,11 @@ "\n", " This guide assumes familiarity with the following concepts:\n", "\n", - " * [Tool calling])(https://python.langchain.com/docs/concepts/tool_calling/)\n", + " * [Tool calling](https://python.langchain.com/docs/concepts/tool_calling/)\n", " * [Human-in-the-loop](../../../concepts/human_in_the_loop)\n", - " * [Breakpoints](../../../concepts/breakpoints\n", " * [LangGraph Glossary](../../../concepts/low_level) \n", "\n", - "Human-in-the-loop (HIL) interactions are crucial for [agentic systems](https://langchain-ai.github.io/langgraph/concepts/agentic_concepts/#human-in-the-loop). A common pattern is to add some human in the loop step after certain tool calls. These tool calls often lead to either a function call or saving of some information. Examples include:\n", + "Human-in-the-loop (HIL) interactions are crucial for [agentic systems](../../../concepts/human-in-the-loop). A common pattern is to add some human in the loop step after certain tool calls. These tool calls often lead to either a function call or saving of some information. Examples include:\n", "\n", "- A tool call to execute SQL, which will then be run by the tool\n", "- A tool call to generate a summary, which will then be saved to the State of the graph\n", @@ -28,9 +27,42 @@ "\n", "1. Approve the tool call and continue\n", "2. Modify the tool call manually and then continue\n", - "3. Give natural language feedback, and then pass that back to the agent instead of continuing\n", + "3. Give natural language feedback, and then pass that back to the agent\n", "\n", - "We can implement this in LangGraph using a [breakpoint](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/): breakpoints allow us to interrupt graph execution before a specific step. At this breakpoint, we can manually update the graph state taking one of the three options above" + "\n", + "We can implement these in LangGraph using a [`interrupt()`][langgraph.types.interrupt]. `interrupt` allows us to stop graph execution to collect input from a user and continue execution with collected input:\n", + "\n", + "\n", + "```python\n", + "def human_review_node(state) -> Command[Literal[\"call_llm\", \"run_tool\"]]:\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, review_data = human_review\n", + " \n", + " # Approve the tool call and continue\n", + " if review_data == \"approve\":\n", + " return Command(goto=\"run_tool\")\n", + " \n", + " # Modify the tool call manually and then continue\n", + " elif review_action == \"update\":\n", + " ...\n", + " updated_msg = get_updated_msg(review_data)\n", + " return Command(goto=\"run_tool\", update={\"messages\": [updated_message]})\n", + "\n", + " # Give natural language feedback, and then pass that back to the agent\n", + " elif review_action == \"feedback\":\n", + " ...\n", + " feedback_msg = get_feedback_msg(review_data)\n", + " return Command(goto=\"call_llm\", update={\"messages\": [feedback_msg]})\n", + "\n", + "```" ] }, { @@ -67,7 +99,15 @@ "execution_count": 2, "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdin", + "output_type": "stream", + "text": [ + "ANTHROPIC_API_KEY: ········\n" + ] + } + ], "source": [ "import getpass\n", "import os\n", @@ -111,13 +151,13 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 3, "id": "85e452f8-f33a-4ead-bb4d-7386cdba8edc", "metadata": {}, "outputs": [ { "data": { - "image/jpeg": "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", + "image/png": "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", "text/plain": [ "" ] @@ -130,6 +170,7 @@ "from typing_extensions import TypedDict, Literal\n", "from langgraph.graph import StateGraph, START, END, MessagesState\n", "from langgraph.checkpoint.memory import MemorySaver\n", + "from langgraph.types import Command, interrupt\n", "from langchain_anthropic import ChatAnthropic\n", "from langchain_core.tools import tool\n", "from langchain_core.messages import AIMessage\n", @@ -145,7 +186,7 @@ " return \"Sunny!\"\n", "\n", "\n", - "model = ChatAnthropic(model_name=\"claude-3-5-sonnet-20240620\").bind_tools(\n", + "model = ChatAnthropic(model_name=\"claude-3-5-sonnet-latest\").bind_tools(\n", " [weather_search]\n", ")\n", "\n", @@ -158,8 +199,58 @@ " return {\"messages\": [model.invoke(state[\"messages\"])]}\n", "\n", "\n", - "def human_review_node(state):\n", - " pass\n", + "def human_review_node(state) -> Command[Literal[\"call_llm\", \"run_tool\"]]:\n", + " last_message = state[\"messages\"][-1]\n", + " tool_call = last_message.tool_calls[-1]\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=\"run_tool\")\n", + " \n", + " # update the AI message AND call tools\n", + " elif review_action == \"update\":\n", + " updated_message = {\n", + " \"role\": \"ai\",\n", + " \"content\": last_message.content,\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": tool_call[\"id\"],\n", + " \"name\": tool_call[\"name\"],\n", + " # This the update provided by the human\n", + " \"args\": review_data,\n", + " }\n", + " ],\n", + " # This is important - this needs to be the same as the message you replacing!\n", + " # Otherwise, it will show up as a separate message\n", + " \"id\": last_message.id,\n", + " }\n", + " return Command(goto=\"run_tool\", update={\"messages\": [updated_message]})\n", + "\n", + " # provide feedback to LLM\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=\"call_llm\", update={\"messages\": [tool_message]})\n", "\n", "\n", "def run_tool(state):\n", @@ -187,27 +278,19 @@ " return \"human_review_node\"\n", "\n", "\n", - "def route_after_human(state) -> Literal[\"run_tool\", \"call_llm\"]:\n", - " if isinstance(state[\"messages\"][-1], AIMessage):\n", - " return \"run_tool\"\n", - " else:\n", - " return \"call_llm\"\n", - "\n", - "\n", "builder = StateGraph(State)\n", "builder.add_node(call_llm)\n", "builder.add_node(run_tool)\n", "builder.add_node(human_review_node)\n", "builder.add_edge(START, \"call_llm\")\n", "builder.add_conditional_edges(\"call_llm\", route_after_llm)\n", - "builder.add_conditional_edges(\"human_review_node\", route_after_human)\n", "builder.add_edge(\"run_tool\", \"call_llm\")\n", "\n", "# Set up memory\n", "memory = MemorySaver()\n", "\n", "# Add\n", - "graph = builder.compile(checkpointer=memory, interrupt_before=[\"human_review_node\"])\n", + "graph = builder.compile(checkpointer=memory)\n", "\n", "# View\n", "display(Image(graph.get_graph().draw_mermaid_png()))" @@ -225,7 +308,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "id": "1b3aa6fc-c7fb-4819-8d7f-ba6057cc4edf", "metadata": {}, "outputs": [ @@ -233,8 +316,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'messages': [HumanMessage(content='hi!', id='393fa21d-4bfb-445b-8faa-78e22b92e346')]}\n", - "{'messages': [HumanMessage(content='hi!', id='393fa21d-4bfb-445b-8faa-78e22b92e346'), AIMessage(content=\"Hello! Welcome to our conversation. How can I assist you today? Is there anything specific you'd like to know or discuss?\", response_metadata={'id': 'msg_017S671xYvZm1mi9EcsKvPzF', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 355, 'output_tokens': 29}}, id='run-8ec507a1-5caf-47d6-89eb-1a2e8f38423c-0', usage_metadata={'input_tokens': 355, 'output_tokens': 29, 'total_tokens': 384})]}\n" + "{'call_llm': {'messages': [AIMessage(content=\"Hello! I'm here to help you. I can assist you with checking the weather in different cities using the weather search tool. Would you like to know the weather for a specific city? Just let me know which city you're interested in!\", additional_kwargs={}, response_metadata={'id': 'msg_01XHvA3ZWpsq4PdyiruWFLBs', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 374, 'output_tokens': 52}}, id='run-c3ff5fea-0135-4d66-8ec1-f8ed6a88356b-0', usage_metadata={'input_tokens': 374, 'output_tokens': 52, 'total_tokens': 426, 'input_token_details': {}})]}}\n", + "\n", + "\n" ] } ], @@ -246,8 +330,9 @@ "thread = {\"configurable\": {\"thread_id\": \"1\"}}\n", "\n", "# Run the graph until the first interruption\n", - "for event in graph.stream(initial_input, thread, stream_mode=\"values\"):\n", - " print(event)" + "for event in graph.stream(initial_input, thread, stream_mode=\"updates\"):\n", + " print(event)\n", + " print(\"\\n\")" ] }, { @@ -258,26 +343,6 @@ "If we check the state, we can see that it is finished" ] }, - { - "cell_type": "code", - "execution_count": 3, - "id": "213323cc-0320-4313-ab11-19042e28b495", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Pending Executions!\n", - "()\n" - ] - } - ], - "source": [ - "print(\"Pending Executions!\")\n", - "print(graph.get_state(thread).next)" - ] - }, { "cell_type": "markdown", "id": "5c1985f7-54f1-420f-a2b6-5e6154909966", @@ -290,7 +355,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "2561a38f-edb5-4b44-b2d7-6a7b70d2e6b7", "metadata": {}, "outputs": [ @@ -298,8 +363,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='8bda37cc-4bd3-4a14-bca5-b992934e710b')]}\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='8bda37cc-4bd3-4a14-bca5-b992934e710b'), AIMessage(content=[{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text'}, {'id': 'toolu_01MW3ETLpq4b8s6VaAMgDBZP', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_019FjC1prjVv8BuQX7DmF65F', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 360, 'output_tokens': 80}}, id='run-1b580410-173c-4fe0-a149-22e8f516b259-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01MW3ETLpq4b8s6VaAMgDBZP', 'type': 'tool_call'}], usage_metadata={'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440})]}\n" + "{'call_llm': {'messages': [AIMessage(content=[{'text': \"I'll help you check the weather in San Francisco.\", 'type': 'text'}, {'id': 'toolu_01Kn67GmQAA3BEF1cfYdNW3c', 'input': {'city': 'sf'}, 'name': 'weather_search', 'type': 'tool_use'}], additional_kwargs={}, response_metadata={'id': 'msg_013eJXUAEA2ANvYLkDUQFRPo', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 379, 'output_tokens': 65}}, id='run-e8174b94-f681-4688-967f-a32295412f91-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'sf'}, 'id': 'toolu_01Kn67GmQAA3BEF1cfYdNW3c', 'type': 'tool_call'}], usage_metadata={'input_tokens': 379, 'output_tokens': 65, 'total_tokens': 444, 'input_token_details': {}})]}}\n", + "\n", + "\n", + "{'__interrupt__': (Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'weather_search', 'args': {'city': 'sf'}, 'id': 'toolu_01Kn67GmQAA3BEF1cfYdNW3c', 'type': 'tool_call'}}, resumable=True, ns=['human_review_node:be252162-5b29-0a98-1ed2-c807c1fc64c6'], when='during'),)}\n", + "\n", + "\n" ] } ], @@ -311,8 +380,9 @@ "thread = {\"configurable\": {\"thread_id\": \"2\"}}\n", "\n", "# Run the graph until the first interruption\n", - "for event in graph.stream(initial_input, thread, stream_mode=\"values\"):\n", - " print(event)" + "for event in graph.stream(initial_input, thread, stream_mode=\"updates\"):\n", + " print(event)\n", + " print(\"\\n\")" ] }, { @@ -325,7 +395,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "33d68f0f-d435-4dd1-8013-6a59186dc9f5", "metadata": {}, "outputs": [ @@ -348,12 +418,12 @@ "id": "14c99fdd-4204-4c2d-b1af-02f38ab6ad57", "metadata": {}, "source": [ - "To approve the tool call, we can just continue the thread with no edits. To do this, we just create a new run with no inputs." + "To approve the tool call, we can just continue the thread with no edits. To do so, we need to let `human_review_node` know what value to use for the `human_review` variable we defined inside the node. We can provide this value by invoking the graph with a `Command(resume=)` input. Since we're approving the tool call, we'll provide `resume` value of `{\"action\": \"continue\"}` to navigate to `run_tool` node:" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "f9a0d5d4-52ff-49e0-a6f4-41f9a0e844d8", "metadata": {}, "outputs": [ @@ -361,17 +431,30 @@ "name": "stdout", "output_type": "stream", "text": [ + "{'human_review_node': None}\n", + "\n", + "\n", "----\n", - "Searching for: San Francisco\n", + "Searching for: sf\n", "----\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='8bda37cc-4bd3-4a14-bca5-b992934e710b'), AIMessage(content=[{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text'}, {'id': 'toolu_01MW3ETLpq4b8s6VaAMgDBZP', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_019FjC1prjVv8BuQX7DmF65F', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 360, 'output_tokens': 80}}, id='run-1b580410-173c-4fe0-a149-22e8f516b259-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01MW3ETLpq4b8s6VaAMgDBZP', 'type': 'tool_call'}], usage_metadata={'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}), ToolMessage(content='Sunny!', name='weather_search', id='835b0fe3-8aa0-45d5-ac29-03bbe57cc767', tool_call_id='toolu_01MW3ETLpq4b8s6VaAMgDBZP')]}\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='8bda37cc-4bd3-4a14-bca5-b992934e710b'), AIMessage(content=[{'text': 'To get the weather information for San Francisco, I can use the weather_search function. Let me do that for you.', 'type': 'text'}, {'id': 'toolu_01MW3ETLpq4b8s6VaAMgDBZP', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_019FjC1prjVv8BuQX7DmF65F', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 360, 'output_tokens': 80}}, id='run-1b580410-173c-4fe0-a149-22e8f516b259-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01MW3ETLpq4b8s6VaAMgDBZP', 'type': 'tool_call'}], usage_metadata={'input_tokens': 360, 'output_tokens': 80, 'total_tokens': 440}), ToolMessage(content='Sunny!', name='weather_search', id='835b0fe3-8aa0-45d5-ac29-03bbe57cc767', tool_call_id='toolu_01MW3ETLpq4b8s6VaAMgDBZP'), AIMessage(content=\"Based on the search results, the weather in San Francisco is sunny! It's a beautiful day in the city. Is there anything else you'd like to know about the weather or any other information I can help you with?\", response_metadata={'id': 'msg_01UY2d6RCzvwagwMb1J5etek', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 453, 'output_tokens': 49}}, id='run-7137f52c-abe6-4dc1-b536-92dd1d9187b0-0', usage_metadata={'input_tokens': 453, 'output_tokens': 49, 'total_tokens': 502})]}\n" + "{'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_01Kn67GmQAA3BEF1cfYdNW3c'}]}}\n", + "\n", + "\n", + "{'call_llm': {'messages': [AIMessage(content=\"According to the search, it's sunny in San Francisco today!\", additional_kwargs={}, response_metadata={'id': 'msg_01FJTbC8oK5fkD73rUBmAtUx', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 457, 'output_tokens': 17}}, id='run-c21af72d-3cc5-4b74-bb7c-fbeb8f88bd6d-0', usage_metadata={'input_tokens': 457, 'output_tokens': 17, 'total_tokens': 474, 'input_token_details': {}})]}}\n", + "\n", + "\n" ] } ], "source": [ - "for event in graph.stream(None, thread, stream_mode=\"values\"):\n", - " print(event)" + "for event in graph.stream(\n", + " # provide value \n", + " Command(resume={\"action\": \"continue\"}), \n", + " thread,\n", + " stream_mode=\"updates\"\n", + "):\n", + " print(event)\n", + " print(\"\\n\")" ] }, { @@ -386,7 +469,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "ec77831c-e6b8-4903-9146-e098a4b2fda1", "metadata": {}, "outputs": [ @@ -394,8 +477,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='0c488edd-7b9c-4416-ba02-8a2d7e9f2597')]}\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='0c488edd-7b9c-4416-ba02-8a2d7e9f2597'), AIMessage(content=[{'text': \"Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search tool. Let me fetch that for you.\", 'type': 'text'}, {'id': 'toolu_01CpbVmprQnjxpQzx8MzE1g8', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01Mv7iqdtPgZEX2LiBBqWDuY', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 360, 'output_tokens': 88}}, id='run-52a09799-efb5-4fff-82c3-884e20119ad3-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01CpbVmprQnjxpQzx8MzE1g8', 'type': 'tool_call'}], usage_metadata={'input_tokens': 360, 'output_tokens': 88, 'total_tokens': 448})]}\n" + "{'call_llm': {'messages': [AIMessage(content=[{'text': \"I'll help you check the weather in San Francisco.\", 'type': 'text'}, {'id': 'toolu_013eUXow3jwM6eekcDJdrjDa', 'input': {'city': 'sf'}, 'name': 'weather_search', 'type': 'tool_use'}], additional_kwargs={}, response_metadata={'id': 'msg_013ruFpCRNZKX3cDeBAH8rEb', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 379, 'output_tokens': 65}}, id='run-13df3982-ce6d-4fe2-9e5c-ea6ce30a63e4-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'sf'}, 'id': 'toolu_013eUXow3jwM6eekcDJdrjDa', 'type': 'tool_call'}], usage_metadata={'input_tokens': 379, 'output_tokens': 65, 'total_tokens': 444, 'input_token_details': {}})]}}\n", + "\n", + "\n", + "{'__interrupt__': (Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'weather_search', 'args': {'city': 'sf'}, 'id': 'toolu_013eUXow3jwM6eekcDJdrjDa', 'type': 'tool_call'}}, resumable=True, ns=['human_review_node:da717c23-60a0-2a1a-45de-cac5cff308bb'], when='during'),)}\n", + "\n", + "\n" ] } ], @@ -404,16 +491,17 @@ "initial_input = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf?\"}]}\n", "\n", "# Thread\n", - "thread = {\"configurable\": {\"thread_id\": \"5\"}}\n", + "thread = {\"configurable\": {\"thread_id\": \"3\"}}\n", "\n", "# Run the graph until the first interruption\n", - "for event in graph.stream(initial_input, thread, stream_mode=\"values\"):\n", - " print(event)" + "for event in graph.stream(initial_input, thread, stream_mode=\"updates\"):\n", + " print(event)\n", + " print(\"\\n\")" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "edcffbd7-829b-4d0c-88bf-cd531bc0e6b2", "metadata": {}, "outputs": [ @@ -436,72 +524,46 @@ "id": "87358aca-9b8f-48c7-98d4-3d755f6b0104", "metadata": {}, "source": [ - "To do this, we first need to update the state. We can do this by passing a message in with the **same** id of the message we want to overwrite. This will have the effect of **replacing** that old message. Note that this is only possible because of the **reducer** we are using that replaces messages with the same ID - read more about that [here](https://langchain-ai.github.io/langgraph/concepts/low_level/#working-with-messages-in-graph-state)" + "To do this, we will use `Command` with a different resume value of `{\"action\": \"update\", \"data\": }`. This will do the following:\n", + "\n", + "* combine existing tool call with user-provided tool call arguments and update the existing AI message with the new tool call\n", + "* navigate to `run_tool` node with the updated AI message and continue execution" ] }, { "cell_type": "code", - "execution_count": 9, - "id": "df4a9900-d953-4465-b8af-bd2858cb63ea", + "execution_count": 10, + "id": "b2f73998-baae-4c00-8a90-f4153e924941", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Current State:\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='0c488edd-7b9c-4416-ba02-8a2d7e9f2597'), AIMessage(content=[{'text': \"Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search tool. Let me fetch that for you.\", 'type': 'text'}, {'id': 'toolu_01CpbVmprQnjxpQzx8MzE1g8', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_01Mv7iqdtPgZEX2LiBBqWDuY', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 360, 'output_tokens': 88}}, id='run-52a09799-efb5-4fff-82c3-884e20119ad3-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01CpbVmprQnjxpQzx8MzE1g8', 'type': 'tool_call'}], usage_metadata={'input_tokens': 360, 'output_tokens': 88, 'total_tokens': 448})]}\n", + "{'human_review_node': {'messages': [{'role': 'ai', 'content': [{'text': \"I'll help you check the weather in San Francisco.\", 'type': 'text'}, {'id': 'toolu_013eUXow3jwM6eekcDJdrjDa', 'input': {'city': 'sf'}, 'name': 'weather_search', 'type': 'tool_use'}], 'tool_calls': [{'id': 'toolu_013eUXow3jwM6eekcDJdrjDa', 'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}}], 'id': 'run-13df3982-ce6d-4fe2-9e5c-ea6ce30a63e4-0'}]}}\n", + "\n", "\n", - "Current Tool Call ID:\n", - "toolu_01CpbVmprQnjxpQzx8MzE1g8\n", "----\n", "Searching for: San Francisco, USA\n", "----\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='0c488edd-7b9c-4416-ba02-8a2d7e9f2597'), AIMessage(content=[{'text': \"Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search tool. Let me fetch that for you.\", 'type': 'text'}, {'id': 'toolu_01CpbVmprQnjxpQzx8MzE1g8', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], id='run-52a09799-efb5-4fff-82c3-884e20119ad3-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01CpbVmprQnjxpQzx8MzE1g8', 'type': 'tool_call'}]), ToolMessage(content='Sunny!', name='weather_search', id='ff968b9f-9b87-4893-9f32-dfb88dbe0536', tool_call_id='toolu_01CpbVmprQnjxpQzx8MzE1g8')]}\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='0c488edd-7b9c-4416-ba02-8a2d7e9f2597'), AIMessage(content=[{'text': \"Certainly! I can help you check the weather in San Francisco. To get this information, I'll use the weather search tool. Let me fetch that for you.\", 'type': 'text'}, {'id': 'toolu_01CpbVmprQnjxpQzx8MzE1g8', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], id='run-52a09799-efb5-4fff-82c3-884e20119ad3-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01CpbVmprQnjxpQzx8MzE1g8', 'type': 'tool_call'}]), ToolMessage(content='Sunny!', name='weather_search', id='ff968b9f-9b87-4893-9f32-dfb88dbe0536', tool_call_id='toolu_01CpbVmprQnjxpQzx8MzE1g8'), AIMessage(content=\"Great news! The weather in San Francisco is currently sunny. It's a beautiful day in the city by the bay. Is there anything else you'd like to know about the weather or any other information I can help you with?\", response_metadata={'id': 'msg_01PhwUeRWkSJB6kzHZS361XZ', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 464, 'output_tokens': 50}}, id='run-5aebcf37-626e-4675-b225-476bc99bdbb8-0', usage_metadata={'input_tokens': 464, 'output_tokens': 50, 'total_tokens': 514})]}\n" + "{'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_013eUXow3jwM6eekcDJdrjDa'}]}}\n", + "\n", + "\n", + "{'call_llm': {'messages': [AIMessage(content=\"According to the search, it's sunny in San Francisco right now!\", additional_kwargs={}, response_metadata={'id': 'msg_01QssVtxXPqr8NWjYjTaiHqN', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 460, 'output_tokens': 18}}, id='run-8ab865c8-cc9e-4300-8e1d-9eb673e8445c-0', usage_metadata={'input_tokens': 460, 'output_tokens': 18, 'total_tokens': 478, 'input_token_details': {}})]}}\n", + "\n", + "\n" ] } ], "source": [ - "# To get the ID of the message we want to replace, we need to fetch the current state and find it there.\n", - "state = graph.get_state(thread)\n", - "print(\"Current State:\")\n", - "print(state.values)\n", - "print(\"\\nCurrent Tool Call ID:\")\n", - "current_content = state.values[\"messages\"][-1].content\n", - "current_id = state.values[\"messages\"][-1].id\n", - "tool_call_id = state.values[\"messages\"][-1].tool_calls[0][\"id\"]\n", - "print(tool_call_id)\n", - "\n", - "# We now need to construct a replacement tool call.\n", - "# We will change the argument to be `San Francisco, USA`\n", - "# Note that we could change any number of arguments or tool names - it just has to be a valid one\n", - "new_message = {\n", - " \"role\": \"assistant\",\n", - " \"content\": current_content,\n", - " \"tool_calls\": [\n", - " {\n", - " \"id\": tool_call_id,\n", - " \"name\": \"weather_search\",\n", - " \"args\": {\"city\": \"San Francisco, USA\"},\n", - " }\n", - " ],\n", - " # This is important - this needs to be the same as the message you replacing!\n", - " # Otherwise, it will show up as a separate message\n", - " \"id\": current_id,\n", - "}\n", - "graph.update_state(\n", - " # This is the config which represents this thread\n", - " thread,\n", - " # This is the updated value we want to push\n", - " {\"messages\": [new_message]},\n", - " # We push this update acting as our human_review_node\n", - " as_node=\"human_review_node\",\n", - ")\n", - "\n", "# Let's now continue executing from here\n", - "for event in graph.stream(None, thread, stream_mode=\"values\"):\n", - " print(event)" + "for event in graph.stream(\n", + " Command(resume={\"action\": \"update\", \"data\": {\"city\": \"San Francisco, USA\"}}), \n", + " thread, \n", + " stream_mode=\"updates\"\n", + "):\n", + " print(event)\n", + " print(\"\\n\")" ] }, { @@ -511,21 +573,21 @@ "source": [ "## Give feedback to a tool call\n", "\n", - "Sometimes, you may not want to execute a tool call, but you also may not want to ask the user to manually modify the tool call. In that case it may be better to get natural language feedback from the user. You can then insert these feedback as a mock **RESULT** of the tool call.\n", + "Sometimes, you may not want to execute a tool call, but you also may not want to ask the user to manually modify the tool call. In that case it may be better to get natural language feedback from the user. You can then insert this feedback as a mock **RESULT** of the tool call.\n", "\n", "There are multiple ways to do this:\n", "\n", "1. You could add a new message to the state (representing the \"result\" of a tool call)\n", "2. You could add TWO new messages to the state - one representing an \"error\" from the tool call, other HumanMessage representing the feedback\n", "\n", - "Both are similar in that they involve adding messages to the state. The main difference lies in the logic AFTER the `human_node` and how it handles different types of messages.\n", + "Both are similar in that they involve adding messages to the state. The main difference lies in the logic AFTER the `human_review_node` and how it handles different types of messages.\n", "\n", - "For this example we will just add a single tool call representing the feedback. Let's see this in action!" + "For this example we will just add a single tool call representing the feedback (see `human_review_node` implementation). Let's see this in action!" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "id": "d57d5131-7912-4216-aa87-b7272507fa51", "metadata": {}, "outputs": [ @@ -533,8 +595,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='601c4c75-f506-4d91-896d-5e382123de24')]}\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='601c4c75-f506-4d91-896d-5e382123de24'), AIMessage(content=[{'text': \"Certainly! I can help you check the weather in San Francisco. To get the most accurate and up-to-date information, I'll use the weather search tool. Let me fetch that for you right away.\", 'type': 'text'}, {'id': 'toolu_014UTKh5uqfc885Fj4RRqGdg', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_013nHyPYxNXFSoXeS6q4oWua', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 360, 'output_tokens': 98}}, id='run-0537e15e-86a4-4c6f-8dfb-6e4c160812c4-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_014UTKh5uqfc885Fj4RRqGdg', 'type': 'tool_call'}], usage_metadata={'input_tokens': 360, 'output_tokens': 98, 'total_tokens': 458})]}\n" + "{'call_llm': {'messages': [AIMessage(content=[{'text': \"I'll help you check the weather in San Francisco.\", 'type': 'text'}, {'id': 'toolu_01QxXNTCasnNLQCGAiVoNUBe', 'input': {'city': 'sf'}, 'name': 'weather_search', 'type': 'tool_use'}], additional_kwargs={}, response_metadata={'id': 'msg_01DjwkVxgfqT2K329rGkycx6', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 379, 'output_tokens': 65}}, id='run-c57bee36-9f5f-4d2e-85df-758b56d3cc05-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'sf'}, 'id': 'toolu_01QxXNTCasnNLQCGAiVoNUBe', 'type': 'tool_call'}], usage_metadata={'input_tokens': 379, 'output_tokens': 65, 'total_tokens': 444, 'input_token_details': {}})]}}\n", + "\n", + "\n", + "{'__interrupt__': (Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'weather_search', 'args': {'city': 'sf'}, 'id': 'toolu_01QxXNTCasnNLQCGAiVoNUBe', 'type': 'tool_call'}}, resumable=True, ns=['human_review_node:47a3f541-b630-5f8a-32d7-5a44826d99da'], when='during'),)}\n", + "\n", + "\n" ] } ], @@ -543,16 +609,17 @@ "initial_input = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf?\"}]}\n", "\n", "# Thread\n", - "thread = {\"configurable\": {\"thread_id\": \"6\"}}\n", + "thread = {\"configurable\": {\"thread_id\": \"4\"}}\n", "\n", "# Run the graph until the first interruption\n", - "for event in graph.stream(initial_input, thread, stream_mode=\"values\"):\n", - " print(event)" + "for event in graph.stream(initial_input, thread, stream_mode=\"updates\"):\n", + " print(event)\n", + " print(\"\\n\")" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "id": "e33ad664-0307-43c5-b85a-1e02eebceb5c", "metadata": {}, "outputs": [ @@ -575,12 +642,15 @@ "id": "483d9455-8625-4c6a-9b98-f731403b2ed3", "metadata": {}, "source": [ - "To do this, we first need to update the state. We can do this by passing a message in with the same **tool call id** of the tool call we want to respond to. Note that this is a **different** ID from above." + "To do this, we will use `Command` with a different resume value of `{\"action\": \"feedback\", \"data\": }`. This will do the following:\n", + "\n", + "* create a new tool message that combines existing tool call from LLM with the with user-provided feedback as content\n", + "* navigate to `call_llm` node with the updated tool message and continue execution" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "id": "3f05f8b6-6128-4de5-8884-862fc93f1227", "metadata": {}, "outputs": [ @@ -588,46 +658,28 @@ "name": "stdout", "output_type": "stream", "text": [ - "Current State:\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='601c4c75-f506-4d91-896d-5e382123de24'), AIMessage(content=[{'text': \"Certainly! I can help you check the weather in San Francisco. To get the most accurate and up-to-date information, I'll use the weather search tool. Let me fetch that for you right away.\", 'type': 'text'}, {'id': 'toolu_014UTKh5uqfc885Fj4RRqGdg', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_013nHyPYxNXFSoXeS6q4oWua', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 360, 'output_tokens': 98}}, id='run-0537e15e-86a4-4c6f-8dfb-6e4c160812c4-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_014UTKh5uqfc885Fj4RRqGdg', 'type': 'tool_call'}], usage_metadata={'input_tokens': 360, 'output_tokens': 98, 'total_tokens': 458})]}\n", + "{'human_review_node': {'messages': [{'role': 'tool', 'content': 'User requested changes: use format for location', 'name': 'weather_search', 'tool_call_id': 'toolu_01QxXNTCasnNLQCGAiVoNUBe'}]}}\n", "\n", - "Current Tool Call ID:\n", - "toolu_014UTKh5uqfc885Fj4RRqGdg\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='601c4c75-f506-4d91-896d-5e382123de24'), AIMessage(content=[{'text': \"Certainly! I can help you check the weather in San Francisco. To get the most accurate and up-to-date information, I'll use the weather search tool. Let me fetch that for you right away.\", 'type': 'text'}, {'id': 'toolu_014UTKh5uqfc885Fj4RRqGdg', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_013nHyPYxNXFSoXeS6q4oWua', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 360, 'output_tokens': 98}}, id='run-0537e15e-86a4-4c6f-8dfb-6e4c160812c4-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_014UTKh5uqfc885Fj4RRqGdg', 'type': 'tool_call'}], usage_metadata={'input_tokens': 360, 'output_tokens': 98, 'total_tokens': 458}), ToolMessage(content='User requested changes: pass in the country as well', name='weather_search', id='e20ceddc-a0d3-469d-b31e-512f3042a07e', tool_call_id='toolu_014UTKh5uqfc885Fj4RRqGdg'), AIMessage(content=[{'text': \"I apologize for the oversight. It seems that the weather search function requires more specific information. Let's try again with a more detailed search, including the country. Since San Francisco is commonly associated with the one in California, USA, I'll use that. Here's the updated search:\", 'type': 'text'}, {'id': 'toolu_01AaipBbWDLjHnPcoApx8wRq', 'input': {'city': 'San Francisco, USA'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_018rErqC2cLe2VVhebdJf81e', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 480, 'output_tokens': 116}}, id='run-fcba65ed-400a-4783-9ecd-e22051682399-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01AaipBbWDLjHnPcoApx8wRq', 'type': 'tool_call'}], usage_metadata={'input_tokens': 480, 'output_tokens': 116, 'total_tokens': 596})]}\n" + "\n", + "{'call_llm': {'messages': [AIMessage(content=[{'text': 'Let me try again with the full city name.', 'type': 'text'}, {'id': 'toolu_01WBGTKBWusaPNZYJi5LKmeQ', 'input': {'city': 'San Francisco, USA'}, 'name': 'weather_search', 'type': 'tool_use'}], additional_kwargs={}, response_metadata={'id': 'msg_0141KCdx6KhJmWXyYwAYGvmj', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 468, 'output_tokens': 68}}, id='run-60c8267a-52c7-4b6e-87ca-16aa3bd6266b-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01WBGTKBWusaPNZYJi5LKmeQ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 468, 'output_tokens': 68, 'total_tokens': 536, 'input_token_details': {}})]}}\n", + "\n", + "\n", + "{'__interrupt__': (Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01WBGTKBWusaPNZYJi5LKmeQ', 'type': 'tool_call'}}, resumable=True, ns=['human_review_node:621fc4a9-bbf1-9a99-f50b-3bf91675234e'], when='during'),)}\n", + "\n", + "\n" ] } ], "source": [ - "# To get the ID of the message we want to replace, we need to fetch the current state and find it there.\n", - "state = graph.get_state(thread)\n", - "print(\"Current State:\")\n", - "print(state.values)\n", - "print(\"\\nCurrent Tool Call ID:\")\n", - "tool_call_id = state.values[\"messages\"][-1].tool_calls[0][\"id\"]\n", - "print(tool_call_id)\n", - "\n", - "# We now need to construct a replacement tool call.\n", - "# We will change the argument to be `San Francisco, USA`\n", - "# Note that we could change any number of arguments or tool names - it just has to be a valid one\n", - "new_message = {\n", - " \"role\": \"tool\",\n", - " # This is our natural language feedback\n", - " \"content\": \"User requested changes: pass in the country as well\",\n", - " \"name\": \"weather_search\",\n", - " \"tool_call_id\": tool_call_id,\n", - "}\n", - "graph.update_state(\n", - " # This is the config which represents this thread\n", - " thread,\n", - " # This is the updated value we want to push\n", - " {\"messages\": [new_message]},\n", - " # We push this update acting as our human_review_node\n", - " as_node=\"human_review_node\",\n", - ")\n", - "\n", "# Let's now continue executing from here\n", - "for event in graph.stream(None, thread, stream_mode=\"values\"):\n", - " print(event)" + "for event in graph.stream(\n", + " # provide our natural language feedback!\n", + " Command(resume={\"action\": \"feedback\", \"data\": \"User requested changes: use format for location\"}), \n", + " thread, \n", + " stream_mode=\"updates\"\n", + "):\n", + " print(event)\n", + " print(\"\\n\")" ] }, { @@ -635,13 +687,13 @@ "id": "2d2e79ab-7cdb-42ce-b2ca-2932f8782c90", "metadata": {}, "source": [ - "We can see that we now get to another breakpoint - because it went back to the model and got an entirely new prediction of what to call. Let's now approve this one and continue." + "We can see that we now get to another interrupt - because it went back to the model and got an entirely new prediction of what to call. Let's now approve this one and continue." ] }, { "cell_type": "code", - "execution_count": 13, - "id": "a30d40ad-611d-4ec3-84be-869ea05acb89", + "execution_count": 14, + "id": "ca558915-f4d9-4ff2-95b7-cdaf0c6db485", "metadata": {}, "outputs": [ { @@ -649,21 +701,48 @@ "output_type": "stream", "text": [ "Pending Executions!\n", - "('human_review_node',)\n", - "----\n", - "Searching for: San Francisco, USA\n", - "----\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='601c4c75-f506-4d91-896d-5e382123de24'), AIMessage(content=[{'text': \"Certainly! I can help you check the weather in San Francisco. To get the most accurate and up-to-date information, I'll use the weather search tool. Let me fetch that for you right away.\", 'type': 'text'}, {'id': 'toolu_014UTKh5uqfc885Fj4RRqGdg', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_013nHyPYxNXFSoXeS6q4oWua', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 360, 'output_tokens': 98}}, id='run-0537e15e-86a4-4c6f-8dfb-6e4c160812c4-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_014UTKh5uqfc885Fj4RRqGdg', 'type': 'tool_call'}], usage_metadata={'input_tokens': 360, 'output_tokens': 98, 'total_tokens': 458}), ToolMessage(content='User requested changes: pass in the country as well', name='weather_search', id='e20ceddc-a0d3-469d-b31e-512f3042a07e', tool_call_id='toolu_014UTKh5uqfc885Fj4RRqGdg'), AIMessage(content=[{'text': \"I apologize for the oversight. It seems that the weather search function requires more specific information. Let's try again with a more detailed search, including the country. Since San Francisco is commonly associated with the one in California, USA, I'll use that. Here's the updated search:\", 'type': 'text'}, {'id': 'toolu_01AaipBbWDLjHnPcoApx8wRq', 'input': {'city': 'San Francisco, USA'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_018rErqC2cLe2VVhebdJf81e', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 480, 'output_tokens': 116}}, id='run-fcba65ed-400a-4783-9ecd-e22051682399-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01AaipBbWDLjHnPcoApx8wRq', 'type': 'tool_call'}], usage_metadata={'input_tokens': 480, 'output_tokens': 116, 'total_tokens': 596}), ToolMessage(content='Sunny!', name='weather_search', id='3f3ee262-70f5-422c-8e3f-6a9af758514d', tool_call_id='toolu_01AaipBbWDLjHnPcoApx8wRq')]}\n", - "{'messages': [HumanMessage(content=\"what's the weather in sf?\", id='601c4c75-f506-4d91-896d-5e382123de24'), AIMessage(content=[{'text': \"Certainly! I can help you check the weather in San Francisco. To get the most accurate and up-to-date information, I'll use the weather search tool. Let me fetch that for you right away.\", 'type': 'text'}, {'id': 'toolu_014UTKh5uqfc885Fj4RRqGdg', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_013nHyPYxNXFSoXeS6q4oWua', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 360, 'output_tokens': 98}}, id='run-0537e15e-86a4-4c6f-8dfb-6e4c160812c4-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_014UTKh5uqfc885Fj4RRqGdg', 'type': 'tool_call'}], usage_metadata={'input_tokens': 360, 'output_tokens': 98, 'total_tokens': 458}), ToolMessage(content='User requested changes: pass in the country as well', name='weather_search', id='e20ceddc-a0d3-469d-b31e-512f3042a07e', tool_call_id='toolu_014UTKh5uqfc885Fj4RRqGdg'), AIMessage(content=[{'text': \"I apologize for the oversight. It seems that the weather search function requires more specific information. Let's try again with a more detailed search, including the country. Since San Francisco is commonly associated with the one in California, USA, I'll use that. Here's the updated search:\", 'type': 'text'}, {'id': 'toolu_01AaipBbWDLjHnPcoApx8wRq', 'input': {'city': 'San Francisco, USA'}, 'name': 'weather_search', 'type': 'tool_use'}], response_metadata={'id': 'msg_018rErqC2cLe2VVhebdJf81e', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'input_tokens': 480, 'output_tokens': 116}}, id='run-fcba65ed-400a-4783-9ecd-e22051682399-0', tool_calls=[{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01AaipBbWDLjHnPcoApx8wRq', 'type': 'tool_call'}], usage_metadata={'input_tokens': 480, 'output_tokens': 116, 'total_tokens': 596}), ToolMessage(content='Sunny!', name='weather_search', id='3f3ee262-70f5-422c-8e3f-6a9af758514d', tool_call_id='toolu_01AaipBbWDLjHnPcoApx8wRq'), AIMessage(content=\"Great news! The weather in San Francisco, USA is currently sunny. \\n\\nHere's a summary of the weather information:\\n- Location: San Francisco, USA\\n- Current conditions: Sunny\\n\\nIt's a beautiful day in San Francisco! The sunny weather is perfect for outdoor activities or simply enjoying the city. Remember to wear sunscreen and stay hydrated if you plan to spend time outside. \\n\\nIs there anything else you'd like to know about the weather in San Francisco or any other location?\", response_metadata={'id': 'msg_017Pnjyte2ZXAREgUvEqbUVt', 'model': 'claude-3-5-sonnet-20240620', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 609, 'output_tokens': 107}}, id='run-30c0d0ef-09a3-40ad-b410-80019b284983-0', usage_metadata={'input_tokens': 609, 'output_tokens': 107, 'total_tokens': 716})]}\n" + "('human_review_node',)\n" ] } ], "source": [ "print(\"Pending Executions!\")\n", - "print(graph.get_state(thread).next)\n", - "\n", - "for event in graph.stream(None, thread, stream_mode=\"values\"):\n", - " print(event)" + "print(graph.get_state(thread).next)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "a30d40ad-611d-4ec3-84be-869ea05acb89", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'human_review_node': None}\n", + "\n", + "\n", + "----\n", + "Searching for: San Francisco, USA\n", + "----\n", + "{'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_01WBGTKBWusaPNZYJi5LKmeQ'}]}}\n", + "\n", + "\n", + "{'call_llm': {'messages': [AIMessage(content='The weather in San Francisco is sunny!', additional_kwargs={}, response_metadata={'id': 'msg_01JrfZd8SYyH51Q8rhZuaC3W', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 549, 'output_tokens': 12}}, id='run-09a198b2-79fa-484d-9d9d-f12432978488-0', usage_metadata={'input_tokens': 549, 'output_tokens': 12, 'total_tokens': 561, 'input_token_details': {}})]}}\n", + "\n", + "\n" + ] + } + ], + "source": [ + "for event in graph.stream(\n", + " Command(resume={\"action\": \"continue\"}), \n", + " thread, \n", + " stream_mode=\"updates\"\n", + "):\n", + " print(event)\n", + " print(\"\\n\")" ] } ], @@ -683,7 +762,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.12.3" } }, "nbformat": 4,