From 0aefe68a5f424f952a872aec097a0165eb8d896f Mon Sep 17 00:00:00 2001 From: Lauren Hirata Singh Date: Wed, 25 Jun 2025 14:16:46 -0400 Subject: [PATCH] docs: HITL consolidation (#5192) * HITL consolidation, minus server * Fix links * Fix server page * remove extra page * nits * updates based on feedback * Update docs/docs/concepts/human_in_the_loop.md Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com> * edits based on feedback --------- Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com> --- docs/_scripts/notebook_hooks.py | 7 +- docs/docs/agents/agents.md | 2 +- docs/docs/agents/human-in-the-loop.md | 238 ------ docs/docs/agents/overview.md | 2 +- docs/docs/agents/ui.md | 2 +- .../cloud/how-tos/add-human-in-the-loop.md | 41 +- .../human_in_the_loop_review_tool_calls.md | 549 ------------- docs/docs/concepts/human_in_the_loop.md | 22 +- .../add-human-in-the-loop.md | 386 +++++---- .../human_in_the_loop/review-tool-calls.ipynb | 773 ------------------ docs/docs/how-tos/memory/add-memory.md | 2 +- .../review-tool-calls-functional.ipynb | 627 -------------- docs/docs/how-tos/use-functional-api.md | 157 +++- .../how-tos/wait-user-input-functional.ipynb | 561 ------------- .../get-started/4-human-in-the-loop.md | 2 +- docs/mkdocs.yml | 3 +- 16 files changed, 430 insertions(+), 2944 deletions(-) delete mode 100644 docs/docs/agents/human-in-the-loop.md delete mode 100644 docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md delete mode 100644 docs/docs/how-tos/human_in_the_loop/review-tool-calls.ipynb delete mode 100644 docs/docs/how-tos/review-tool-calls-functional.ipynb delete mode 100644 docs/docs/how-tos/wait-user-input-functional.ipynb diff --git a/docs/_scripts/notebook_hooks.py b/docs/_scripts/notebook_hooks.py index 463680cc6..c233b2415 100644 --- a/docs/_scripts/notebook_hooks.py +++ b/docs/_scripts/notebook_hooks.py @@ -64,6 +64,7 @@ REDIRECT_MAP = { "how-tos/subgraph-persistence.ipynb": "how-tos/memory/add-memory.md#use-with-subgraphs", "how-tos/cross-thread-persistence.ipynb": "how-tos/memory/add-memory.md#add-long-term-memory", "cloud/how-tos/copy_threads": "cloud/how-tos/use_threads", + "cloud/how-tos/check-thread-status": "cloud/how-tos/use_threads", "cloud/concepts/threads.md": "concepts/persistence.md#threads", "how-tos/persistence.ipynb": "how-tos/memory/add-memory.md", # tool calling how-tos @@ -97,7 +98,6 @@ REDIRECT_MAP = { "how-tos/create-react-agent.ipynb": "agents/agents.md#basic-configuration", "how-tos/create-react-agent-memory.ipynb": "agents/memory.md", "how-tos/create-react-agent-system-prompt.ipynb": "agents/context.md#prompts", - "how-tos/create-react-agent-hitl.ipynb": "agents/human-in-the-loop.md", "how-tos/create-react-agent-structured-output.ipynb": "agents/agents.md#structured-output", # Time-travel "how-tos/human_in_the_loop/edit-graph-state.ipynb": "how-tos/human_in_the_loop/time-travel.ipynb", @@ -118,6 +118,11 @@ REDIRECT_MAP = { # assistant redirects "cloud/how-tos/assistant_versioning.md": "cloud/how-tos/configuration_cloud.md", "cloud/concepts/runs.md": "concepts/assistants.md#execution", + # hitl redirects + "how-tos/wait-user-input-functional.ipynb": "how-tos/use-functional-api.md", + "how-tos/review-tool-calls-functional.ipynb": "how-tos/use-functional-api.md", + "how-tos/create-react-agent-hitl.ipynb": "how-tos/human_in_the_loop/add-human-in-the-loop.md", + "agents/human-in-the-loop.md": "how-tos/human_in_the_loop/add-human-in-the-loop.md", } diff --git a/docs/docs/agents/agents.md b/docs/docs/agents/agents.md index c3383fcd6..b00184266 100644 --- a/docs/docs/agents/agents.md +++ b/docs/docs/agents/agents.md @@ -180,7 +180,7 @@ ny_response = agent.invoke( ) ``` -1. `checkpointer` allows the agent to store its state at every step in the tool calling loop. This enables [short-term memory](../how-tos/memory/add-memory.md#add-short-term-memory) and [human-in-the-loop](./human-in-the-loop.md) capabilities. +1. `checkpointer` allows the agent to store its state at every step in the tool calling loop. This enables [short-term memory](../how-tos/memory/add-memory.md#add-short-term-memory) and [human-in-the-loop](../concepts/human_in_the_loop.md) capabilities. 2. Pass configuration with `thread_id` to be able to resume the same conversation on future agent invocations. When you enable the checkpointer, it stores agent state at every step in the provided checkpointer database (or in memory, if using `InMemorySaver`). diff --git a/docs/docs/agents/human-in-the-loop.md b/docs/docs/agents/human-in-the-loop.md deleted file mode 100644 index 44b9f6f88..000000000 --- a/docs/docs/agents/human-in-the-loop.md +++ /dev/null @@ -1,238 +0,0 @@ ---- -search: - boost: 2 -tags: - - human-in-the-loop - - hil - - agent -hide: - - tags ---- - -# Human-in-the-loop - -To review, edit and approve tool calls in an agent you can use LangGraph's built-in [Human-In-the-Loop (HIL)](../concepts/human_in_the_loop.md) features, specifically the [`interrupt()`][langgraph.types.interrupt] primitive. - -LangGraph allows you to pause execution **indefinitely** — for minutes, hours, or even days—until human input is received. - -This is possible because the agent state is **checkpointed into a database**, which allows the system to persist execution context and later resume the workflow, continuing from where it left off. - -For a deeper dive into the **human-in-the-loop** concept, see the [concept guide](../concepts/human_in_the_loop.md). - -
-![image](../concepts/img/human_in_the_loop/tool-call-review.png){: style="max-height:400px"} -
-A human can review and edit the output from the agent before proceeding. This is particularly critical in applications where the tool calls requested may be sensitive or require human oversight. -
-
- - -## Review tool calls - -To add a human approval step to a tool: - -1. Use `interrupt()` in the tool to pause execution. -2. Resume with a `Command(resume=...)` to continue based on human input. - -```python -from langgraph.checkpoint.memory import InMemorySaver -from langgraph.types import interrupt -from langgraph.prebuilt import create_react_agent - -# An example of a sensitive tool that requires human review / approval -def book_hotel(hotel_name: str): - """Book a hotel""" - # highlight-next-line - response = interrupt( # (1)! - f"Trying to call `book_hotel` with args {{'hotel_name': {hotel_name}}}. " - "Please approve or suggest edits." - ) - if response["type"] == "accept": - pass - elif response["type"] == "edit": - hotel_name = response["args"]["hotel_name"] - else: - raise ValueError(f"Unknown response type: {response['type']}") - return f"Successfully booked a stay at {hotel_name}." - -# highlight-next-line -checkpointer = InMemorySaver() # (2)! - -agent = create_react_agent( - model="anthropic:claude-3-5-sonnet-latest", - tools=[book_hotel], - # highlight-next-line - checkpointer=checkpointer, # (3)! -) -``` - -1. The [`interrupt` function][langgraph.types.interrupt] pauses the agent graph at a specific node. In this case, we call `interrupt()` at the beginning of the tool function, which pauses the graph at the node that executes the tool. The information inside `interrupt()` (e.g., tool calls) can be presented to a human, and the graph can be resumed with the user input (tool call approval, edit or feedback). -2. The `InMemorySaver` is used to store the agent state at every step in the tool calling loop. This enables [short-term memory](../how-tos/memory/add-memory.md#add-short-term-memory) and [human-in-the-loop](./human-in-the-loop.md) capabilities. In this example, we use `InMemorySaver` to store the agent state in memory. In a production application, the agent state will be stored in a database. -3. Initialize the agent with the `checkpointer`. - -Run the agent with the `stream()` method, passing the `config` object to specify the thread ID. This allows the agent to resume the same conversation on future invocations. - -```python -config = { - "configurable": { - # highlight-next-line - "thread_id": "1" - } -} - -for chunk in agent.stream( - {"messages": [{"role": "user", "content": "book a stay at McKittrick hotel"}]}, - # highlight-next-line - config -): - print(chunk) - print("\n") -``` - -> You should see that the agent runs until it reaches the `interrupt()` call, at which point it pauses and waits for human input. - -Resume the agent with a `Command(resume=...)` to continue based on human input. - -```python -from langgraph.types import Command - -for chunk in agent.stream( - # highlight-next-line - Command(resume={"type": "accept"}), # (1)! - # Command(resume={"type": "edit", "args": {"hotel_name": "McKittrick Hotel"}}), - config -): - print(chunk) - print("\n") -``` - -1. The [`interrupt` function][langgraph.types.interrupt] is used in conjunction with the [`Command`][langgraph.types.Command] object to resume the graph with a value provided by the human. - -## Using with Agent Inbox - -You can create a wrapper to add interrupts to *any* tool. - -The example below provides a reference implementation compatible with [Agent Inbox UI](https://github.com/langchain-ai/agent-inbox) and [Agent Chat UI](https://github.com/langchain-ai/agent-chat-ui). - -```python title="Wrapper that adds human-in-the-loop to any tool" -from typing import Callable -from langchain_core.tools import BaseTool, tool as create_tool -from langchain_core.runnables import RunnableConfig -from langgraph.types import interrupt -from langgraph.prebuilt.interrupt import HumanInterruptConfig, HumanInterrupt - -def add_human_in_the_loop( - tool: Callable | BaseTool, - *, - interrupt_config: HumanInterruptConfig = None, -) -> BaseTool: - """Wrap a tool to support human-in-the-loop review.""" - if not isinstance(tool, BaseTool): - tool = create_tool(tool) - - if interrupt_config is None: - interrupt_config = { - "allow_accept": True, - "allow_edit": True, - "allow_respond": True, - } - - @create_tool( # (1)! - tool.name, - description=tool.description, - args_schema=tool.args_schema - ) - def call_tool_with_interrupt(config: RunnableConfig, **tool_input): - request: HumanInterrupt = { - "action_request": { - "action": tool.name, - "args": tool_input - }, - "config": interrupt_config, - "description": "Please review the tool call" - } - # highlight-next-line - response = interrupt([request])[0] # (2)! - # approve the tool call - if response["type"] == "accept": - tool_response = tool.invoke(tool_input, config) - # update tool call args - elif response["type"] == "edit": - tool_input = response["args"]["args"] - tool_response = tool.invoke(tool_input, config) - # respond to the LLM with user feedback - elif response["type"] == "response": - user_feedback = response["args"] - tool_response = user_feedback - else: - raise ValueError(f"Unsupported interrupt response type: {response['type']}") - - return tool_response - - return call_tool_with_interrupt -``` - -1. This wrapper creates a new tool that calls `interrupt()` **before** executing the wrapped tool. -2. `interrupt()` is using special input and output format that's expected by [Agent Inbox UI](https://github.com/langchain-ai/agent-inbox): - - a list of [`HumanInterrupt`][langgraph.prebuilt.interrupt.HumanInterrupt] objects is sent to `AgentInbox` render interrupt information to the end user - - resume value is provided by `AgentInbox` as a list (i.e., `Command(resume=[...])`) - -You can use the `add_human_in_the_loop` wrapper to add `interrupt()` to any tool without having to add it *inside* the tool: - -```python -from langgraph.checkpoint.memory import InMemorySaver -from langgraph.prebuilt import create_react_agent - -# highlight-next-line -checkpointer = InMemorySaver() - -def book_hotel(hotel_name: str): - """Book a hotel""" - return f"Successfully booked a stay at {hotel_name}." - - -agent = create_react_agent( - model="anthropic:claude-3-5-sonnet-latest", - tools=[ - # highlight-next-line - add_human_in_the_loop(book_hotel), # (1)! - ], - # highlight-next-line - checkpointer=checkpointer, -) - -config = {"configurable": {"thread_id": "1"}} - -# Run the agent -for chunk in agent.stream( - {"messages": [{"role": "user", "content": "book a stay at McKittrick hotel"}]}, - # highlight-next-line - config -): - print(chunk) - print("\n") -``` - -1. The `add_human_in_the_loop` wrapper is used to add `interrupt()` to the tool. This allows the agent to pause execution and wait for human input before proceeding with the tool call. - -> You should see that the agent runs until it reaches the `interrupt()` call, -> at which point it pauses and waits for human input. - -Resume the agent with a `Command(resume=...)` to continue based on human input. - -```python -from langgraph.types import Command - -for chunk in agent.stream( - # highlight-next-line - Command(resume=[{"type": "accept"}]), - # Command(resume=[{"type": "edit", "args": {"args": {"hotel_name": "McKittrick Hotel"}}}]), - config -): - print(chunk) - print("\n") -``` - -## Additional resources - -* [Human-in-the-loop in LangGraph](../concepts/human_in_the_loop.md) diff --git a/docs/docs/agents/overview.md b/docs/docs/agents/overview.md index 01268e604..84d496a82 100644 --- a/docs/docs/agents/overview.md +++ b/docs/docs/agents/overview.md @@ -28,7 +28,7 @@ The LLM operates in a loop. In each iteration, it selects a tool to invoke, prov LangGraph includes several capabilities essential for building robust, production-ready agentic systems: - [**Memory integration**](../how-tos/memory/add-memory.md): Native support for *short-term* (session-based) and *long-term* (persistent across sessions) memory, enabling stateful behaviors in chatbots and assistants. -- [**Human-in-the-loop control**](./human-in-the-loop.md): Execution can pause *indefinitely* to await human feedback—unlike websocket-based solutions limited to real-time interaction. This enables asynchronous approval, correction, or intervention at any point in the workflow. +- [**Human-in-the-loop control**](../concepts/human_in_the_loop.md): Execution can pause *indefinitely* to await human feedback—unlike websocket-based solutions limited to real-time interaction. This enables asynchronous approval, correction, or intervention at any point in the workflow. - [**Streaming support**](../how-tos/streaming.md): Real-time streaming of agent state, model tokens, tool outputs, or combined streams. - [**Deployment tooling**](./deployment.md): Includes infrastructure-free deployment tools. [**LangGraph Platform**](https://langchain-ai.github.io/langgraph/concepts/langgraph_platform/) supports testing, debugging, and deployment. - **[Studio](https://langchain-ai.github.io/langgraph/concepts/langgraph_studio/)**: A visual IDE for inspecting and debugging workflows. diff --git a/docs/docs/agents/ui.md b/docs/docs/agents/ui.md index 29a8e7cc4..64b321f60 100644 --- a/docs/docs/agents/ui.md +++ b/docs/docs/agents/ui.md @@ -25,7 +25,7 @@ Then, navigate to [Agent Chat UI](https://agentchat.vercel.app), or clone the re ## Add human-in-the-loop -Agent Chat UI has full support for [human-in-the-loop](../concepts/human_in_the_loop.md) workflows. To try it out, replace the agent code in `src/agent/graph.py` (from the [deployment](./deployment.md) guide) with this [agent implementation](./human-in-the-loop.md#using-with-agent-inbox): +Agent Chat UI has full support for [human-in-the-loop](../concepts/human_in_the_loop.md) workflows. To try it out, replace the agent code in `src/agent/graph.py` (from the [deployment](./deployment.md) guide) with this [agent implementation](../how-tos/human_in_the_loop/add-human-in-the-loop.md#add-interrupts-to-any-tool): diff --git a/docs/docs/cloud/how-tos/add-human-in-the-loop.md b/docs/docs/cloud/how-tos/add-human-in-the-loop.md index 866c53eae..2d6525a71 100644 --- a/docs/docs/cloud/how-tos/add-human-in-the-loop.md +++ b/docs/docs/cloud/how-tos/add-human-in-the-loop.md @@ -1,38 +1,8 @@ -# Human-in-the-loop +# Human-in-the-loop in LangGraph Server -LangGraph supports robust **human-in-the-loop (HIL)** workflows, enabling human intervention at any point in an automated process. This is especially useful in large language model (LLM)-driven applications where model output may require validation, correction, or additional context. +To review, edit, and approve tool calls in an agent or workflow, use LangGraph's [human-in-the-loop](../../concepts/human_in_the_loop.md) features. -Please see [the overview of LangGraph human-in-the-loop](../../concepts/human_in_the_loop.md) features for more information. - -## `interrupt` - -The [`interrupt` function][langgraph.types.interrupt] in LangGraph enables human-in-the-loop workflows by pausing the graph at a specific node, presenting information to a human, and resuming the graph with their input. It's useful for tasks like approvals, edits, or gathering additional context. - -The graph is resumed using a [`Command`][langgraph.types.Command] object that provides the human's response. - -**Graph node with `interrupt`:** - -```python -# highlight-next-line -from langgraph.types import interrupt, Command - -def human_node(state: State): - # highlight-next-line - value = interrupt( # (1)! - { - "text_to_revise": state["some_text"] # (2)! - } - ) - return { - "some_text": value # (3)! - } -``` - -1. `interrupt(...)` pauses execution at `human_node`, surfacing the given payload to a human. -2. Any JSON serializable value can be passed to the `interrupt` function. Here, a dict containing the text to revise. -3. Once resumed, the return value of `interrupt(...)` is the human-provided input, which is used to update the state. - -**LangGraph API invoke & resume:** +## LangGraph API invoke & resume === "Python" @@ -337,6 +307,5 @@ def human_node(state: State): ## Learn more -- [**LangGraph human-in-the-loop overview**](../../concepts/human_in_the_loop.md): learn more about LangGraph human-in-the-loop features. -- [**Design patterns**](../../how-tos/human_in_the_loop/add-human-in-the-loop.md#design-patterns): learn how to implement patterns like approving/rejecting actions, requesting user input, and more. -- [**How to review tool calls**](./human_in_the_loop_review_tool_calls.md): detailed examples of how to review and approve/edit tool calls or provide feedback to the tool-calling LLM. \ No newline at end of file +- [Human-in-the-loop conceptual guide](../../concepts/human_in_the_loop.md): learn more about LangGraph human-in-the-loop features. +- [Common patterns](../../how-tos/human_in_the_loop/add-human-in-the-loop.md#common-patterns): learn how to implement patterns like approving/rejecting actions, requesting user input, tool call review, and validating human input. \ No newline at end of file diff --git a/docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md b/docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md deleted file mode 100644 index d1bf81312..000000000 --- a/docs/docs/cloud/how-tos/human_in_the_loop_review_tool_calls.md +++ /dev/null @@ -1,549 +0,0 @@ -# How to review tool calls - -!!! tip "Prerequisites" - - This guide assumes familiarity with the following concepts: - - * [Tool calling](https://python.langchain.com/docs/concepts/tool_calling/) - * [Human-in-the-loop](../../concepts/human_in_the_loop.md) - * [LangGraph Glossary](../../concepts/low_level.md) - -Human-in-the-loop (HIL) interactions are crucial for [agentic systems](../../concepts/agentic_concepts.md). 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: - -- A tool call to execute SQL, which will then be run by the tool -- A tool call to generate a summary, which will then be saved to the State of the graph - -Note that using tool calls is common **whether actually calling tools or not**. - -There are typically a few different interactions you may want to do here: - -1. Approve the tool call and continue -2. Modify the tool call manually and then continue -3. Give natural language feedback, and then pass that back to the agent - - -We can implement these in LangGraph using the [`interrupt()`][langgraph.types.interrupt] function. `interrupt` allows us to stop graph execution to collect input from a user and continue execution with collected input: - - -```python -def human_review_node(state) -> Command[Literal["call_llm", "run_tool"]]: - # this is the value we'll be providing via Command(resume=) - human_review = interrupt( - { - "question": "Is this correct?", - # Surface tool calls for review - "tool_call": tool_call - } - ) - - review_action, review_data = human_review - - # Approve the tool call and continue - if review_action == "continue": - return Command(goto="run_tool") - - # Modify the tool call manually and then continue - elif review_action == "update": - ... - updated_msg = get_updated_msg(review_data) - return Command(goto="run_tool", update={"messages": [updated_message]}) - - # Give natural language feedback, and then pass that back to the agent - elif review_action == "feedback": - ... - feedback_msg = get_feedback_msg(review_data) - return Command(goto="call_llm", update={"messages": [feedback_msg]}) - -``` - -## Setup - -We are not going to show the full code for the graph we are hosting, but you can see it [here](../../how-tos/human_in_the_loop/review-tool-calls.ipynb). Once this graph is hosted, we are ready to invoke it and wait for user input. - -### SDK initialization - -First, we need to setup our client so that we can communicate with our hosted graph: - - -=== "Python" - - ```python - from langgraph_sdk import get_client - client = get_client(url=) - # Using the graph deployed with the name "agent" - assistant_id = "agent" - thread = await client.threads.create() - ``` - -=== "Javascript" - - ```js - import { Client } from "@langchain/langgraph-sdk"; - - const client = new Client({ apiUrl: }); - // Using the graph deployed with the name "agent" - const assistantId = "agent"; - const thread = await client.threads.create(); - ``` - -=== "cURL" - - ```bash - curl --request POST \ - --url /threads \ - --header 'Content-Type: application/json' \ - --data '{}' - ``` - -## Example of approving tool - -First, let's run the agent with an input that requires tool calls with approval: - -=== "Python" - - ```python - input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]} - - async for chunk in client.runs.stream( - thread["thread_id"], - assistant_id, - input=input, - stream_mode="updates", - ): - if chunk.data and chunk.event != "metadata": - print(chunk.data) - ``` - -=== "Javascript" - - ```js - const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] }; - - const streamResponse = client.runs.stream( - thread["thread_id"], - assistantId, - { - input: input, - streamMode: "updates" - } - ); - - for await (const chunk of streamResponse) { - if (chunk.data && chunk.event !== "metadata") { - console.log(chunk.data); - } - } - ``` - -=== "cURL" - - ```bash - curl --request POST \ - --url /threads//runs/stream \ - --header 'Content-Type: application/json' \ - --data "{ - \"assistant_id\": \"agent\", - \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]}, - \"stream_mode\": [ - \"updates\" - ] - }" - ``` - -Output: - - {'call_llm': {'messages': [{'content': [{'text': "I'll help you check the weather in San Francisco.", 'type': 'text'}, {'id': 'toolu_01142G3woscA8JjFTLdqymtn', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01Tdfufy4nZYXMbVZvgyNbhc', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 379, 'output_tokens': 66}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-a33434b2-f5ca-40c6-98e2-6288d349d4ce-0', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01142G3woscA8JjFTLdqymtn', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 379, 'output_tokens': 66, 'total_tokens': 445, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}} - {'__interrupt__': [{'value': {'question': 'Is this correct?', 'tool_call': {'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01142G3woscA8JjFTLdqymtn', 'type': 'tool_call'}}, 'resumable': True, 'ns': ['human_review_node:9caf42cf-1371-7213-a331-e6fe5d026be8'], 'when': 'during'}]} - -To approve the tool call, 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: - -=== "Python" - - ```python - # highlight-next-line - from langgraph_sdk.schema import Command - - async for chunk in client.runs.stream( - thread["thread_id"], - assistant_id, - # highlight-next-line - command=Command(resume={"action": "continue"}), - stream_mode="updates", - ): - if chunk.data and chunk.event != "metadata": - print(chunk.data) - ``` - -=== "Javascript" - - ```js - const streamResponse = client.runs.stream( - thread["thread_id"], - assistantId, - { - // highlight-next-line - command: { resume: { "action": "continue" } }, - streamMode: "updates" - } - ); - - for await (const chunk of streamResponse) { - if (chunk.data && chunk.event !== "metadata") { - console.log(chunk.data); - } - } - ``` - -=== "cURL" - - ```bash - curl --request POST \ - --url /threads//runs/stream \ - --header 'Content-Type: application/json' \ - --data "{ - \"assistant_id\": \"agent\", - \"command\": { - \"resume\": { \"action\": \"continue\"} - }, - \"stream_mode\": [ - \"updates\" - ] - }" - ``` - -Output: - - {'human_review_node': None} - {'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_01142G3woscA8JjFTLdqymtn'}]}} - {'call_llm': {'messages': [{'content': "According to the search, it's sunny in San Francisco right now!", 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01JJE9AtT4a9Lob91RRiW9rU', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 458, 'output_tokens': 18}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-5e8d80b5-c46a-4aad-af37-b01f8bb15963-0', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 458, 'output_tokens': 18, 'total_tokens': 476, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}} - -## Edit Tool Call - -Let's now say we want to edit the tool call. E.g. change some of the parameters (or even the tool called!) but then execute that tool. - -=== "Python" - - ```python - input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]} - - async for chunk in client.runs.stream( - thread["thread_id"], - assistant_id, - input=input, - stream_mode="updates", - ): - if chunk.data and chunk.event != "metadata": - print(chunk.data) - ``` - -=== "Javascript" - - ```js - const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] }; - - const streamResponse = client.runs.stream( - thread["thread_id"], - assistantId, - { - input: input, - streamMode: "updates", - } - ); - - for await (const chunk of streamResponse) { - if (chunk.data && chunk.event !== "metadata") { - console.log(chunk.data); - } - } - ``` - -=== "cURL" - - ```bash - curl --request POST \ - --url /threads//runs/stream \ - --header 'Content-Type: application/json' \ - --data "{ - \"assistant_id\": \"agent\", - \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]}, - \"stream_mode\": [ - \"updates\" - ] - }" - ``` - -To do this, we will use `Command` with a different resume value of `{"action": "update", "data": }`. This will do the following: - -* combine existing tool call with user-provided tool call arguments and update the existing AI message with the new tool call -* navigate to `run_tool` node with the updated AI message and continue execution - - -=== "Python" - - ```python - # highlight-next-line - from langgraph_sdk.schema import Command - - async for chunk in client.runs.stream( - thread["thread_id"], - assistant_id, - # highlight-next-line - command=Command( - # highlight-next-line - resume={"action": "update", "data": {"city": "San Francisco, USA"}} - # highlight-next-line - ), - stream_mode="updates", - ): - if chunk.data and chunk.event != "metadata": - print(chunk.data) - ``` - -=== "Javascript" - - ```js - const streamResponse = client.runs.stream( - thread["thread_id"], - assistantId, - { - // highlight-next-line - command: { - // highlight-next-line - resume: { "action": "update", "data": { "city": "San Francisco, USA" } } - // highlight-next-line - }, - streamMode: "updates" - } - ); - - for await (const chunk of streamResponse) { - if (chunk.data && chunk.event !== "metadata") { - console.log(chunk.data); - } - } - ``` - -=== "cURL" - - ```bash - curl --request POST \ - --url /threads//runs/stream \ - --header 'Content-Type: application/json' \ - --data "{ - \"assistant_id\": \"agent\", - \"command\": { - \"resume\": { \"action\": \"update\", \"data\": { \"city\": \"San Francisco, USA\" } } - }, - \"stream_mode\": [ - \"updates\" - ] - }" - ``` - -Output: - - {'human_review_node': {'messages': [{'role': 'ai', 'content': [{'text': "I'll help you check the weather in San Francisco.", 'type': 'text'}, {'id': 'toolu_016L4EDPcaQRzzZxiB4Wq2wa', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], 'tool_calls': [{'id': 'toolu_016L4EDPcaQRzzZxiB4Wq2wa', 'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}}], 'id': 'run-b07f0c35-4e93-43a5-9b48-363767ada3ca-0'}]}} - {'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_016L4EDPcaQRzzZxiB4Wq2wa'}]}} - {'call_llm': {'messages': [{'content': "According to the search, it's sunny in San Francisco right now!", 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01De5HurjNUMwMUpfRtMLbX1', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 460, 'output_tokens': 18}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-85e2aaaa-6f61-4fa0-b594-b6e57129d7e7-0', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 460, 'output_tokens': 18, 'total_tokens': 478, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}} - -## Give feedback to a tool call - -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. - -There are multiple ways to do this: - -1. You could add a new message to the state (representing the "result" of a tool call) -2. You could add TWO new messages to the state - one representing an "error" from the tool call, other HumanMessage representing the feedback - -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. - -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! - -=== "Python" - - ```python - input = {"messages": [{"role": "user", "content": "what's the weather in sf?"}]} - - async for chunk in client.runs.stream( - thread["thread_id"], - assistant_id, - input=input, - stream_mode="updates", - ): - if chunk.data and chunk.event != "metadata": - print(chunk.data) - ``` - -=== "Javascript" - - ```js - const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] }; - - const streamResponse = client.runs.stream( - thread["thread_id"], - assistantId, - { - input: input, - streamMode: "updates" - } - ); - - for await (const chunk of streamResponse) { - if (chunk.data && chunk.event !== "metadata") { - console.log(chunk.data); - } - } - ``` - -=== "cURL" - - ```bash - curl --request POST \ - --url /threads//runs/stream \ - --header 'Content-Type: application/json' \ - --data "{ - \"assistant_id\": \"agent\", - \"input\": {\"messages\": [{\"role\": \"human\", \"content\": \"what's the weather in sf?\"}]}, - \"stream_mode\": [ - \"updates\" - ] - }" - ``` - -To do this, we will use `Command` with a different resume value of `{"action": "feedback", "data": }`. This will do the following: - -* create a new tool message that combines existing tool call from LLM with the with user-provided feedback as content -* navigate to `call_llm` node with the updated tool message and continue execution - -=== "Python" - - ```python - # highlight-next-line - from langgraph_sdk.schema import Command - - async for chunk in client.runs.stream( - thread["thread_id"], - assistant_id, - # highlight-next-line - command=Command( - resume={ - "action": "feedback", - "data": "User requested changes: use format for location" - } - ), - stream_mode="updates", - ): - if chunk.data and chunk.event != "metadata": - print(chunk.data) - ``` - -=== "Javascript" - - ```js - const streamResponse = client.runs.stream( - thread["thread_id"], - assistantId, - { - // highlight-next-line - command: { - resume: { - "action": "feedback", - "data": "User requested changes: use format for location" - } - }, - streamMode: "updates" - } - ); - - for await (const chunk of streamResponse) { - if (chunk.data && chunk.event !== "metadata") { - console.log(chunk.data); - } - } - ``` - -=== "cURL" - - ```bash - curl --request POST \ - --url /threads//runs/stream \ - --header 'Content-Type: application/json' \ - --data "{ - \"assistant_id\": \"agent\", - \"command\": { - \"resume\": { \"action\": \"feedback\", \"data\": \"User requested changes: use format for location\" } - }, - \"stream_mode\": [ - \"updates\" - ] - }" - ``` - - -Output: - - {'human_review_node': {'messages': [{'role': 'tool', 'content': 'User requested changes: use format for location', 'name': 'weather_search', 'tool_call_id': 'toolu_01RkPHCjpfoUvPAktaq4Cqhm'}]}} - {'call_llm': {'messages': [{'content': [{'text': 'Let me try that again with the correct format:', 'type': 'text'}, {'id': 'toolu_01Rdrag6cVufHZG26BwVaiE7', 'input': {'city': 'San Francisco, USA'}, 'name': 'weather_search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01EBan969yY5f6iGk6sPgKcj', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 469, 'output_tokens': 68}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-64bbc255-d126-4db0-8ae5-3197cf29bed1-0', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01Rdrag6cVufHZG26BwVaiE7', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 469, 'output_tokens': 68, 'total_tokens': 537, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}} - {'__interrupt__': [{'value': {'question': 'Is this correct?', 'tool_call': {'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01Rdrag6cVufHZG26BwVaiE7', 'type': 'tool_call'}}, 'resumable': True, 'ns': ['human_review_node:e9856878-e28c-5dd1-d353-4d83aa1a3a2b'], 'when': 'during'}]} - -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. - -=== "Python" - - ```python - # highlight-next-line - from langgraph_sdk.schema import Command - - async for chunk in client.runs.stream( - thread["thread_id"], - assistant_id, - # highlight-next-line - command=Command(resume={"action": "continue"}), - stream_mode="updates", - ): - if chunk.data and chunk.event != "metadata": - print(chunk.data) - ``` - -=== "Javascript" - - ```js - const streamResponse = client.runs.stream( - thread["thread_id"], - assistantId, - { - // highlight-next-line - command: { resume: { "action": "continue" } }, - streamMode: "updates" - } - ); - - for await (const chunk of streamResponse) { - if (chunk.data && chunk.event !== "metadata") { - console.log(chunk.data); - } - } - ``` - -=== "cURL" - - ```bash - curl --request POST \ - --url /threads//runs/stream \ - --header 'Content-Type: application/json' \ - --data "{ - \"assistant_id\": \"agent\", - \"command\": { - \"resume\": { \"action\": \"continue\"} - }, - \"stream_mode\": [ - \"updates\" - ] - }" - ``` - -Output: - - {'human_review_node': None} - {'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_01Rdrag6cVufHZG26BwVaiE7'}]}} - {'call_llm': {'messages': [{'content': 'The weather in San Francisco is sunny!', 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_013WTDHhbg8WiYLiQ9n2CaTk', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 550, 'output_tokens': 12}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-b6c815f0-989a-47cf-b150-33e3bbc4eab7-0', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 550, 'output_tokens': 12, 'total_tokens': 562, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}} \ No newline at end of file diff --git a/docs/docs/concepts/human_in_the_loop.md b/docs/docs/concepts/human_in_the_loop.md index 141ca1c2f..4da198223 100644 --- a/docs/docs/concepts/human_in_the_loop.md +++ b/docs/docs/concepts/human_in_the_loop.md @@ -11,21 +11,23 @@ hide: # Human-in-the-loop -LangGraph supports robust **human-in-the-loop (HIL)** workflows, enabling human intervention at any point in an automated process. This is especially useful in large language model (LLM)-driven applications where model output may require validation, correction, or additional context. +To review, edit, and approve tool calls in an agent or workflow, [use LangGraph's human-in-the-loop features](../how-tos/human_in_the_loop/add-human-in-the-loop.md) to enable human intervention at any point in a workflow. This is especially useful in large language model (LLM)-driven applications where model output may require validation, correction, or additional context. + +
+![image](../concepts/img/human_in_the_loop/tool-call-review.png){: style="max-height:400px"} +
## Key capabilities -* **Persistent execution state**: LangGraph checkpoints the graph state after each step, allowing execution to pause indefinitely at defined nodes. This supports asynchronous human review or input without time constraints. +* **Persistent execution state**: LangGraph allows you to pause execution **indefinitely** — for minutes, hours, or even days—until human input is received. This is possible because LangGraph checkpoints the graph state after each step, which allows the system to persist execution context and later resume the workflow, continuing from where it left off. This supports asynchronous human review or input without time constraints. * **Flexible integration points**: HIL logic can be introduced at any point in the workflow. This allows targeted human involvement, such as approving API calls, correcting outputs, or guiding conversations. -## Typical use cases +## Patterns -1. [**🛠️ Reviewing tool calls**](../how-tos/human_in_the_loop/add-human-in-the-loop.md#review-tool-calls): Humans can review, edit, or approve tool calls requested by the LLM before tool execution. -2. **✅ Validating LLM outputs**: Humans can review, edit, or approve content generated by the LLM. -3. **💡 Providing context**: Enable the LLM to explicitly request human input for clarification or additional details or to support multi-turn conversations. +There are four typical design patterns that you can implement using `interrupt` and `Command`: -## Implementation - -* `interrupt` function: Pauses execution at a specific point, presents information for human review. -* `Command` primitive: Used to resume execution with a value provided by the human. +- [Approve or reject](../how-tos/human_in_the_loop/add-human-in-the-loop.md#approve-or-reject): Pause the graph before a critical step, such as an API call, to review and approve the action. If the action is rejected, you can prevent the graph from executing the step, and potentially take an alternative action. This pattern often involves routing the graph based on the human's input. +- [Edit graph state](../how-tos/human_in_the_loop/add-human-in-the-loop.md#review-and-edit-state): Pause the graph to review and edit the graph state. This is useful for correcting mistakes or updating the state with additional information. This pattern often involves updating the state with the human's input. +- [Review tool calls](../how-tos/human_in_the_loop/add-human-in-the-loop.md#review-tool-calls): Pause the graph to review and edit tool calls requested by the LLM before tool execution. +- [Validate human input](../how-tos/human_in_the_loop/add-human-in-the-loop.md#validate-human-input): Pause the graph to validate human input before proceeding with the next step. \ No newline at end of file diff --git a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md index 89ccc2d45..9e929d2fb 100644 --- a/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md +++ b/docs/docs/how-tos/human_in_the_loop/add-human-in-the-loop.md @@ -9,13 +9,20 @@ hide: - tags --- -# Add human-in-the-loop +# Enable human intervention -## `interrupt` +To review, edit, and approve tool calls in an agent or workflow, use LangGraph's [human-in-the-loop](../../concepts/human_in_the_loop.md) features. + +## Pause using `interrupt` The [`interrupt` function][langgraph.types.interrupt] in LangGraph enables human-in-the-loop workflows by pausing the graph at a specific node, presenting information to a human, and resuming the graph with their input. It's useful for tasks like approvals, edits, or gathering additional context. -The graph is resumed using a [`Command`][langgraph.types.Command] object that provides the human's response. +To use `interrupt` in your graph, you need to: + +1. [**Specify a checkpointer**](../../concepts/persistence.md#checkpoints) to save the graph state after each step. +2. **Call `interrupt()`** in the appropriate place. See the [Common Patterns](#common-patterns) section for examples. +3. **Run the graph** with a [**thread ID**](../../concepts/persistence.md#threads) until the `interrupt` is hit. +4. **Resume execution** using `invoke`/`ainvoke`/`stream`/`astream` (see [**The `Command` primitive**](#resume-using-the-command-primitive)). ```python # highlight-next-line @@ -125,34 +132,48 @@ print(graph.invoke(Command(resume="Edited text"), config=config)) # (7)! 7. The graph is resumed with a `Command(resume=...)`, injecting the human's input and continuing execution. - !!! tip "New in 0.4.0" `__interrupt__` is a special key that will be returned when running the graph if the graph is interrupted. Support for `__interrupt__` in `invoke` and `ainvoke` has been added in version 0.4.0. If you're on an older version, you will only see `__interrupt__` in the result if you use `stream` or `astream`. You can also use `graph.get_state(thread_id)` to get the interrupt value. !!! warning - Interrupts are both powerful and ergonomic. However, while they may resemble Python's input() function in terms of developer experience, it's important to note that they do not automatically resume execution from the interruption point. Instead, they rerun the entire node where the interrupt was used. - For this reason, interrupts are typically best placed at the start of a node or in a dedicated node. Please read the [resuming from an interrupt](#how-does-resuming-from-an-interrupt-work) section for more details. + Interrupts are both powerful and ergonomic. However, while they may resemble Python's input() function in terms of developer experience, it's important to note that they do not automatically resume execution from the interruption point. Instead, they rerun the entire node where the interrupt was used. For this reason, interrupts are typically best placed at the start of a node or in a dedicated node. -## Requirements -To use `interrupt` in your graph, you need to: +## Resume using the `Command` primitive -1. [**Specify a checkpointer**](../../concepts/persistence.md#checkpoints) to save the graph state after each step. -2. **Call `interrupt()`** in the appropriate place. See the [Design Patterns](#design-patterns) section for examples. -3. **Run the graph** with a [**thread ID**](../../concepts/persistence.md#threads) until the `interrupt` is hit. -4. **Resume execution** using `invoke`/`ainvoke`/`stream`/`astream` (see [**The `Command` primitive**](#resume-using-the-command-primitive)). +!!! warning -## Design patterns + Resuming from an `interrupt` is different from Python's `input()` function, where execution resumes from the exact point where the `input()` function was called. -There are typically three different **actions** that you can do with a human-in-the-loop workflow: +When the `interrupt` function is used within a graph, execution pauses at that point and awaits user input. -1. **Approve or Reject**: Pause the graph before a critical step, such as an API call, to review and approve the action. If the action is rejected, you can prevent the graph from executing the step, and potentially take an alternative action. This pattern often involve **routing** the graph based on the human's input. -2. **Edit Graph State**: Pause the graph to review and edit the graph state. This is useful for correcting mistakes or updating the state with additional information. This pattern often involves **updating** the state with the human's input. -3. **Get Input**: Explicitly request human input at a particular step in the graph. This is useful for collecting additional information or context to inform the agent's decision-making process. +To resume execution, use the [`Command`][langgraph.types.Command] primitive, which can be supplied via the `invoke`, `ainvoke`, `stream`, or `astream` methods. The graph resumes execution from the beginning of the node where `interrupt(...)` was initially called. This time, the `interrupt` function will return the value provided in `Command(resume=value)` rather than pausing again. All code from the beginning of the node to the `interrupt` will be re-executed. -Below we show different design patterns that can be implemented using these **actions**. +```python +# Resume graph execution by providing the user's input. +graph.invoke(Command(resume={"age": "25"}), thread_config) +``` + +### Resume multiple interrupts with one invocation + +If you have multiple interrupts in the task queue, you can use `Command.resume` with a dictionary mapping of interrupt ids to resume with a single `invoke` / `stream` call. + +For example, once your graph has been interrupted (multiple times, theoretically) and is stalled: + +```python +resume_map = { + i.interrupt_id: f"human input for prompt {i.value}" + for i in parent.get_state(thread_config).interrupts +} + +parent_graph.invoke(Command(resume=resume_map), config=thread_config) +``` + +## Common patterns + +Below we show different design patterns that can be implemented using `interrupt` and `Command`. ### Approve or reject @@ -263,9 +284,7 @@ graph.invoke(Command(resume=True), config=thread_config) print(final_result) ``` -See [how to review tool calls](./review-tool-calls.ipynb) for a more detailed example. - -### Review & edit state +### Review and edit state
![image](../../concepts/img/human_in_the_loop/edit-graph-state-simple.png){: style="max-height:400px"} @@ -393,41 +412,209 @@ critical in applications where the tool calls requested by the LLM may be sensit
+To add a human approval step to a tool: + +1. Use `interrupt()` in the tool to pause execution. +2. Resume with a `Command(resume=...)` to continue based on human input. + ```python -def human_review_node(state) -> Command[Literal["call_llm", "run_tool"]]: - # This is the value we'll be providing via Command(resume=) - human_review = interrupt( - { - "question": "Is this correct?", - # Surface tool calls for review - "tool_call": tool_call - } +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.types import interrupt +from langgraph.prebuilt import create_react_agent + +# An example of a sensitive tool that requires human review / approval +def book_hotel(hotel_name: str): + """Book a hotel""" + # highlight-next-line + response = interrupt( # (1)! + f"Trying to call `book_hotel` with args {{'hotel_name': {hotel_name}}}. " + "Please approve or suggest edits." ) + if response["type"] == "accept": + pass + elif response["type"] == "edit": + hotel_name = response["args"]["hotel_name"] + else: + raise ValueError(f"Unknown response type: {response['type']}") + return f"Successfully booked a stay at {hotel_name}." - review_action, review_data = human_review +# highlight-next-line +checkpointer = InMemorySaver() # (2)! - # Approve the tool call and continue - if review_action == "continue": - return Command(goto="run_tool") - - # Modify the tool call manually and then continue - elif review_action == "update": - ... - updated_msg = get_updated_msg(review_data) - # Remember that to modify an existing message you will need - # to pass the message with a matching ID. - return Command(goto="run_tool", update={"messages": [updated_message]}) - - # Give natural language feedback, and then pass that back to the agent - elif review_action == "feedback": - ... - feedback_msg = get_feedback_msg(review_data) - return Command(goto="call_llm", update={"messages": [feedback_msg]}) +agent = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + tools=[book_hotel], + # highlight-next-line + checkpointer=checkpointer, # (3)! +) ``` -See [how to review tool calls](./review-tool-calls.ipynb) for a more detailed example. +1. The [`interrupt` function][langgraph.types.interrupt] pauses the agent graph at a specific node. In this case, we call `interrupt()` at the beginning of the tool function, which pauses the graph at the node that executes the tool. The information inside `interrupt()` (e.g., tool calls) can be presented to a human, and the graph can be resumed with the user input (tool call approval, edit or feedback). +2. The `InMemorySaver` is used to store the agent state at every step in the tool calling loop. This enables [short-term memory](../memory/add-memory.md#add-short-term-memory) and [human-in-the-loop](../../concepts/human_in_the_loop.md) capabilities. In this example, we use `InMemorySaver` to store the agent state in memory. In a production application, the agent state will be stored in a database. +3. Initialize the agent with the `checkpointer`. -### Validating human input +Run the agent with the `stream()` method, passing the `config` object to specify the thread ID. This allows the agent to resume the same conversation on future invocations. + +```python +config = { + "configurable": { + # highlight-next-line + "thread_id": "1" + } +} + +for chunk in agent.stream( + {"messages": [{"role": "user", "content": "book a stay at McKittrick hotel"}]}, + # highlight-next-line + config +): + print(chunk) + print("\n") +``` + +> You should see that the agent runs until it reaches the `interrupt()` call, at which point it pauses and waits for human input. + +Resume the agent with a `Command(resume=...)` to continue based on human input. + +```python +from langgraph.types import Command + +for chunk in agent.stream( + # highlight-next-line + Command(resume={"type": "accept"}), # (1)! + # Command(resume={"type": "edit", "args": {"hotel_name": "McKittrick Hotel"}}), + config +): + print(chunk) + print("\n") +``` + +1. The [`interrupt` function][langgraph.types.interrupt] is used in conjunction with the [`Command`][langgraph.types.Command] object to resume the graph with a value provided by the human. + +### Add interrupts to any tool + +You can create a wrapper to add interrupts to *any* tool. The example below provides a reference implementation compatible with [Agent Inbox UI](https://github.com/langchain-ai/agent-inbox) and [Agent Chat UI](https://github.com/langchain-ai/agent-chat-ui). + +```python title="Wrapper that adds human-in-the-loop to any tool" +from typing import Callable +from langchain_core.tools import BaseTool, tool as create_tool +from langchain_core.runnables import RunnableConfig +from langgraph.types import interrupt +from langgraph.prebuilt.interrupt import HumanInterruptConfig, HumanInterrupt + +def add_human_in_the_loop( + tool: Callable | BaseTool, + *, + interrupt_config: HumanInterruptConfig = None, +) -> BaseTool: + """Wrap a tool to support human-in-the-loop review.""" + if not isinstance(tool, BaseTool): + tool = create_tool(tool) + + if interrupt_config is None: + interrupt_config = { + "allow_accept": True, + "allow_edit": True, + "allow_respond": True, + } + + @create_tool( # (1)! + tool.name, + description=tool.description, + args_schema=tool.args_schema + ) + def call_tool_with_interrupt(config: RunnableConfig, **tool_input): + request: HumanInterrupt = { + "action_request": { + "action": tool.name, + "args": tool_input + }, + "config": interrupt_config, + "description": "Please review the tool call" + } + # highlight-next-line + response = interrupt([request])[0] # (2)! + # approve the tool call + if response["type"] == "accept": + tool_response = tool.invoke(tool_input, config) + # update tool call args + elif response["type"] == "edit": + tool_input = response["args"]["args"] + tool_response = tool.invoke(tool_input, config) + # respond to the LLM with user feedback + elif response["type"] == "response": + user_feedback = response["args"] + tool_response = user_feedback + else: + raise ValueError(f"Unsupported interrupt response type: {response['type']}") + + return tool_response + + return call_tool_with_interrupt +``` + +1. This wrapper creates a new tool that calls `interrupt()` **before** executing the wrapped tool. +2. `interrupt()` is using special input and output format that's expected by [Agent Inbox UI](https://github.com/langchain-ai/agent-inbox): + - a list of [`HumanInterrupt`][langgraph.prebuilt.interrupt.HumanInterrupt] objects is sent to `AgentInbox` render interrupt information to the end user + - resume value is provided by `AgentInbox` as a list (i.e., `Command(resume=[...])`) + +You can use the `add_human_in_the_loop` wrapper to add `interrupt()` to any tool without having to add it *inside* the tool: + +```python +from langgraph.checkpoint.memory import InMemorySaver +from langgraph.prebuilt import create_react_agent + +# highlight-next-line +checkpointer = InMemorySaver() + +def book_hotel(hotel_name: str): + """Book a hotel""" + return f"Successfully booked a stay at {hotel_name}." + + +agent = create_react_agent( + model="anthropic:claude-3-5-sonnet-latest", + tools=[ + # highlight-next-line + add_human_in_the_loop(book_hotel), # (1)! + ], + # highlight-next-line + checkpointer=checkpointer, +) + +config = {"configurable": {"thread_id": "1"}} + +# Run the agent +for chunk in agent.stream( + {"messages": [{"role": "user", "content": "book a stay at McKittrick hotel"}]}, + # highlight-next-line + config +): + print(chunk) + print("\n") +``` + +1. The `add_human_in_the_loop` wrapper is used to add `interrupt()` to the tool. This allows the agent to pause execution and wait for human input before proceeding with the tool call. + +> You should see that the agent runs until it reaches the `interrupt()` call, +> at which point it pauses and waits for human input. + +Resume the agent with a `Command(resume=...)` to continue based on human input. + +```python +from langgraph.types import Command + +for chunk in agent.stream( + # highlight-next-line + Command(resume=[{"type": "accept"}]), + # Command(resume=[{"type": "edit", "args": {"args": {"hotel_name": "McKittrick Hotel"}}}]), + config +): + print(chunk) + print("\n") +``` + +### Validate human input If you need to validate the input provided by the human within the graph itself (rather than on the client side), you can achieve this by using multiple interrupt calls within a single node. @@ -525,91 +712,15 @@ def human_node(state: State): print(final_result) # Should include the valid age ``` +## Considerations -## Resume using the `Command` primitive +When using human-in-the-loop, there are some considerations to keep in mind. -When the `interrupt` function is used within a graph, execution pauses at that point and awaits user input. +### Using with code with side-effects -To resume execution, use the [`Command`][langgraph.types.Command] primitive, which can be supplied via the `invoke`, `ainvoke`, `stream`, or `astream` methods. +Place code with side effects, such as API calls, after the `interrupt` or in a separate node to avoid duplication, as these are re-triggered every time the node is resumed. -**Providing a response to the `interrupt`:** -To continue execution, pass the user's input using `Command(resume=value)`. The graph resumes execution from the beginning of the node where `interrupt(...)` was initially called. This time, the `interrupt` function will return the value provided in `Command(resume=value)` rather than pausing again. - -```python -# Resume graph execution by providing the user's input. -graph.invoke(Command(resume={"age": "25"}), thread_config) -``` - -## How does resuming from an interrupt work? - -!!! warning - - Resuming from an `interrupt` is **different** from Python's `input()` function, where execution resumes from the exact point where the `input()` function was called. - -A critical aspect of using `interrupt` is understanding how resuming works. When you resume execution after an `interrupt`, graph execution starts from the **beginning** of the **graph node** where the last `interrupt` was triggered. - -**All** code from the beginning of the node to the `interrupt` will be re-executed. - -```python -counter = 0 -def node(state: State): - # All the code from the beginning of the node to the interrupt will be re-executed - # when the graph resumes. - global counter - counter += 1 - print(f"> Entered the node: {counter} # of times") - # Pause the graph and wait for user input. - answer = interrupt() - print("The value of counter is:", counter) - ... -``` - -Upon **resuming** the graph, the counter will be incremented a second time, resulting in the following output: - -```pycon -> Entered the node: 2 # of times -The value of counter is: 2 -``` - -### Resuming multiple interrupts with one invocation - -If you have multiple interrupts in the task queue, you can use `Command.resume` with a dictionary mapping -of interrupt ids to resume values to resume multiple interrupts with a single `invoke` / `stream` call. - -For example, once your graph has been interrupted (multiple times, theoretically) and is stalled: - -```python -resume_map = { - i.interrupt_id: f"human input for prompt {i.value}" - for i in parent.get_state(thread_config).interrupts -} - -parent_graph.invoke(Command(resume=resume_map), config=thread_config) -``` - -## Common pitfalls - -### Side-effects - -Place code with side effects, such as API calls, **after** the `interrupt` to avoid duplication, as these are re-triggered every time the node is resumed. - -=== "Side effects before interrupt (BAD)" - - This code will re-execute the API call another time when the node is resumed from - the `interrupt`. - - This can be problematic if the API call is not idempotent or is just expensive. - - ```python - from langgraph.types import interrupt - - def human_node(state: State): - """Human node with validation.""" - api_call(...) # This code will be re-executed when the node is resumed. - answer = interrupt(question) - ``` - -=== "Side effects after interrupt (OK)" +=== "Side effects after interrupt" ```python from langgraph.types import interrupt @@ -622,7 +733,7 @@ Place code with side effects, such as API calls, **after** the `interrupt` to av api_call(answer) # OK as it's after the interrupt ``` -=== "Side effects in a separate node (OK)" +=== "Side effects in a separate node" ```python from langgraph.types import interrupt @@ -640,11 +751,9 @@ Place code with side effects, such as API calls, **after** the `interrupt` to av api_call(...) # OK as it's in a separate node ``` -### Subgraphs called as functions +### Using with subgraphs called as functions -When invoking a subgraph [as a function](../../how-tos/subgraph.ipynb#different-state-schemas), the **parent graph** will resume execution from the **beginning of the node** where the subgraph was invoked (and where an `interrupt` was triggered). Similarly, the **subgraph**, will resume from the **beginning of the node** where the `interrupt()` function was called. - -For example, +When invoking a subgraph as a function, the parent graph will resume execution from the **beginning of the node** where the subgraph was invoked where the `interrupt` was triggered. Similarly, the **subgraph** will resume from the **beginning of the node** where the `interrupt()` function was called. ```python def node_in_parent_graph(state: State): @@ -772,11 +881,9 @@ def node_in_parent_graph(state: State): {'parent_node': {'state_counter': 1}} ``` - - ### Using multiple interrupts -Using multiple interrupts within a **single** node can be helpful for patterns like [validating human input](#validating-human-input). However, using multiple interrupts in the same node can lead to unexpected behavior if not handled carefully. +Using multiple interrupts within a **single** node can be helpful for patterns like [validating human input](../how-tos/human_in_the_loop/add-human-in-the-loop.md#validate-human-input). However, using multiple interrupts in the same node can lead to unexpected behavior if not handled carefully. When a node contains multiple interrupt calls, LangGraph keeps a list of resume values specific to the task executing the node. Whenever execution resumes, it starts at the beginning of the node. For each interrupt encountered, LangGraph checks if a matching value exists in the task's resume list. Matching is **strictly index-based**, so the order of interrupt calls within the node is critical. @@ -845,4 +952,5 @@ To avoid issues, refrain from dynamically changing the node's structure between {'__interrupt__': (Interrupt(value='what is your name?', resumable=True, ns=['human_node:3a007ef9-c30d-c357-1ec1-86a1a70d8fba'], when='during'),)} Name: N/A. Age: John {'human_node': {'age': 'John', 'name': 'N/A'}} - ``` \ No newline at end of file + ``` + 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 deleted file mode 100644 index c080c4afa..000000000 --- a/docs/docs/how-tos/human_in_the_loop/review-tool-calls.ipynb +++ /dev/null @@ -1,773 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", - "metadata": {}, - "source": [ - "# How to Review Tool Calls\n", - "\n", - "!!! tip \"Prerequisites\"\n", - "\n", - " This guide assumes familiarity with the following concepts:\n", - "\n", - " * [Tool calling](https://python.langchain.com/docs/concepts/tool_calling/)\n", - " * [Human-in-the-loop](../../../concepts/human_in_the_loop)\n", - " * [LangGraph Glossary](../../../concepts/low_level) \n", - "\n", - "Human-in-the-loop (HIL) interactions are crucial for [agentic systems](../../../concepts/agentic_concepts). 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", - "\n", - "Note that using tool calls is common **whether actually calling tools or not**.\n", - "\n", - "There are typically a few different interactions you may want to do here:\n", - "\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\n", - "\n", - "\n", - "We can implement these in LangGraph using the [`interrupt()`][langgraph.types.interrupt] function. `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_action == \"continue\":\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", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "7cbd446a-808f-4394-be92-d45ab818953c", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "First we need to install the packages required" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", - "metadata": {}, - "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install --quiet -U langgraph langchain_anthropic" - ] - }, - { - "cell_type": "markdown", - "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", - "metadata": {}, - "source": [ - "Next, we need to set API keys for Anthropic (the LLM we will use)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ANTHROPIC_API_KEY: ········\n" - ] - } - ], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"ANTHROPIC_API_KEY\")" - ] - }, - { - "cell_type": "markdown", - "id": "f0ed46a8-effe-4596-b0e1-a6a29ee16f5c", - "metadata": {}, - "source": [ - "
\n", - "

Set up LangSmith for LangGraph development

\n", - "

\n", - " Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph — read more about how to get started here. \n", - "

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "id": "035e567c-db5c-4085-ba4e-5b3814561c21", - "metadata": {}, - "source": [ - "## Simple Usage\n", - "\n", - "Let's set up a very simple graph that facilitates this.\n", - "First, we will have an LLM call that decides what action to take.\n", - "Then we go to a human node. This node actually doesn't do anything - the idea is that we interrupt before this node and then apply any updates to the state.\n", - "After that, we check the state and either route back to the LLM or to the correct tool.\n", - "\n", - "Let's see this in action!" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "85e452f8-f33a-4ead-bb4d-7386cdba8edc", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "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", - "from IPython.display import Image, display\n", - "\n", - "\n", - "@tool\n", - "def weather_search(city: str):\n", - " \"\"\"Search for the weather\"\"\"\n", - " print(\"----\")\n", - " print(f\"Searching for: {city}\")\n", - " print(\"----\")\n", - " return \"Sunny!\"\n", - "\n", - "\n", - "model = ChatAnthropic(model_name=\"claude-3-5-sonnet-latest\").bind_tools(\n", - " [weather_search]\n", - ")\n", - "\n", - "\n", - "class State(MessagesState):\n", - " \"\"\"Simple state.\"\"\"\n", - "\n", - "\n", - "def call_llm(state):\n", - " return {\"messages\": [model.invoke(state[\"messages\"])]}\n", - "\n", - "\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", - " new_messages = []\n", - " tools = {\"weather_search\": weather_search}\n", - " tool_calls = state[\"messages\"][-1].tool_calls\n", - " for tool_call in tool_calls:\n", - " tool = tools[tool_call[\"name\"]]\n", - " result = tool.invoke(tool_call[\"args\"])\n", - " new_messages.append(\n", - " {\n", - " \"role\": \"tool\",\n", - " \"name\": tool_call[\"name\"],\n", - " \"content\": result,\n", - " \"tool_call_id\": tool_call[\"id\"],\n", - " }\n", - " )\n", - " return {\"messages\": new_messages}\n", - "\n", - "\n", - "def route_after_llm(state) -> Literal[END, \"human_review_node\"]:\n", - " if len(state[\"messages\"][-1].tool_calls) == 0:\n", - " return END\n", - " else:\n", - " return \"human_review_node\"\n", - "\n", - "\n", - "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_edge(\"run_tool\", \"call_llm\")\n", - "\n", - "# Set up memory\n", - "memory = MemorySaver()\n", - "\n", - "# Add\n", - "graph = builder.compile(checkpointer=memory)\n", - "\n", - "# View\n", - "display(Image(graph.get_graph().draw_mermaid_png()))" - ] - }, - { - "cell_type": "markdown", - "id": "d246d39f-4b36-459b-bd54-bf363753e590", - "metadata": {}, - "source": [ - "## Example with no review\n", - "\n", - "Let's look at an example when no review is required (because no tools are called)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "1b3aa6fc-c7fb-4819-8d7f-ba6057cc4edf", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'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" - ] - } - ], - "source": [ - "# Input\n", - "initial_input = {\"messages\": [{\"role\": \"user\", \"content\": \"hi!\"}]}\n", - "\n", - "# Thread\n", - "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=\"updates\"):\n", - " print(event)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "markdown", - "id": "d59dc607-e70d-497b-aac9-78c847c27042", - "metadata": {}, - "source": [ - "If we check the state, we can see that it is finished" - ] - }, - { - "cell_type": "markdown", - "id": "5c1985f7-54f1-420f-a2b6-5e6154909966", - "metadata": {}, - "source": [ - "## Example of approving tool\n", - "\n", - "Let's now look at what it looks like to approve a tool call" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "2561a38f-edb5-4b44-b2d7-6a7b70d2e6b7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'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" - ] - } - ], - "source": [ - "# Input\n", - "initial_input = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf?\"}]}\n", - "\n", - "# Thread\n", - "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=\"updates\"):\n", - " print(event)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "markdown", - "id": "4ef6d51c-e2b6-4266-8de7-acf1a0b62a57", - "metadata": {}, - "source": [ - "If we now check, we can see that it is waiting on human review" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "33d68f0f-d435-4dd1-8013-6a59186dc9f5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Pending Executions!\n", - "('human_review_node',)\n" - ] - } - ], - "source": [ - "print(\"Pending Executions!\")\n", - "print(graph.get_state(thread).next)" - ] - }, - { - "cell_type": "markdown", - "id": "14c99fdd-4204-4c2d-b1af-02f38ab6ad57", - "metadata": {}, - "source": [ - "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": 7, - "id": "f9a0d5d4-52ff-49e0-a6f4-41f9a0e844d8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'human_review_node': None}\n", - "\n", - "\n", - "----\n", - "Searching for: sf\n", - "----\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(\n", - " # provide value\n", - " Command(resume={\"action\": \"continue\"}),\n", - " thread,\n", - " stream_mode=\"updates\",\n", - "):\n", - " print(event)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "markdown", - "id": "8d30c4a7-b480-4ede-b2b4-8ec11de95e30", - "metadata": {}, - "source": [ - "## Edit Tool Call\n", - "\n", - "Let's now say we want to edit the tool call. E.g. change some of the parameters (or even the tool called!) but then execute that tool." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "ec77831c-e6b8-4903-9146-e098a4b2fda1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'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" - ] - } - ], - "source": [ - "# Input\n", - "initial_input = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf?\"}]}\n", - "\n", - "# Thread\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=\"updates\"):\n", - " print(event)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "edcffbd7-829b-4d0c-88bf-cd531bc0e6b2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Pending Executions!\n", - "('human_review_node',)\n" - ] - } - ], - "source": [ - "print(\"Pending Executions!\")\n", - "print(graph.get_state(thread).next)" - ] - }, - { - "cell_type": "markdown", - "id": "87358aca-9b8f-48c7-98d4-3d755f6b0104", - "metadata": {}, - "source": [ - "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": 10, - "id": "b2f73998-baae-4c00-8a90-f4153e924941", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'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", - "----\n", - "Searching for: San Francisco, USA\n", - "----\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": [ - "# Let's now continue executing from here\n", - "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\")" - ] - }, - { - "cell_type": "markdown", - "id": "e14acc96-3d50-44b1-8616-b8d9131e46c4", - "metadata": {}, - "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 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_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 (see `human_review_node` implementation). Let's see this in action!" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "d57d5131-7912-4216-aa87-b7272507fa51", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'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" - ] - } - ], - "source": [ - "# Input\n", - "initial_input = {\"messages\": [{\"role\": \"user\", \"content\": \"what's the weather in sf?\"}]}\n", - "\n", - "# Thread\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=\"updates\"):\n", - " print(event)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "e33ad664-0307-43c5-b85a-1e02eebceb5c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Pending Executions!\n", - "('human_review_node',)\n" - ] - } - ], - "source": [ - "print(\"Pending Executions!\")\n", - "print(graph.get_state(thread).next)" - ] - }, - { - "cell_type": "markdown", - "id": "483d9455-8625-4c6a-9b98-f731403b2ed3", - "metadata": {}, - "source": [ - "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": 13, - "id": "3f05f8b6-6128-4de5-8884-862fc93f1227", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'human_review_node': {'messages': [{'role': 'tool', 'content': 'User requested changes: use format for location', 'name': 'weather_search', 'tool_call_id': 'toolu_01QxXNTCasnNLQCGAiVoNUBe'}]}}\n", - "\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": [ - "# Let's now continue executing from here\n", - "for event in graph.stream(\n", - " # provide our natural language feedback!\n", - " Command(\n", - " resume={\n", - " \"action\": \"feedback\",\n", - " \"data\": \"User requested changes: use format for location\",\n", - " }\n", - " ),\n", - " thread,\n", - " stream_mode=\"updates\",\n", - "):\n", - " print(event)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "markdown", - "id": "2d2e79ab-7cdb-42ce-b2ca-2932f8782c90", - "metadata": {}, - "source": [ - "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": 14, - "id": "ca558915-f4d9-4ff2-95b7-cdaf0c6db485", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Pending Executions!\n", - "('human_review_node',)\n" - ] - } - ], - "source": [ - "print(\"Pending Executions!\")\n", - "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\"}), thread, stream_mode=\"updates\"\n", - "):\n", - " print(event)\n", - " print(\"\\n\")" - ] - } - ], - "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.4" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/docs/how-tos/memory/add-memory.md b/docs/docs/how-tos/memory/add-memory.md index c44b7d4f0..4aaf8ec69 100644 --- a/docs/docs/how-tos/memory/add-memory.md +++ b/docs/docs/how-tos/memory/add-memory.md @@ -1343,7 +1343,7 @@ def delete_messages(state): The problem with trimming or removing messages, as shown above, is that you may lose information from culling of the message queue. Because of this, some applications benefit from a more sophisticated approach of summarizing the message history using a chat model. -![](img/memory/summary.png) +![](../../concepts/img/memory/summary.png) === "In an agent" diff --git a/docs/docs/how-tos/review-tool-calls-functional.ipynb b/docs/docs/how-tos/review-tool-calls-functional.ipynb deleted file mode 100644 index bdf68432e..000000000 --- a/docs/docs/how-tos/review-tool-calls-functional.ipynb +++ /dev/null @@ -1,627 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# How to review tool calls (Functional API)\n", - "\n", - "!!! info \"Prerequisites\"\n", - " This guide assumes familiarity with the following:\n", - "\n", - " - Implementing [human-in-the-loop](../../concepts/human_in_the_loop) workflows with [interrupt](../../concepts/human_in_the_loop/#interrupt)\n", - " - [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional)\n", - "\n", - "This guide demonstrates how to implement human-in-the-loop workflows in a ReAct agent using the LangGraph [Functional API](../../concepts/functional_api).\n", - "\n", - "We will build off of the agent created in the [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional) guide.\n", - "\n", - "Specifically, we will demonstrate how to review [tool calls](https://python.langchain.com/docs/concepts/tool_calling/) generated by a [chat model](https://python.langchain.com/docs/concepts/chat_models/) prior to their execution. This can be accomplished through use of the [interrupt](../../concepts/human_in_the_loop/#interrupt) function at key points in our application.\n", - "\n", - "**Preview**:\n", - "\n", - "We will implement a simple function that reviews tool calls generated from our chat model and call it from inside our application's [entrypoint](../../concepts/functional_api/#entrypoint):\n", - "\n", - "```python\n", - "def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]:\n", - " \"\"\"Review a tool call, returning a validated version.\"\"\"\n", - " human_review = interrupt(\n", - " {\n", - " \"question\": \"Is this correct?\",\n", - " \"tool_call\": tool_call,\n", - " }\n", - " )\n", - " review_action = human_review[\"action\"]\n", - " review_data = human_review.get(\"data\")\n", - " if review_action == \"continue\":\n", - " return tool_call\n", - " elif review_action == \"update\":\n", - " updated_tool_call = {**tool_call, **{\"args\": review_data}}\n", - " return updated_tool_call\n", - " elif review_action == \"feedback\":\n", - " return ToolMessage(\n", - " content=review_data, name=tool_call[\"name\"], tool_call_id=tool_call[\"id\"]\n", - " )\n", - "```\n", - "\n", - "## Setup\n", - "\n", - "First, let's install the required packages and set our API keys:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph langchain-openai" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Set up LangSmith for better debugging

\n", - "

\n", - " Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM aps built with LangGraph — read more about how to get started in the docs. \n", - "

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define model and tools\n", - "\n", - "Let's first define the tools and model we will use for our example. As in the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional), we will use a single place-holder tool that gets a description of the weather for a location.\n", - "\n", - "We will use an [OpenAI](https://python.langchain.com/docs/integrations/providers/openai/) chat model for this example, but any model [supporting tool-calling](https://python.langchain.com/docs/integrations/chat/) will suffice." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "from langchain_core.tools import tool\n", - "\n", - "model = ChatOpenAI(model=\"gpt-4o-mini\")\n", - "\n", - "\n", - "@tool\n", - "def get_weather(location: str):\n", - " \"\"\"Call to get the weather from a specific location.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n", - " return \"It's sunny!\"\n", - " elif \"boston\" in location.lower():\n", - " return \"It's rainy!\"\n", - " else:\n", - " return f\"I am not sure what the weather is in {location}\"\n", - "\n", - "\n", - "tools = [get_weather]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define tasks\n", - "\n", - "Our [tasks](../../concepts/functional_api/#task) are unchanged from the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional):\n", - "\n", - "1. **Call model**: We want to query our chat model with a list of messages.\n", - "2. **Call tool**: If our model generates tool calls, we want to execute them." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.messages import ToolCall, ToolMessage\n", - "from langgraph.func import entrypoint, task\n", - "\n", - "\n", - "tools_by_name = {tool.name: tool for tool in tools}\n", - "\n", - "\n", - "@task\n", - "def call_model(messages):\n", - " \"\"\"Call model with a sequence of messages.\"\"\"\n", - " response = model.bind_tools(tools).invoke(messages)\n", - " return response\n", - "\n", - "\n", - "@task\n", - "def call_tool(tool_call):\n", - " tool = tools_by_name[tool_call[\"name\"]]\n", - " observation = tool.invoke(tool_call[\"args\"])\n", - " return ToolMessage(content=observation, tool_call_id=tool_call[\"id\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Define entrypoint\n", - "\n", - "To review tool calls before execution, we add a `review_tool_call` function that calls [interrupt](../../concepts/human_in_the_loop/#interrupt). When this function is called, execution will be paused until we issue a command to resume it.\n", - "\n", - "Given a tool call, our function will `interrupt` for human review. At that point we can either:\n", - "\n", - "- Accept the tool call;\n", - "- Revise the tool call and continue;\n", - "- Generate a custom tool message (e.g., instructing the model to re-format its tool call).\n", - "\n", - "We will demonstrate these three cases in the [usage examples](#usage) below." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Union\n", - "\n", - "\n", - "def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]:\n", - " \"\"\"Review a tool call, returning a validated version.\"\"\"\n", - " human_review = interrupt(\n", - " {\n", - " \"question\": \"Is this correct?\",\n", - " \"tool_call\": tool_call,\n", - " }\n", - " )\n", - " review_action = human_review[\"action\"]\n", - " review_data = human_review.get(\"data\")\n", - " if review_action == \"continue\":\n", - " return tool_call\n", - " elif review_action == \"update\":\n", - " updated_tool_call = {**tool_call, **{\"args\": review_data}}\n", - " return updated_tool_call\n", - " elif review_action == \"feedback\":\n", - " return ToolMessage(\n", - " content=review_data, name=tool_call[\"name\"], tool_call_id=tool_call[\"id\"]\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now update our [entrypoint](../../concepts/functional_api/#entrypoint) to review the generated tool calls. If a tool call is accepted or revised, we execute in the same way as before. Otherwise, we just append the `ToolMessage` supplied by the human.\n", - "\n", - "!!! tip\n", - "\n", - " The results of prior tasks — in this case the initial model call — are persisted, so that they are not run again following the `interrupt`." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.checkpoint.memory import MemorySaver\n", - "from langgraph.graph.message import add_messages\n", - "from langgraph.types import Command, interrupt\n", - "\n", - "\n", - "checkpointer = MemorySaver()\n", - "\n", - "\n", - "@entrypoint(checkpointer=checkpointer)\n", - "def agent(messages, previous):\n", - " if previous is not None:\n", - " messages = add_messages(previous, messages)\n", - "\n", - " llm_response = call_model(messages).result()\n", - " while True:\n", - " if not llm_response.tool_calls:\n", - " break\n", - "\n", - " # Review tool calls\n", - " tool_results = []\n", - " tool_calls = []\n", - " for i, tool_call in enumerate(llm_response.tool_calls):\n", - " review = review_tool_call(tool_call)\n", - " if isinstance(review, ToolMessage):\n", - " tool_results.append(review)\n", - " else: # is a validated tool call\n", - " tool_calls.append(review)\n", - " if review != tool_call:\n", - " llm_response.tool_calls[i] = review # update message\n", - "\n", - " # Execute remaining tool calls\n", - " tool_result_futures = [call_tool(tool_call) for tool_call in tool_calls]\n", - " remaining_tool_results = [fut.result() for fut in tool_result_futures]\n", - "\n", - " # Append to message list\n", - " messages = add_messages(\n", - " messages,\n", - " [llm_response, *tool_results, *remaining_tool_results],\n", - " )\n", - "\n", - " # Call model again\n", - " llm_response = call_model(messages).result()\n", - "\n", - " # Generate final response\n", - " messages = add_messages(messages, llm_response)\n", - " return entrypoint.final(value=llm_response, save=messages)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Usage\n", - "\n", - "Let's demonstrate some scenarios." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "def _print_step(step: dict) -> None:\n", - " for task_name, result in step.items():\n", - " if task_name == \"agent\":\n", - " continue # just stream from tasks\n", - " print(f\"\\n{task_name}:\")\n", - " if task_name in (\"__interrupt__\", \"review_tool_call\"):\n", - " print(result)\n", - " else:\n", - " result.pretty_print()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Accept a tool call\n", - "\n", - "To accept a tool call, we just indicate in the data we provide in the `Command` that the tool call should pass through." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"1\"}}" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n", - "\n", - "call_model:\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "Tool Calls:\n", - " get_weather (call_Bh5cSwMqCpCxTjx7AjdrQTPd)\n", - " Call ID: call_Bh5cSwMqCpCxTjx7AjdrQTPd\n", - " Args:\n", - " location: San Francisco\n", - "\n", - "__interrupt__:\n", - "(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco'}, 'id': 'call_Bh5cSwMqCpCxTjx7AjdrQTPd', 'type': 'tool_call'}}, resumable=True, ns=['agent:22fcc9cd-3573-b39b-eea7-272a025903e2'], when='during'),)\n" - ] - } - ], - "source": [ - "user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n", - "print(user_message)\n", - "\n", - "for step in agent.stream([user_message], config):\n", - " _print_step(step)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "call_tool:\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "\n", - "It's sunny!\n", - "\n", - "call_model:\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "The weather in San Francisco is sunny!\n" - ] - } - ], - "source": [ - "# highlight-next-line\n", - "human_input = Command(resume={\"action\": \"continue\"})\n", - "\n", - "for step in agent.stream(human_input, config):\n", - " _print_step(step)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Revise a tool call\n", - "\n", - "To revise a tool call, we can supply updated arguments." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"2\"}}" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n", - "\n", - "call_model:\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "Tool Calls:\n", - " get_weather (call_b9h8e18FqH0IQm3NMoeYKz6N)\n", - " Call ID: call_b9h8e18FqH0IQm3NMoeYKz6N\n", - " Args:\n", - " location: san francisco\n", - "\n", - "__interrupt__:\n", - "(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'san francisco'}, 'id': 'call_b9h8e18FqH0IQm3NMoeYKz6N', 'type': 'tool_call'}}, resumable=True, ns=['agent:9559a81d-5720-dc19-a457-457bac7bdd83'], when='during'),)\n" - ] - } - ], - "source": [ - "user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n", - "print(user_message)\n", - "\n", - "for step in agent.stream([user_message], config):\n", - " _print_step(step)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "call_tool:\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "\n", - "It's sunny!\n", - "\n", - "call_model:\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "The weather in San Francisco is sunny!\n" - ] - } - ], - "source": [ - "# highlight-next-line\n", - "human_input = Command(resume={\"action\": \"update\", \"data\": {\"location\": \"SF, CA\"}})\n", - "\n", - "for step in agent.stream(human_input, config):\n", - " _print_step(step)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The LangSmith traces for this run are particularly informative:\n", - "\n", - "- In the trace [before the interrupt](https://smith.langchain.com/public/c8b07579-5cf4-4adb-a849-282163bc9d99/r/b5b128d6-e715-480b-b58d-59e64f724275), we generate a tool call for location `\"San Francisco\"`.\n", - "- In the trace [after resuming](https://smith.langchain.com/public/b28b92e5-a555-482d-aa4d-c675a19f0eb5/r), we see that the tool call in the message has been updated to `\"SF, CA\"`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Generate a custom ToolMessage\n", - "\n", - "To Generate a custom `ToolMessage`, we supply the content of the message. In this case we will ask the model to reformat its tool call." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"3\"}}" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'role': 'user', 'content': \"What's the weather in san francisco?\"}\n", - "\n", - "call_model:\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "Tool Calls:\n", - " get_weather (call_VqGjKE7uu8HdWs9XuY1kMV18)\n", - " Call ID: call_VqGjKE7uu8HdWs9XuY1kMV18\n", - " Args:\n", - " location: San Francisco\n", - "\n", - "__interrupt__:\n", - "(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco'}, 'id': 'call_VqGjKE7uu8HdWs9XuY1kMV18', 'type': 'tool_call'}}, resumable=True, ns=['agent:4b3b372b-9da3-70be-5c68-3d9317346070'], when='during'),)\n" - ] - } - ], - "source": [ - "user_message = {\"role\": \"user\", \"content\": \"What's the weather in san francisco?\"}\n", - "print(user_message)\n", - "\n", - "for step in agent.stream([user_message], config):\n", - " _print_step(step)" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "call_model:\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "Tool Calls:\n", - " get_weather (call_xoXkK8Cz0zIpvWs78qnXpvYp)\n", - " Call ID: call_xoXkK8Cz0zIpvWs78qnXpvYp\n", - " Args:\n", - " location: San Francisco, CA\n", - "\n", - "__interrupt__:\n", - "(Interrupt(value={'question': 'Is this correct?', 'tool_call': {'name': 'get_weather', 'args': {'location': 'San Francisco, CA'}, 'id': 'call_xoXkK8Cz0zIpvWs78qnXpvYp', 'type': 'tool_call'}}, resumable=True, ns=['agent:4b3b372b-9da3-70be-5c68-3d9317346070'], when='during'),)\n" - ] - } - ], - "source": [ - "# highlight-next-line\n", - "human_input = Command(\n", - " # highlight-next-line\n", - " resume={\n", - " # highlight-next-line\n", - " \"action\": \"feedback\",\n", - " # highlight-next-line\n", - " \"data\": \"Please format as , .\",\n", - " # highlight-next-line\n", - " },\n", - " # highlight-next-line\n", - ")\n", - "\n", - "for step in agent.stream(human_input, config):\n", - " _print_step(step)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Once it is re-formatted, we can accept it:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "call_tool:\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "\n", - "It's sunny!\n", - "\n", - "call_model:\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "The weather in San Francisco, CA is sunny!\n" - ] - } - ], - "source": [ - "# highlight-next-line\n", - "human_input = Command(resume={\"action\": \"continue\"})\n", - "\n", - "for step in agent.stream(human_input, config):\n", - " _print_step(step)" - ] - } - ], - "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.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/docs/how-tos/use-functional-api.md b/docs/docs/how-tos/use-functional-api.md index a50014e41..340bbb347 100644 --- a/docs/docs/how-tos/use-functional-api.md +++ b/docs/docs/how-tos/use-functional-api.md @@ -462,10 +462,161 @@ main.invoke(None, config=config) The functional API supports [human-in-the-loop](../concepts/human_in_the_loop.md) workflows using the `interrupt` function and the `Command` primitive. -Please see the following examples for more details: +### Basic human-in-the-loop workflow -* [How to wait for user input (Functional API)](./wait-user-input-functional.ipynb): Shows how to implement a simple human-in-the-loop workflow using the functional API. -* [How to review tool calls (Functional API)](./review-tool-calls-functional.ipynb): Guide demonstrates how to implement human-in-the-loop workflows in a ReAct agent using the LangGraph Functional API. +We will create three [tasks](../concepts/functional_api.md#task): + +1. Append `"bar"`. +2. Pause for human input. When resuming, append human input. +3. Append `"qux"`. + +```python +from langgraph.func import entrypoint, task +from langgraph.types import Command, interrupt + + +@task +def step_1(input_query): + """Append bar.""" + return f"{input_query} bar" + + +@task +def human_feedback(input_query): + """Append user input.""" + feedback = interrupt(f"Please provide feedback: {input_query}") + return f"{input_query} {feedback}" + + +@task +def step_3(input_query): + """Append qux.""" + return f"{input_query} qux" +``` + +We can now compose these tasks in an [entrypoint](../concepts/functional_api.md#entrypoint): + +```python +from langgraph.checkpoint.memory import MemorySaver + +checkpointer = MemorySaver() + + +@entrypoint(checkpointer=checkpointer) +def graph(input_query): + result_1 = step_1(input_query).result() + result_2 = human_feedback(result_1).result() + result_3 = step_3(result_2).result() + + return result_3 +``` + +[interrupt()](../how-tos/human_in_the_loop/add-human-in-the-loop.md#pause-using-interrupt) is called inside a task, enabling a human to review and edit the output of the previous task. The results of prior tasks-- in this case `step_1`-- are persisted, so that they are not run again following the `interrupt`. + +Let's send in a query string: + +```python +config = {"configurable": {"thread_id": "1"}} + +for event in graph.stream("foo", config): + print(event) + print("\n") +``` + +Note that we've paused with an `interrupt` after `step_1`. The interrupt provides instructions to resume the run. To resume, we issue a [Command](../how-tos/human_in_the_loop/add-human-in-the-loop.md#resume-using-the-command-primitive) containing the data expected by the `human_feedback` task. + +```python +# Continue execution +for event in graph.stream(Command(resume="baz"), config): + print(event) + print("\n") +``` +After resuming, the run proceeds through the remaining step and terminates as expected. + +### Review tool calls + +To review tool calls before execution, we add a `review_tool_call` function that calls [`interrupt`](../how-tos/human_in_the_loop/add-human-in-the-loop.md#pause-using-interrupt). When this function is called, execution will be paused until we issue a command to resume it. + +Given a tool call, our function will `interrupt` for human review. At that point we can either: + +- Accept the tool call +- Revise the tool call and continue +- Generate a custom tool message (e.g., instructing the model to re-format its tool call) + +```python +from typing import Union + +def review_tool_call(tool_call: ToolCall) -> Union[ToolCall, ToolMessage]: + """Review a tool call, returning a validated version.""" + human_review = interrupt( + { + "question": "Is this correct?", + "tool_call": tool_call, + } + ) + review_action = human_review["action"] + review_data = human_review.get("data") + if review_action == "continue": + return tool_call + elif review_action == "update": + updated_tool_call = {**tool_call, **{"args": review_data}} + return updated_tool_call + elif review_action == "feedback": + return ToolMessage( + content=review_data, name=tool_call["name"], tool_call_id=tool_call["id"] + ) +``` + +We can now update our [entrypoint](../concepts/functional_api.md#entrypoint) to review the generated tool calls. If a tool call is accepted or revised, we execute in the same way as before. Otherwise, we just append the `ToolMessage` supplied by the human. The results of prior tasks — in this case the initial model call — are persisted, so that they are not run again following the `interrupt`. + +```python +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph.message import add_messages +from langgraph.types import Command, interrupt + + +checkpointer = MemorySaver() + + +@entrypoint(checkpointer=checkpointer) +def agent(messages, previous): + if previous is not None: + messages = add_messages(previous, messages) + + llm_response = call_model(messages).result() + while True: + if not llm_response.tool_calls: + break + + # Review tool calls + tool_results = [] + tool_calls = [] + for i, tool_call in enumerate(llm_response.tool_calls): + review = review_tool_call(tool_call) + if isinstance(review, ToolMessage): + tool_results.append(review) + else: # is a validated tool call + tool_calls.append(review) + if review != tool_call: + llm_response.tool_calls[i] = review # update message + + # Execute remaining tool calls + tool_result_futures = [call_tool(tool_call) for tool_call in tool_calls] + remaining_tool_results = [fut.result() for fut in tool_result_futures] + + # Append to message list + messages = add_messages( + messages, + [llm_response, *tool_results, *remaining_tool_results], + ) + + # Call model again + llm_response = call_model(messages).result() + + # Generate final response + messages = add_messages(messages, llm_response) + return entrypoint.final(value=llm_response, save=messages) +``` ## Short-term memory diff --git a/docs/docs/how-tos/wait-user-input-functional.ipynb b/docs/docs/how-tos/wait-user-input-functional.ipynb deleted file mode 100644 index 33279fd74..000000000 --- a/docs/docs/how-tos/wait-user-input-functional.ipynb +++ /dev/null @@ -1,561 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# How to wait for user input (Functional API)\n", - "\n", - "!!! info \"Prerequisites\"\n", - " This guide assumes familiarity with the following:\n", - "\n", - " - Implementing [human-in-the-loop](../../concepts/human_in_the_loop) workflows with [interrupt](../../concepts/human_in_the_loop/#interrupt)\n", - " - [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional)\n", - "\n", - "**Human-in-the-loop (HIL)** interactions are crucial for [agentic systems](../../concepts/agentic_concepts/#human-in-the-loop). Waiting for human input is a common HIL interaction pattern, allowing the agent to ask the user clarifying questions and await input before proceeding. \n", - "\n", - "We can implement this in LangGraph using the [interrupt()][langgraph.types.interrupt] function. `interrupt` allows us to stop graph execution to collect input from a user and continue execution with collected input.\n", - "\n", - "This guide demonstrates how to implement human-in-the-loop workflows using LangGraph's [Functional API](../../concepts/functional_api). Specifically, we will demonstrate:\n", - "\n", - "1. [A simple usage example](#simple-usage)\n", - "2. [How to use with a ReAct agent](#agent)\n", - "\n", - "## Setup\n", - "\n", - "First, let's install the required packages and set our API keys:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph langchain-openai" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "\n", - "def _set_env(var: str):\n", - " if not os.environ.get(var):\n", - " os.environ[var] = getpass.getpass(f\"{var}: \")\n", - "\n", - "\n", - "_set_env(\"OPENAI_API_KEY\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Set up LangSmith for better debugging

\n", - "

\n", - " Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM aps built with LangGraph — read more about how to get started in the docs. \n", - "

\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Simple usage\n", - "\n", - "Let's demonstrate a simple usage example. We will create three [tasks](../../concepts/functional_api/#task):\n", - "\n", - "1. Append `\"bar\"`.\n", - "2. Pause for human input. When resuming, append human input.\n", - "3. Append `\"qux\"`." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.func import entrypoint, task\n", - "from langgraph.types import Command, interrupt\n", - "\n", - "\n", - "@task\n", - "def step_1(input_query):\n", - " \"\"\"Append bar.\"\"\"\n", - " return f\"{input_query} bar\"\n", - "\n", - "\n", - "@task\n", - "def human_feedback(input_query):\n", - " \"\"\"Append user input.\"\"\"\n", - " feedback = interrupt(f\"Please provide feedback: {input_query}\")\n", - " return f\"{input_query} {feedback}\"\n", - "\n", - "\n", - "@task\n", - "def step_3(input_query):\n", - " \"\"\"Append qux.\"\"\"\n", - " return f\"{input_query} qux\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now compose these tasks in a simple [entrypoint](../../concepts/functional_api/#entrypoint):" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.checkpoint.memory import MemorySaver\n", - "\n", - "checkpointer = MemorySaver()\n", - "\n", - "\n", - "@entrypoint(checkpointer=checkpointer)\n", - "def graph(input_query):\n", - " result_1 = step_1(input_query).result()\n", - " result_2 = human_feedback(result_1).result()\n", - " result_3 = step_3(result_2).result()\n", - "\n", - " return result_3" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "All we have done to enable human-in-the-loop workflows is called [interrupt()](../../concepts/human_in_the_loop/#interrupt) inside a task.\n", - "\n", - "!!! tip\n", - "\n", - " The results of prior tasks-- in this case `step_1`-- are persisted, so that they are not run again following the `interrupt`.\n", - "\n", - "\n", - "Let's send in a query string:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"1\"}}" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'step_1': 'foo bar'}\n", - "\n", - "\n", - "{'__interrupt__': (Interrupt(value='Please provide feedback: foo bar', resumable=True, ns=['graph:d66b2e35-0ee3-d8d6-1a22-aec9d58f13b9', 'human_feedback:e0cd4ee2-b874-e1d2-8bc4-3f7ddc06bcc2'], when='during'),)}\n", - "\n", - "\n" - ] - } - ], - "source": [ - "for event in graph.stream(\"foo\", config):\n", - " print(event)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that we've paused with an `interrupt` after `step_1`. The interrupt provides instructions to resume the run. To resume, we issue a [Command](../../concepts/human_in_the_loop/#the-command-primitive) containing the data expected by the `human_feedback` task." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'human_feedback': 'foo bar baz'}\n", - "\n", - "\n", - "{'step_3': 'foo bar baz qux'}\n", - "\n", - "\n", - "{'graph': 'foo bar baz qux'}\n", - "\n", - "\n" - ] - } - ], - "source": [ - "# Continue execution\n", - "for event in graph.stream(Command(resume=\"baz\"), config):\n", - " print(event)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "After resuming, the run proceeds through the remaining step and terminates as expected." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Agent\n", - "\n", - "We will build off of the agent created in the [How to create a ReAct agent using the Functional API](../../how-tos/react-agent-from-scratch-functional) guide.\n", - "\n", - "Here we will extend the agent by allowing it to reach out to a human for assistance when needed.\n", - "\n", - "### Define model and tools\n", - "\n", - "Let's first define the tools and model we will use for our example. As in the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional), we will use a single place-holder tool that gets a description of the weather for a location.\n", - "\n", - "We will use an [OpenAI](https://python.langchain.com/docs/integrations/providers/openai/) chat model for this example, but any model [supporting tool-calling](https://python.langchain.com/docs/integrations/chat/) will suffice." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "from langchain_core.tools import tool\n", - "\n", - "model = ChatOpenAI(model=\"gpt-4o-mini\")\n", - "\n", - "\n", - "@tool\n", - "def get_weather(location: str):\n", - " \"\"\"Call to get the weather from a specific location.\"\"\"\n", - " # This is a placeholder for the actual implementation\n", - " if any([city in location.lower() for city in [\"sf\", \"san francisco\"]]):\n", - " return \"It's sunny!\"\n", - " elif \"boston\" in location.lower():\n", - " return \"It's rainy!\"\n", - " else:\n", - " return f\"I am not sure what the weather is in {location}\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To reach out to a human for assistance, we can simply add a tool that calls [interrupt](../../concepts/human_in_the_loop/#interrupt):" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.types import Command, interrupt\n", - "\n", - "\n", - "@tool\n", - "def human_assistance(query: str) -> str:\n", - " \"\"\"Request assistance from a human.\"\"\"\n", - " human_response = interrupt({\"query\": query})\n", - " return human_response[\"data\"]\n", - "\n", - "\n", - "tools = [get_weather, human_assistance]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Define tasks\n", - "\n", - "Our tasks are otherwise unchanged from the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional):\n", - "\n", - "1. **Call model**: We want to query our chat model with a list of messages.\n", - "2. **Call tool**: If our model generates tool calls, we want to execute them.\n", - "\n", - "We just have one more tool accessible to the model." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.messages import ToolMessage\n", - "from langgraph.func import entrypoint, task\n", - "\n", - "tools_by_name = {tool.name: tool for tool in tools}\n", - "\n", - "\n", - "@task\n", - "def call_model(messages):\n", - " \"\"\"Call model with a sequence of messages.\"\"\"\n", - " response = model.bind_tools(tools).invoke(messages)\n", - " return response\n", - "\n", - "\n", - "@task\n", - "def call_tool(tool_call):\n", - " tool = tools_by_name[tool_call[\"name\"]]\n", - " observation = tool.invoke(tool_call)\n", - " return ToolMessage(content=observation, tool_call_id=tool_call[\"id\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Define entrypoint\n", - "\n", - "Our [entrypoint](../../concepts/functional_api/#entrypoint) is also unchanged from the [ReAct agent guide](../../how-tos/react-agent-from-scratch-functional):" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.checkpoint.memory import MemorySaver\n", - "from langgraph.graph.message import add_messages\n", - "\n", - "checkpointer = MemorySaver()\n", - "\n", - "\n", - "@entrypoint(checkpointer=checkpointer)\n", - "def agent(messages, previous):\n", - " if previous is not None:\n", - " messages = add_messages(previous, messages)\n", - "\n", - " llm_response = call_model(messages).result()\n", - " while True:\n", - " if not llm_response.tool_calls:\n", - " break\n", - "\n", - " # Execute tools\n", - " tool_result_futures = [\n", - " call_tool(tool_call) for tool_call in llm_response.tool_calls\n", - " ]\n", - " tool_results = [fut.result() for fut in tool_result_futures]\n", - "\n", - " # Append to message list\n", - " messages = add_messages(messages, [llm_response, *tool_results])\n", - "\n", - " # Call model again\n", - " llm_response = call_model(messages).result()\n", - "\n", - " # Generate final response\n", - " messages = add_messages(messages, llm_response)\n", - " return entrypoint.final(value=llm_response, save=messages)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Usage\n", - "\n", - "Let's invoke our model with a question that requires human assistance. Our question will also require an invocation of the `get_weather` tool:" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "def _print_step(step: dict) -> None:\n", - " for task_name, result in step.items():\n", - " if task_name == \"agent\":\n", - " continue # just stream from tasks\n", - " print(f\"\\n{task_name}:\")\n", - " if task_name == \"__interrupt__\":\n", - " print(result)\n", - " else:\n", - " result.pretty_print()" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "config = {\"configurable\": {\"thread_id\": \"1\"}}" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'role': 'user', 'content': 'Can you reach out for human assistance: what should I feed my cat? Separately, can you check the weather in San Francisco?'}\n", - "\n", - "call_model:\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "Tool Calls:\n", - " human_assistance (call_joAEBVX7Abfm7TsZ0k95ZkVx)\n", - " Call ID: call_joAEBVX7Abfm7TsZ0k95ZkVx\n", - " Args:\n", - " query: What should I feed my cat?\n", - " get_weather (call_ut7zfHFCcms63BOZLrRHszGH)\n", - " Call ID: call_ut7zfHFCcms63BOZLrRHszGH\n", - " Args:\n", - " location: San Francisco\n", - "\n", - "call_tool:\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "\n", - "content=\"It's sunny!\" name='get_weather' tool_call_id='call_ut7zfHFCcms63BOZLrRHszGH'\n", - "\n", - "__interrupt__:\n", - "(Interrupt(value={'query': 'What should I feed my cat?'}, resumable=True, ns=['agent:aa676ccc-b038-25e3-9c8a-18e81d4e1372', 'call_tool:059d53d2-3344-13bc-e170-48b632c2dd97'], when='during'),)\n" - ] - } - ], - "source": [ - "user_message = {\n", - " \"role\": \"user\",\n", - " \"content\": (\n", - " \"Can you reach out for human assistance: what should I feed my cat? \"\n", - " \"Separately, can you check the weather in San Francisco?\"\n", - " ),\n", - "}\n", - "print(user_message)\n", - "\n", - "for step in agent.stream([user_message], config):\n", - " _print_step(step)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that we generate two tool calls, and although our run is interrupted, we did not block the execution of the `get_weather` tool.\n", - "\n", - "Let's inspect where we're interrupted:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'__interrupt__': (Interrupt(value={'query': 'What should I feed my cat?'}, resumable=True, ns=['agent:aa676ccc-b038-25e3-9c8a-18e81d4e1372', 'call_tool:059d53d2-3344-13bc-e170-48b632c2dd97'], when='during'),)}\n" - ] - } - ], - "source": [ - "print(step)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can resume execution by issuing a [Command](../../concepts/human_in_the_loop/#the-command-primitive). Note that the data we supply in the `Command` can be customized to your needs based on the implementation of `human_assistance`." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "call_tool:\n", - "=================================\u001b[1m Tool Message \u001b[0m=================================\n", - "\n", - "content='You should feed your cat a fish.' name='human_assistance' tool_call_id='call_joAEBVX7Abfm7TsZ0k95ZkVx'\n", - "\n", - "call_model:\n", - "==================================\u001b[1m Ai Message \u001b[0m==================================\n", - "\n", - "For human assistance, you should feed your cat fish. \n", - "\n", - "Regarding the weather in San Francisco, it's sunny!\n" - ] - } - ], - "source": [ - "human_response = \"You should feed your cat a fish.\"\n", - "human_command = Command(resume={\"data\": human_response})\n", - "\n", - "for step in agent.stream(human_command, config):\n", - " _print_step(step)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Above, when we resume we provide the final tool message, allowing the model to generate its response. Check out the LangSmith traces to see a full breakdown of the runs:\n", - "\n", - "1. [Trace from initial query](https://smith.langchain.com/public/c3d8879d-4d01-41be-807e-6d9eed15df99/r)\n", - "2. [Trace after resuming](https://smith.langchain.com/public/97c05ef9-8b4c-428e-8826-3fd417c8c75f/r)" - ] - } - ], - "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.12.3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/docs/tutorials/get-started/4-human-in-the-loop.md b/docs/docs/tutorials/get-started/4-human-in-the-loop.md index de515c351..6327a0788 100644 --- a/docs/docs/tutorials/get-started/4-human-in-the-loop.md +++ b/docs/docs/tutorials/get-started/4-human-in-the-loop.md @@ -78,7 +78,7 @@ graph_builder.add_edge(START, "chatbot") !!! tip - For more information and examples of human-in-the-loop workflows, see [Human-in-the-loop](../../concepts/human_in_the_loop.md). This includes how to [review and edit tool calls](../../how-tos/human_in_the_loop/review-tool-calls.ipynb) before they are executed. + For more information and examples of human-in-the-loop workflows, see [Human-in-the-loop](../../concepts/human_in_the_loop.md). ## 2. Compile the graph diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 3fc89c979..a10204556 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -157,8 +157,7 @@ nav: - Memory: - Add memory: how-tos/memory/add-memory.md - Human-in-the-loop: - - Add to agent: agents/human-in-the-loop.md - - Add to workflow: how-tos/human_in_the_loop/add-human-in-the-loop.md + - how-tos/human_in_the_loop/add-human-in-the-loop.md - Use Server API: cloud/how-tos/add-human-in-the-loop.md - Time travel: - Use Server API: cloud/how-tos/human_in_the_loop_time_travel.md