From 265f5012c136ba32ab0d24c026707220d30577f3 Mon Sep 17 00:00:00 2001 From: Andrew Nguonly Date: Mon, 24 Jun 2024 16:30:18 -0700 Subject: [PATCH] docs: Breakup LangGraph Cloud human-in-the-loop notebook (#794) * Breakup how-to pages for human-in-the-loop. * Update indentation of Python code in notebooks. --- docs/docs/cloud/concepts/index.md | 4 +- docs/mkdocs.yml | 6 +- .../human-in-the-loop_cloud.ipynb | 675 ------------------ .../human_in_the_loop_breakpoint.ipynb | 116 +++ .../human_in_the_loop_edit_state.ipynb | 173 +++++ .../human_in_the_loop_time_travel.ipynb | 235 ++++++ .../human_in_the_loop_user_input.ipynb | 175 +++++ 7 files changed, 706 insertions(+), 678 deletions(-) delete mode 100644 examples/cloud_examples/human-in-the-loop_cloud.ipynb create mode 100644 examples/cloud_examples/human_in_the_loop_breakpoint.ipynb create mode 100644 examples/cloud_examples/human_in_the_loop_edit_state.ipynb create mode 100644 examples/cloud_examples/human_in_the_loop_time_travel.ipynb create mode 100644 examples/cloud_examples/human_in_the_loop_user_input.ipynb diff --git a/docs/docs/cloud/concepts/index.md b/docs/docs/cloud/concepts/index.md index 91fc813a3..aec2ded36 100644 --- a/docs/docs/cloud/concepts/index.md +++ b/docs/docs/cloud/concepts/index.md @@ -47,10 +47,10 @@ You can also specify multiple streaming modes at the same time. See the [How-to See the [API Reference](../reference/api_ref.md) for how to create streaming runs. ### Human-in-the-Loop -There are many occasions where the graph cannot run completely autonomously. For instance, the user might need to input some additional arguments to a function call, or select the next edge for the graph to continue on. In these instances, we need to insert some human in the loop interaction, which you can learn about in [this how-to](../how_tos/cloud_examples/human-in-the-loop_cloud). +There are many occasions where the graph cannot run completely autonomously. For instance, the user might need to input some additional arguments to a function call, or select the next edge for the graph to continue on. In these instances, we need to insert some human in the loop interaction, which you can learn about in the [human in the loop how-tos](../how-tos/cloud_examples/human_in_the_loop_breakpoint). ### Double Texting -Many times users might interact with your graph in unintended ways. For instance, a user may send one message and before the graph has finished running send a second message. To solve this issue of "double-texting" (i.e. prompting the graph a second time before the first run has finished), Langgraph has provided four different solutions, all of which are covered in the [Double Texting how-tos](../how_tos/cloud_examples/interrupt_concurrent/). These options are: +Many times users might interact with your graph in unintended ways. For instance, a user may send one message and before the graph has finished running send a second message. To solve this issue of "double-texting" (i.e. prompting the graph a second time before the first run has finished), Langgraph has provided four different solutions, all of which are covered in the [Double Texting how-tos](../how-tos/cloud_examples/interrupt_concurrent/). These options are: - `reject`: This is the simplest option, this just rejects any follow up runs and does not allow double texting. - `enqueue`: This is a relatively simple option which continues the first run until it completes the whole run, then sends the new input as a separate run. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index a28948043..9799e0a43 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -189,7 +189,11 @@ nav: - Enqueue: 'cloud/how-tos/cloud_examples/enqueue_concurrent.ipynb' - Run Agent in Background: 'cloud/how-tos/cloud_examples/background_run.ipynb' - Run Multiple Agents in Thread: 'cloud/how-tos/cloud_examples/same-thread.ipynb' - - Human in the loop: 'cloud/how-tos/cloud_examples/human-in-the-loop_cloud.ipynb' + - Human-in-the-Loop: + - Add Breakpoint: 'cloud/how-tos/cloud_examples/human_in_the_loop_breakpoint.ipynb' + - Wait for User Input: 'cloud/how-tos/cloud_examples/human_in_the_loop_user_input.ipynb' + - Edit Graph State: 'cloud/how-tos/cloud_examples/human_in_the_loop_edit_state.ipynb' + - Replay and Branch from Prior States: 'cloud/how-tos/cloud_examples/human_in_the_loop_time_travel.ipynb' - Create Agents with Configuration: 'cloud/how-tos/cloud_examples/configuration_cloud.ipynb' - Convert LangGraph calls to LangGraph Cloud calls: 'cloud/how-tos/cloud_examples/langgraph_to_langgraph_cloud.ipynb' - SDK: diff --git a/examples/cloud_examples/human-in-the-loop_cloud.ipynb b/examples/cloud_examples/human-in-the-loop_cloud.ipynb deleted file mode 100644 index ec305b0fa..000000000 --- a/examples/cloud_examples/human-in-the-loop_cloud.ipynb +++ /dev/null @@ -1,675 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# How to have a human in the loop\n", - "\n", - "With it's built in persistence layer, LangGraph API is perfect for human-in-the-loop workflows.\n", - "Here we cover a few such examples:\n", - "\n", - "1. Having a human in the loop to approve a tool call\n", - "2. Having a human in the loop to edit a tool call\n", - "3. Having a human in the loop to edit an old state and resume execution from there\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "521d975b-e94b-4c37-bfa1-82d969e2a4dc", - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph_sdk import get_client" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "27a1392b-86c3-464e-99a8-90ffc965f3ec", - "metadata": {}, - "outputs": [], - "source": [ - "client = get_client()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "230c0464-a6e5-420f-9e38-ca514e5634ce", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca',\n", - " 'graph_id': 'agent',\n", - " 'config': {},\n", - " 'created_at': '2024-05-18T00:19:39.688822+00:00',\n", - " 'updated_at': '2024-05-18T00:19:39.688822+00:00',\n", - " 'metadata': {'created_by': 'system'}}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "assistant_id = \"agent\"" - ] - }, - { - "cell_type": "markdown", - "id": "e0209129-239b-452e-a59a-47be716bbf8c", - "metadata": {}, - "source": [ - "## Approve a tool call" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "56aa5159-5583-4134-9210-709b969bda6f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'thread_id': '54ed0901-6767-46c9-a5f9-b65c1c5fd89c',\n", - " 'created_at': '2024-05-18T22:46:16.724701+00:00',\n", - " 'updated_at': '2024-05-18T22:46:16.724701+00:00',\n", - " 'metadata': {}}" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "thread = await client.threads.create()\n", - "thread" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "147c3f98-f889-4f05-a090-6b31f2a0b291", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "runs = await client.runs.list(thread[\"thread_id\"])\n", - "runs" - ] - }, - { - "cell_type": "markdown", - "id": "77dae6ad-bb7b-468d-b7fd-9b8a35f13ccb", - "metadata": {}, - "source": [ - "We now want to add a human-in-the-loop step before a tool is called.\n", - "We can do this by adding `interrupt_before=[\"action\"]`, which tells us to interrupt before calling the action node.\n", - "We can do this either when compiling the graph or when kicking off a run.\n", - "Here we will do it when kicking of a run." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "7da70e20-1a4e-4df2-b996-1927f474c835", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Receiving new event of type: metadata...\n", - "{'run_id': '3b77ef83-687a-4840-8858-0371f91a92c3'}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: data...\n", - "{'agent': {'messages': [{'content': [{'id': 'toolu_01HwZqM1ptX6E15A5LAmyZTB', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-e5d17791-4d37-4ad2-815f-a0c4cba62585', 'example': False, 'tool_calls': [{'name': 'tavily_search_results_json', 'args': {'query': 'weather in san francisco'}, 'id': 'toolu_01HwZqM1ptX6E15A5LAmyZTB'}], 'invalid_tool_calls': []}]}}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: end...\n", - "None\n", - "\n", - "\n", - "\n" - ] - } - ], - "source": [ - "input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]}\n", - "async for chunk in client.runs.stream(\n", - " thread[\"thread_id\"],\n", - " assistant_id,\n", - " input=input,\n", - " stream_mode=\"updates\",\n", - " interrupt_before=[\"action\"],\n", - "):\n", - " print(f\"Receiving new event of type: {chunk.event}...\")\n", - " print(chunk.data)\n", - " print(\"\\n\\n\")" - ] - }, - { - "cell_type": "markdown", - "id": "a36ac0d6-7843-4fab-909c-0b5b6e725a7f", - "metadata": {}, - "source": [ - "We can now kick off a new run on the same thread with `None` as the input in order to just continue the existing thread." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "bded66c7-b56e-4db5-809f-fa5a31d8a012", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Receiving new event of type: metadata...\n", - "{'run_id': 'a46f733d-cf5b-4ee3-9e07-08612468c8df'}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: data...\n", - "{'action': {'messages': [{'content': '[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'San Francisco\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 37.78, \\'lon\\': -122.42, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1716072201, \\'localtime\\': \\'2024-05-18 15:43\\'}, \\'current\\': {\\'last_updated_epoch\\': 1716071400, \\'last_updated\\': \\'2024-05-18 15:30\\', \\'temp_c\\': 18.9, \\'temp_f\\': 66.0, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Partly cloudy\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/116.png\\', \\'code\\': 1003}, \\'wind_mph\\': 18.6, \\'wind_kph\\': 29.9, \\'wind_degree\\': 280, \\'wind_dir\\': \\'W\\', \\'pressure_mb\\': 1015.0, \\'pressure_in\\': 29.96, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 59, \\'cloud\\': 25, \\'feelslike_c\\': 18.9, \\'feelslike_f\\': 66.0, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 5.0, \\'gust_mph\\': 23.0, \\'gust_kph\\': 37.1}}\"}]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'tavily_search_results_json', 'id': '8be98ff3-6d61-41c5-8384-8db6b7abdbfb', 'tool_call_id': 'toolu_01HwZqM1ptX6E15A5LAmyZTB'}]}}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: data...\n", - "{'agent': {'messages': [{'content': \"The weather in San Francisco is currently partly cloudy with a temperature of around 66°F (18.9°C). There are westerly winds of 18.6 mph (29.9 km/h) with gusts up to 23 mph (37.1 km/h). The humidity is 59% and visibility is good at 9 miles (16 km). UV levels are moderate at 5.0.\\n\\nIn summary, it's a nice partly cloudy spring day in San Francisco with comfortable temperatures and a moderate breeze. The weather conditions seem ideal for being outdoors and enjoying the city.\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-7a8a2ff8-d0d6-4200-b0a5-926f2b6a4798', 'example': False, 'tool_calls': [], 'invalid_tool_calls': []}]}}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: end...\n", - "None\n", - "\n", - "\n", - "\n" - ] - } - ], - "source": [ - "input = None\n", - "async for chunk in client.runs.stream(\n", - " thread[\"thread_id\"],\n", - " assistant_id,\n", - " input=input,\n", - " stream_mode=\"updates\",\n", - " interrupt_before=[\"action\"],\n", - "):\n", - " print(f\"Receiving new event of type: {chunk.event}...\")\n", - " print(chunk.data)\n", - " print(\"\\n\\n\")" - ] - }, - { - "cell_type": "markdown", - "id": "2072ce5a-8771-42f9-b2de-5d3a7a9c817b", - "metadata": {}, - "source": [ - "## Edit a tool call\n", - "\n", - "What if we want to edit the tool call?\n", - "We can also do that.\n", - "Let's kick off another run, with the same `interrupt_before=['action']`" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "b226b687-02da-4eef-9286-46dba92b17ba", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Receiving new event of type: metadata...\n", - "{'run_id': 'c7c8e313-dad9-47d9-bd03-e112c94eff9e'}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: data...\n", - "{'agent': {'messages': [{'content': [{'id': 'toolu_01NGhKmeciaT7TfhBSwUT3mi', 'input': {'query': 'weather in los angeles'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-3d417aa5-e9c1-4b76-90f8-597519c28af9', 'example': False, 'tool_calls': [{'name': 'tavily_search_results_json', 'args': {'query': 'weather in los angeles'}, 'id': 'toolu_01NGhKmeciaT7TfhBSwUT3mi'}], 'invalid_tool_calls': []}]}}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: end...\n", - "None\n", - "\n", - "\n", - "\n" - ] - } - ], - "source": [ - "input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in la?\"}]}\n", - "async for chunk in client.runs.stream(\n", - " thread[\"thread_id\"],\n", - " assistant_id,\n", - " input=input,\n", - " stream_mode=\"updates\",\n", - " interrupt_before=[\"action\"],\n", - "):\n", - " print(f\"Receiving new event of type: {chunk.event}...\")\n", - " print(chunk.data)\n", - " print(\"\\n\\n\")" - ] - }, - { - "cell_type": "markdown", - "id": "ab338423-c18d-446c-9aa3-3ad2f16d742a", - "metadata": {}, - "source": [ - "We can now inspect the state of the thread" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "bd9ca1f4-c3b0-4fa3-8c91-233a9129a142", - "metadata": {}, - "outputs": [], - "source": [ - "thread_state = await client.threads.get_state(thread[\"thread_id\"])" - ] - }, - { - "cell_type": "markdown", - "id": "31e82414-afd2-46c4-a605-ce3eb46df485", - "metadata": {}, - "source": [ - "Let's get the last message of the thread - this is the one we want to update" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "fe832ec1-7ae0-4d11-8408-d4da88d4dced", - "metadata": {}, - "outputs": [], - "source": [ - "last_message = thread_state[\"values\"][\"messages\"][-1]" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "434253fe-7397-45e2-8be8-91d002088a96", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'id': 'toolu_01NGhKmeciaT7TfhBSwUT3mi',\n", - " 'input': {'query': 'weather in los angeles'},\n", - " 'name': 'tavily_search_results_json',\n", - " 'type': 'tool_use'}]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "last_message[\"content\"]" - ] - }, - { - "cell_type": "markdown", - "id": "6d007b31-c8a2-465c-bc78-a5909ca7931c", - "metadata": {}, - "source": [ - "Let's now modify the tool call to say Louisiana" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "55fcb316-450b-4b8c-9ae9-e7ee395acc55", - "metadata": {}, - "outputs": [], - "source": [ - "last_message[\"tool_calls\"] = [\n", - " {\n", - " \"id\": last_message[\"tool_calls\"][0][\"id\"],\n", - " \"name\": \"tavily_search_results_json\",\n", - " # We change the query to say temperature\n", - " \"args\": {\"query\": \"weather in Louisiana\"},\n", - " }\n", - "]\n", - "# last_message['content'] = [{\n", - "# 'id': last_message['content'][0]['id'],\n", - "# 'name': 'tavily_search_results_json',\n", - "# # We change the query to say temperature\n", - "# 'input': {'query': 'weather in Louisiana'},\n", - "# 'type': 'tool_use'\n", - "# }]" - ] - }, - { - "cell_type": "markdown", - "id": "d49be54e-5334-47be-8dfb-78b8a8155e98", - "metadata": {}, - "source": [ - "We can now update the state - we only need to pass in the last updated message because our graph will handle the update." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "0438f997-bad3-48f6-b532-9ac3a95263c2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'configurable': {'thread_id': '54ed0901-6767-46c9-a5f9-b65c1c5fd89c',\n", - " 'thread_ts': '1ef15688-1dbd-68f5-8007-75dc0e110124'}}" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "await client.threads.update_state(\n", - " thread[\"thread_id\"], values={\"messages\": [last_message]}\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "c96668ab-80fa-4ae6-a90b-773a943ba331", - "metadata": {}, - "source": [ - "Let's now check the state of the thread again, and in particular the final message" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "31936711-4af4-4bd1-ac10-9ce52922dd2f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'name': 'tavily_search_results_json',\n", - " 'args': {'query': 'weather in Louisiana'},\n", - " 'id': 'toolu_01NGhKmeciaT7TfhBSwUT3mi'}]" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "thread_state = await client.threads.get_state(thread[\"thread_id\"])\n", - "thread_state[\"values\"][\"messages\"][-1][\"tool_calls\"]" - ] - }, - { - "cell_type": "markdown", - "id": "20aa8ff3-7876-4db2-9333-c5396cd637ac", - "metadata": {}, - "source": [ - "Great! We changed it. If we now resume execution (by kicking off a new run with null inputs on the same thread) it should use that new tool call." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "8e2c4eeb-2888-4979-9877-aa4a53dec5ea", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Receiving new event of type: metadata...\n", - "{'run_id': '1a1ebed1-3581-418a-81be-e834b40c5c82'}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: data...\n", - "{'action': {'messages': [{'content': '[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'Louisiana\\', \\'region\\': \\'Missouri\\', \\'country\\': \\'USA United States of America\\', \\'lat\\': 39.44, \\'lon\\': -91.06, \\'tz_id\\': \\'America/Chicago\\', \\'localtime_epoch\\': 1716072393, \\'localtime\\': \\'2024-05-18 17:46\\'}, \\'current\\': {\\'last_updated_epoch\\': 1716072300, \\'last_updated\\': \\'2024-05-18 17:45\\', \\'temp_c\\': 29.0, \\'temp_f\\': 84.2, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Partly cloudy\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/116.png\\', \\'code\\': 1003}, \\'wind_mph\\': 6.9, \\'wind_kph\\': 11.2, \\'wind_degree\\': 220, \\'wind_dir\\': \\'SW\\', \\'pressure_mb\\': 1011.0, \\'pressure_in\\': 29.86, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 46, \\'cloud\\': 50, \\'feelslike_c\\': 31.4, \\'feelslike_f\\': 88.6, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 7.0, \\'gust_mph\\': 7.4, \\'gust_kph\\': 11.9}}\"}]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'tavily_search_results_json', 'id': '728f8ac9-729e-4bf7-b560-b332a73c8f47', 'tool_call_id': 'toolu_01NGhKmeciaT7TfhBSwUT3mi'}]}}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: data...\n", - "{'agent': {'messages': [{'content': [{'text': 'The search results seem to be for the weather in Louisiana, Missouri rather than Los Angeles, California. Let me try the search again:', 'type': 'text'}, {'id': 'toolu_019YAXWMK33tG9DaxMzrowc8', 'input': {'query': 'weather in los angeles california'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-c42a3b14-2611-4a1d-8907-95dcdb18f07f', 'example': False, 'tool_calls': [{'name': 'tavily_search_results_json', 'args': {'query': 'weather in los angeles california'}, 'id': 'toolu_019YAXWMK33tG9DaxMzrowc8'}], 'invalid_tool_calls': []}]}}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: end...\n", - "None\n", - "\n", - "\n", - "\n" - ] - } - ], - "source": [ - "input = None\n", - "async for chunk in client.runs.stream(\n", - " thread[\"thread_id\"],\n", - " assistant_id,\n", - " input=input,\n", - " stream_mode=\"updates\",\n", - " interrupt_before=[\"action\"],\n", - "):\n", - " print(f\"Receiving new event of type: {chunk.event}...\")\n", - " print(chunk.data)\n", - " print(\"\\n\\n\")" - ] - }, - { - "cell_type": "markdown", - "id": "065f8165-43d8-4876-86af-0cfffd712fee", - "metadata": {}, - "source": [ - "## Edit an old state\n", - "\n", - "Let's now imagine we want to go back in time and edit the tool call after we had already made it.\n", - "In order to do this, we can get first get the full history of the thread." - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "id": "de050efd-73a4-441e-91e0-18e08f773a42", - "metadata": {}, - "outputs": [], - "source": [ - "thread_history = await client.threads.get_history(thread[\"thread_id\"], limit=100)" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "id": "07e15435-4a5f-4c2a-b748-0e0f7ab02a28", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "11" - ] - }, - "execution_count": 47, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "len(thread_history)" - ] - }, - { - "cell_type": "markdown", - "id": "a292e721-36c4-41b8-85e4-378f0770652a", - "metadata": {}, - "source": [ - "After that, we can get the correct state we want to be in. The 0th index state is the most recent one, while the -1 index state is the first.\n", - "In this case, we want to go to the state where the last message had the tool calls for `weather in los angeles`" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "id": "132d207c-11cb-4efb-a330-88ebdfc612c8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'name': 'tavily_search_results_json',\n", - " 'args': {'query': 'weather in los angeles'},\n", - " 'id': 'toolu_01FnuDKhUfagwoqhNfiTYTfS'}]" - ] - }, - "execution_count": 48, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "rewind_state = thread_history[3]\n", - "rewind_state[\"values\"][\"messages\"][-1][\"tool_calls\"]" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "id": "45e01ddf-2ccf-4029-b431-e5fce2235b59", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'configurable': {'thread_id': 'df85453d-cb86-48c8-ae84-12081faa1bdf',\n", - " 'thread_ts': '1ef15582-3442-6db7-8006-9166bbb0e80f'}}" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "rewind_state[\"config\"]" - ] - }, - { - "cell_type": "markdown", - "id": "d229468e-2f94-4b29-b56b-1d402554dcfb", - "metadata": {}, - "source": [ - "If we want to, we can now resume execution from that place in time" - ] - }, - { - "cell_type": "code", - "execution_count": 50, - "id": "94ebc63e-f2cf-4da1-bc8d-52c4731ab0c6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Receiving new event of type: metadata...\n", - "{'run_id': 'a1cc9263-ef0a-4c04-9194-6f01624d0ef0'}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: data...\n", - "{'action': {'messages': [{'content': '[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'Los Angeles\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 34.05, \\'lon\\': -118.24, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1716071728, \\'localtime\\': \\'2024-05-18 15:35\\'}, \\'current\\': {\\'last_updated_epoch\\': 1716071400, \\'last_updated\\': \\'2024-05-18 15:30\\', \\'temp_c\\': 20.0, \\'temp_f\\': 68.0, \\'is_day\\': 1, \\'condition\\': {\\'text\\': \\'Partly cloudy\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/day/116.png\\', \\'code\\': 1003}, \\'wind_mph\\': 2.2, \\'wind_kph\\': 3.6, \\'wind_degree\\': 226, \\'wind_dir\\': \\'SW\\', \\'pressure_mb\\': 1016.0, \\'pressure_in\\': 29.99, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 61, \\'cloud\\': 50, \\'feelslike_c\\': 20.0, \\'feelslike_f\\': 68.0, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 6.0, \\'gust_mph\\': 12.6, \\'gust_kph\\': 20.3}}\"}]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'tavily_search_results_json', 'id': '7137b2e5-566b-418b-b642-b3c6b64c5224', 'tool_call_id': 'toolu_01FnuDKhUfagwoqhNfiTYTfS'}]}}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: data...\n", - "{'agent': {'messages': [{'content': 'The search results show the current weather conditions in Los Angeles. As of 3:30pm on May 18, 2024, the weather in Los Angeles is partly cloudy with a temperature around 68°F (20°C). Winds are light from the southwest around 2-3 mph. The humidity is 61% and visibility is good at 9 miles. Overall, it appears to be a nice spring day in LA with partly sunny skies and comfortable temperatures in the upper 60s.', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-3966b68a-c381-4933-a852-e6a4697c962c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': []}]}}\n", - "\n", - "\n", - "\n", - "Receiving new event of type: end...\n", - "None\n", - "\n", - "\n", - "\n" - ] - } - ], - "source": [ - "input = None\n", - "async for chunk in client.runs.stream(\n", - " thread[\"thread_id\"],\n", - " assistant_id,\n", - " input=input,\n", - " stream_mode=\"updates\",\n", - " interrupt_before=[\"action\"],\n", - " config=rewind_state[\"config\"],\n", - "):\n", - " print(f\"Receiving new event of type: {chunk.event}...\")\n", - " print(chunk.data)\n", - " print(\"\\n\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "492f1d37-0979-4210-8dd7-bc70cdc308f3", - "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.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/cloud_examples/human_in_the_loop_breakpoint.ipynb b/examples/cloud_examples/human_in_the_loop_breakpoint.ipynb new file mode 100644 index 000000000..f9f3e5cf4 --- /dev/null +++ b/examples/cloud_examples/human_in_the_loop_breakpoint.ipynb @@ -0,0 +1,116 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "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", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "### Code for your graph\n", + "\n", + "In this how-to we use a simple ReAct style hosted graph (you can see the full code for defining it [here](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/)). The important thing is that there are two nodes (one named `agent` that calls the LLM, and one named `action` that calls the tool), and a routing function from `agent` that determines whether to call `action` next or just end the graph run (the `action` node always calls the `agent` node after execution).\n", + "\n", + "### SDK Initialization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph_sdk import get_client\n", + "client = get_client()\n", + "assistants = await client.assistants.search()\n", + "assistants = [a for a in assistants if not a['config']]\n", + "assistant = assistants[0]\n", + "assistant_id = assistant['assistant_id']\n", + "thread = await client.threads.create()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding a breakpoint\n", + "\n", + "We now want to add a breakpoint in our graph run, which we will do before a tool is called.\n", + "We can do this by adding `interrupt_before=[\"action\"]`, which tells us to interrupt before calling the action node.\n", + "We can do this either when compiling the graph or when kicking off a run.\n", + "Here we will do it when kicking of a run, if you would like to to do it at compile time you need to edit the python file where your graph is defined and add the `interrupt_before` parameter when you call `.compile`.\n", + "\n", + "First let's access our hosted Langgraph instance through the SDK:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And, now let's compile it with a breakpoint before the tool node:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Receiving new event of type: metadata...\n", + "{'run_id': '3b77ef83-687a-4840-8858-0371f91a92c3'}\n", + "\n", + "\n", + "\n", + "Receiving new event of type: data...\n", + "{'agent': {'messages': [{'content': [{'id': 'toolu_01HwZqM1ptX6E15A5LAmyZTB', 'input': {'query': 'weather in san francisco'}, 'name': 'tavily_search_results_json', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-e5d17791-4d37-4ad2-815f-a0c4cba62585', 'example': False, 'tool_calls': [{'name': 'tavily_search_results_json', 'args': {'query': 'weather in san francisco'}, 'id': 'toolu_01HwZqM1ptX6E15A5LAmyZTB'}], 'invalid_tool_calls': []}]}}\n", + "\n", + "\n", + "\n", + "Receiving new event of type: end...\n", + "None\n", + "\n", + "\n", + "\n" + ] + } + ], + "source": [ + "input = {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf\"}]}\n", + "async for chunk in client.runs.stream(\n", + " thread[\"thread_id\"],\n", + " assistant_id,\n", + " input=input,\n", + " stream_mode=\"updates\",\n", + " interrupt_before=[\"action\"],\n", + "):\n", + " print(f\"Receiving new event of type: {chunk.event}...\")\n", + " print(chunk.data)\n", + " print(\"\\n\\n\")" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/cloud_examples/human_in_the_loop_edit_state.ipynb b/examples/cloud_examples/human_in_the_loop_edit_state.ipynb new file mode 100644 index 000000000..741acb2b8 --- /dev/null +++ b/examples/cloud_examples/human_in_the_loop_edit_state.ipynb @@ -0,0 +1,173 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to Edit State of a Deployed Graph\n", + "\n", + "When creating LangGraph agents, it is often nice to add a human-in-the-loop component. This can be helpful when giving them access to tools. 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. This interrupts execution at that node. You can then use update_state to update the state, and then resume from that spot to continue.\n", + "\n", + "## Setup\n", + "\n", + "We are not going to show the full code for the graph we are hosting, but you can see it [here](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/edit-graph-state/#build-the-agent) if you want to. Once this graph is hosted, we are ready to invoke it and wait for user input. \n", + "\n", + "### SDK initialization\n", + "\n", + "First, we need to setup our client so that we can communicate with our hosted graph:" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph_sdk import get_client\n", + "client = get_client()\n", + "assistants = await client.assistants.search()\n", + "assistants = [a for a in assistants if not a['config']]\n", + "assistant = assistants[0]\n", + "thread = await client.threads.create()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Editing state\n", + "\n", + "### Initial invocation\n", + "\n", + "Now let's invoke our graph, making sure to interrupt before the `action` node." + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'agent': {'messages': [{'content': [{'text': \"Certainly! I'll search for the current weather in San Francisco for you using the search function. Here's how I'll do that:\", 'type': 'text'}, {'id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-6dbb0167-f8f6-4e2a-ab68-229b2d1fbb64', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n" + ] + } + ], + "source": [ + "input = {'messages':[{\"role\":\"user\",\"content\":\"search for weather in SF\"}]}\n", + "\n", + "async for chunk in client.runs.stream(\n", + " thread[\"thread_id\"],\n", + " assistant[\"assistant_id\"], # graph_id\n", + " input=input,\n", + " stream_mode=\"updates\",\n", + " interrupt_before=[\"action\"],\n", + "):\n", + " if chunk.data and \"run_id\" not in chunk.data:\n", + " print(chunk.data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Edit the state\n", + "\n", + "Now, let's assume we actually meant to search for the weather in Sidi Frej (another city with the initials SF). We can edit the state to properly reflect that:" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'configurable': {'thread_id': '88d58d3f-4151-47a9-a8e0-e42fdd3527b8',\n", + " 'thread_ts': '1ef3274b-a809-6913-8002-91536ce6554d'}}" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# First, lets get the current state\n", + "current_state = await client.threads.get_state(thread['thread_id'])\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 Sidi Frej'}\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", + "await client.threads.update_state(thread['thread_id'], {\"messages\": last_message})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Resume invocation\n", + "\n", + "Now we can resume our graph run but with the updated state:" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'action': {'messages': [{'content': '[\"I looked up: current weather in Sidi Frej. Result: It\\'s sunny in San Francisco, but you better look out if you\\'re a Gemini 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '1161b8d1-bee4-4188-9be8-698aecb69f10', 'tool_call_id': 'toolu_01KEJMBFozSiZoS4mAcPZeqQ'}]}}\n", + "{'agent': {'messages': [{'content': [{'text': 'I apologize for the confusion in my search query. It seems the search function interpreted \"SF\" as \"Sidi Frej\" instead of \"San Francisco\" as we intended. Let me search again with the full city name to get the correct information:', 'type': 'text'}, {'id': 'toolu_0111rrwgfAcmurHZn55qjqTR', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-b8c25779-cfb4-46fc-a421-48553551242f', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_0111rrwgfAcmurHZn55qjqTR'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n", + "{'action': {'messages': [{'content': '[\"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 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '6bc632ae-5ee6-4d01-9532-79c524a2d443', 'tool_call_id': 'toolu_0111rrwgfAcmurHZn55qjqTR'}]}}\n", + "{'agent': {'messages': [{'content': \"Now, based on the search results, I can provide you with information about the current weather in San Francisco:\\n\\nThe weather in San Francisco is currently sunny. \\n\\nIt's worth noting that the search result included an unusual comment about Gemini, which doesn't seem directly related to the weather. This might be due to the search engine including some astrological information or a joke in its results. However, for the purpose of weather information, we can focus on the fact that it's sunny in San Francisco right now.\\n\\nIs there anything else you'd like to know about the weather in San Francisco or any other location?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-227a042b-dd97-476e-af32-76a3703af5d8', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n" + ] + } + ], + "source": [ + "async for chunk in client.runs.stream(\n", + " thread[\"thread_id\"],\n", + " assistant[\"assistant_id\"], # graph_id\n", + " input=None,\n", + " stream_mode=\"updates\",\n", + "):\n", + " if chunk.data and \"run_id\" not in chunk.data:\n", + " print(chunk.data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see it now looks up the current weather in Sidi Frej (although our dummy search node still returns results for SF because we don't actually do a search in this example, we just return the same \"It's sunny in San Francisco ...\" result every time)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/cloud_examples/human_in_the_loop_time_travel.ipynb b/examples/cloud_examples/human_in_the_loop_time_travel.ipynb new file mode 100644 index 000000000..77450204c --- /dev/null +++ b/examples/cloud_examples/human_in_the_loop_time_travel.ipynb @@ -0,0 +1,235 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to Replay and Branch from Prior States\n", + "\n", + "With Langgraph Cloud you have the ability to return to any of your prior states and either re-run the graph to reproduce issues noticed during testing, or branch out in a different way from what was originally done in the prior states. In this guide we will show a quick example of how to rerun past states and how to branch off from previous states as well.\n", + "\n", + "## Setup\n", + "\n", + "We are not going to show the full code for the graph we are hosting, but you can see it [here](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/time-travel/#build-the-agent) if you want to. Once this graph is hosted, we are ready to invoke it and wait for user input. \n", + "\n", + "### SDK initialization\n", + "\n", + "First, we need to setup our client so that we can communicate with our hosted graph:" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph_sdk import get_client\n", + "client = get_client()\n", + "assistants = await client.assistants.search()\n", + "assistants = [a for a in assistants if not a['config']]\n", + "assistant = assistants[0]\n", + "thread = await client.threads.create()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Replay a state\n", + "\n", + "### Initial invocation\n", + "\n", + "Before replaying a state - we need to create states to replay from! In order to do this, let's invoke our graph with a simple message:" + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'agent': {'messages': [{'content': [{'text': \"Certainly! I'll use the search function to look up the current weather in San Francisco for you. Let me do that now.\", 'type': 'text'}, {'id': 'toolu_011vroKUtWU7SBdrngpgpFMn', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-ee639877-d97d-40f8-96dc-d0d1ae22d203', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n", + "{'action': {'messages': [{'content': '[\"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 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '7bad0e72-5ebe-4b08-9b8a-b99b0fe22fb7', 'tool_call_id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}]}}\n", + "{'agent': {'messages': [{'content': \"Based on the search results, I can provide you with information about the current weather in San Francisco:\\n\\nThe weather in San Francisco is currently sunny. This is great news for outdoor activities and enjoying the city's beautiful sights.\\n\\nIt's worth noting that the search result included an unusual comment about Geminis, which isn't typically part of a weather report. This might be due to the search engine including some astrological information or a joke in its results. However, for the purpose of answering your question about the weather, we can focus on the fact that it's sunny in San Francisco.\\n\\nIf you need any more specific information about the weather in San Francisco, 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 for you.\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-dbac539a-33c8-4f0c-9e20-91f318371e7c', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n" + ] + } + ], + "source": [ + "input = {'messages':[{\"role\":\"user\",\"content\":\"Please search the weather in SF\"}]}\n", + "\n", + "async for chunk in client.runs.stream(\n", + " thread[\"thread_id\"],\n", + " assistant[\"assistant_id\"], # graph_id\n", + " input=input,\n", + " stream_mode=\"updates\",\n", + "):\n", + " if chunk.data and \"run_id\" not in chunk.data:\n", + " print(chunk.data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's get our list of states, and invoke from the third state (right before the tool get called):" + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "metadata": {}, + "outputs": [], + "source": [ + "states = await client.threads.get_history(thread['thread_id'])" + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['action']" + ] + }, + "execution_count": 102, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# We can confirm that this state is correct by checking the 'next' attribute and seeing that it is the tool call node\n", + "state_to_replay = states[2]\n", + "state_to_replay['next']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To rerun from a state, we need to pass in the `checkpoint_id` into the config of the run like follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 103, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'action': {'messages': [{'content': '[\"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 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': 'eba650e5-400e-4938-8508-f878dcbcc532', 'tool_call_id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}]}}\n", + "{'agent': {'messages': [{'content': \"Based on the search results, I can provide you with information about the current weather in San Francisco:\\n\\nThe weather in San Francisco is currently sunny. This is great news if you're planning any outdoor activities or simply want to enjoy a pleasant day in the city.\\n\\nIt's worth noting that the search result included an unusual comment about Geminis, which doesn't seem directly related to the weather. This appears to be a playful or humorous addition to the weather report, possibly from the source where this information was obtained.\\n\\nIs there anything else you'd like to know about the weather in San Francisco or any other information you need?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-bc6dca3f-a1e2-4f59-a69b-fe0515a348bb', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n" + ] + } + ], + "source": [ + "async for chunk in client.runs.stream(\n", + " thread[\"thread_id\"],\n", + " assistant[\"assistant_id\"], # graph_id\n", + " input=None,\n", + " stream_mode=\"updates\",\n", + " config={\"configurable\":{\"thread_ts\":state_to_replay['checkpoint_id']}}\n", + "):\n", + " if chunk.data and \"run_id\" not in chunk.data:\n", + " print(chunk.data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, the graph restarted from the tool node with the same input as our original graph run.\n", + "\n", + "## Branch off from previous 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": 104, + "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 = state_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", + "new_state = await client.threads.update_state(thread['thread_id'],{\"messages\":[last_message]},checkpoint_id=state_to_replay['checkpoint_id'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can rerun our graph with this new config, starting from the `new_state`, which is a branch of our `state_to_replay`:" + ] + }, + { + "cell_type": "code", + "execution_count": 105, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'action': {'messages': [{'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 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '2baf9941-4fda-4081-9f87-d76795d289f1', 'tool_call_id': 'toolu_011vroKUtWU7SBdrngpgpFMn'}]}}\n", + "{'agent': {'messages': [{'content': \"Based on the search results, I can provide you with information about the current weather in San Francisco (SF):\\n\\nThe weather in San Francisco is currently sunny. This means it's a clear day with plenty of sunshine. \\n\\nIt's worth noting that the specific temperature wasn't provided in the search result, but sunny weather in San Francisco typically means comfortable temperatures. San Francisco is known for its mild climate, so even on sunny days, it's often not too hot.\\n\\nThe search result also included a playful reference to astrological signs, mentioning Gemini. However, this is likely just a joke or part of the search engine's presentation and not related to the actual weather conditions.\\n\\nIs there any specific information about the weather in San Francisco you'd like to know more about? I'd be happy to perform another search if you need details on temperature, wind conditions, or the forecast for the coming days.\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-a83de52d-ed18-4402-9384-75c462485743', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n" + ] + } + ], + "source": [ + "async for chunk in client.runs.stream(\n", + " thread[\"thread_id\"],\n", + " assistant[\"assistant_id\"], # graph_id\n", + " input=None,\n", + " stream_mode=\"updates\",\n", + " config={\"configurable\":{\"thread_ts\":new_state['configurable']['thread_ts']}}\n", + "):\n", + " if chunk.data and \"run_id\" not in chunk.data:\n", + " print(chunk.data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, the search query changed from San Francisco to SF, just as we had hoped!" + ] + } + ], + "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.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/cloud_examples/human_in_the_loop_user_input.ipynb b/examples/cloud_examples/human_in_the_loop_user_input.ipynb new file mode 100644 index 000000000..f11d13e4d --- /dev/null +++ b/examples/cloud_examples/human_in_the_loop_user_input.ipynb @@ -0,0 +1,175 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "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." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "We are not going to show the full code for the graph we are hosting, but you can see it [here](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/wait-user-input/#build-the-agent) if you want to. Once this graph is hosted, we are ready to invoke it and wait for user input. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### SDK initialization\n", + "\n", + "First, we need to setup our client so that we can communicate with our hosted graph:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph_sdk import get_client\n", + "client = get_client()\n", + "assistants = await client.assistants.search()\n", + "assistants = [a for a in assistants if not a['config']]\n", + "assistant = assistants[0]\n", + "thread = await client.threads.create()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Waiting for user input\n", + "\n", + "### Initial invocation\n", + "\n", + "Now, let's invoke our graph by interrupting before `ask_human` node:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'agent': {'messages': [{'content': [{'text': \"Certainly! I'll use the AskHuman function to ask the user about their location, and then I'll use the search function to look up the weather for that location. Let's start by asking the user where they are.\", 'type': 'text'}, {'id': 'toolu_01RFahzYPvnPWTb2USk2RdKR', 'input': {'question': 'Where are you currently located?'}, 'name': 'AskHuman', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-a8422215-71d3-4093-afb4-9db141c94ddb', 'example': False, 'tool_calls': [{'name': 'AskHuman', 'args': {'question': 'Where are you currently located?'}, 'id': 'toolu_01RFahzYPvnPWTb2USk2RdKR'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n" + ] + } + ], + "source": [ + "input = {'messages':[{\"role\":\"user\",\"content\":\"Use the search tool to ask the user where they are, then look up the weather there\"}]}\n", + "\n", + "async for chunk in client.runs.stream(\n", + " thread[\"thread_id\"],\n", + " assistant[\"assistant_id\"], # graph_id\n", + " input=input,\n", + " stream_mode=\"updates\",\n", + " interrupt_before=[\"ask_human\"],\n", + "):\n", + " if chunk.data and \"run_id\" not in chunk.data:\n", + " print(chunk.data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Adding user input to state\n", + "\n", + "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": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'configurable': {'thread_id': '10d0ee61-db47-48fc-a58c-109a1e68cd73',\n", + " 'thread_ts': '1ef32729-3cc3-6647-8002-14dcb621b46e'}}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "state = await client.threads.get_state(thread['thread_id'])\n", + "tool_call_id = state['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", + "await client.threads.update_state(thread['thread_id'], {\"messages\": tool_message}, as_node=\"ask_human\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Invoking after receiving human input\n", + "\n", + "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": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'agent': {'messages': [{'content': [{'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_01K57ofmgG2wyJ8tYJjbq5k7', 'input': {'query': 'current weather in San Francisco'}, 'name': 'search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-241baed7-db5e-44ce-ac3c-56431705c22b', 'example': False, 'tool_calls': [{'name': 'search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7'}], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n", + "{'action': {'messages': [{'content': '[\"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 😈.\"]', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'tool', 'name': 'search', 'id': '8b699b95-8546-4557-8e66-14ea71a15ed8', 'tool_call_id': 'toolu_01K57ofmgG2wyJ8tYJjbq5k7'}]}}\n", + "{'agent': {'messages': [{'content': \"Based on the search results, I can provide you with information about the current weather in San Francisco:\\n\\nThe weather in San Francisco is currently sunny. It's a beautiful day in the city! \\n\\nHowever, I should note that the search result included an unusual comment about Gemini zodiac signs. This appears to be either a joke or potentially irrelevant information added by the search engine. For accurate and detailed weather information, you might want to check a reliable weather service or app for San Francisco.\\n\\nIs there anything else you'd like to know about the weather or San Francisco?\", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-b4d7309f-f849-46aa-b6ef-475bcabd2be9', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}}\n" + ] + } + ], + "source": [ + "async for chunk in client.runs.stream(\n", + " thread[\"thread_id\"],\n", + " assistant[\"assistant_id\"], # graph_id\n", + " input=None,\n", + " stream_mode=\"updates\",\n", + "):\n", + " if chunk.data and \"run_id\" not in chunk.data:\n", + " print(chunk.data)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}