Human-in-the-loop is not working. #956

Closed
opened 2026-02-20 17:42:31 -05:00 by yindo · 1 comment
Owner

Originally created by @spcBackToLife on GitHub (Sep 1, 2025).

Checked other resources

  • This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
  • I added a clear and detailed title that summarizes the issue.
  • I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
  • I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.

Example Code

graph.py

"""Graphs that extract memories on a schedule."""

import asyncio
import logging
from datetime import datetime
from typing import cast

from langchain.chat_models import init_chat_model
from langgraph.graph import END, StateGraph
from langgraph.runtime import Runtime
from langgraph.store.base import BaseStore

from memory_agent import tools, utils
from memory_agent.context import Context
from memory_agent.human_loop import add_human_in_the_loop
from memory_agent.state import State


logger = logging.getLogger(__name__)

# Initialize the language model to be used for memory extraction
llm = init_chat_model(model="gpt-4o-mini")


async def call_model(state: State, runtime: Runtime[Context]) -> dict:
    """Extract the user's state from the conversation and update the memory."""
    user_id = runtime.context.user_id
    model = runtime.context.model
    system_prompt = runtime.context.system_prompt

    # Retrieve the most recent memories for context
    memories = await cast(BaseStore, runtime.store).asearch(
        ("memories", user_id),
        query=str([m.content for m in state.messages[-3:]]),
        limit=10,
    )

    # Format memories for inclusion in the prompt
    formatted = "\n".join(
        f"[{mem.key}]: {mem.value} (similarity: {mem.score})" for mem in memories
    )
    if formatted:
        formatted = f"""
<memories>
{formatted}
</memories>"""

    # Prepare the system prompt with user memories and current time
    # This helps the model understand the context and temporal relevance
    sys = system_prompt.format(user_info=formatted, time=datetime.now().isoformat())

    # Invoke the language model with the prepared prompt and tools
    # "bind_tools" gives the LLM the JSON schema for all tools in the list so it knows how
    # to use them.
    msg = await llm.bind_tools(
        [tools.upsert_memory, add_human_in_the_loop(tools.book_hotel)]
    ).ainvoke(
        [{"role": "system", "content": sys}, *state.messages],
        # context=utils.split_model_and_provider(model),
    )
    return {"messages": [msg]}


async def process_tools(state: State, runtime: Runtime[Context]):
    # Extract tool calls from the last message
    tool_calls = getattr(state.messages[-1], "tool_calls", [])

    # Process tool calls based on their type
    async def process_tool_call(tc):
        tool_name = tc["name"]
        args = tc["args"]

        if tool_name == "upsert_memory":
            return await tools.upsert_memory(
                **args,
                user_id=runtime.context.user_id,
                store=cast(BaseStore, runtime.store),
            )
        elif tool_name == "human_assistance":
            return tools.human_assistance(**args)
        elif tool_name == "book_hotel":
            return tools.book_hotel(**args)
        else:
            raise ValueError(f"Unknown tool: {tool_name}")

    # Concurrently execute all tool calls
    saved_memories = await asyncio.gather(*(process_tool_call(tc) for tc in tool_calls))

    # Format the results of tool operations
    # This provides confirmation to the model that the actions it took were completed
    results = [
        {
            "role": "tool",
            "content": mem,
            "tool_call_id": tc["id"],
        }
        for tc, mem in zip(tool_calls, saved_memories)
    ]
    return {"messages": results}


def route_message(state: State):
    """Determine the next step based on the presence of tool calls."""
    msg = state.messages[-1]
    if getattr(msg, "tool_calls", None):
        # If there are tool calls, we need to process them
        return "process_tools"
    # Otherwise, finish; user can send the next message
    return END


# Create the graph + all nodes
builder = StateGraph(State, context_schema=Context)

# Define the flow of the memory extraction process
builder.add_node(call_model)
builder.add_edge("__start__", "call_model")
builder.add_node(process_tools)
builder.add_conditional_edges("call_model", route_message, ["process_tools", END])
# Right now, we're returning control to the user after processing tools
# Depending on the model, you may want to route back to the model
# to let it first process tools, then generate a response
builder.add_edge("process_tools", "call_model")
graph = builder.compile()
graph.name = "MemoryAgent"


__all__ = ["graph"]

human_loop.py

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(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",
        }
        print("toollllllllasd")
        response = interrupt([request])[0]
        # 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


tools.py
"""Define he agent's tools."""

import uuid
from typing import Annotated, Optional
from langgraph.types import Command, interrupt
from langchain_core.tools import InjectedToolArg
from langgraph.store.base import BaseStore


def human_assistance(query: str) -> str:
    """Request assistance from a human operator when you need help or cannot provide the requested service.

    Use this tool when:
    - The user asks for something outside your capabilities
    - You need clarification on complex requests
    - The user requests technical support or account help
    - You encounter sensitive topics requiring human judgment
    - The user asks for real-time information you don't have access to
    - You need to escalate the conversation for any reason

    Args:
        query: A clear description of what you need help with or why you're escalating
    """
    human_response = interrupt({"query": query})
    return human_response["data"]


def book_hotel(hotel_name: str):
    """Book a hotel"""
    return f"Successfully booked a stay at {hotel_name}."


async def upsert_memory(
    content: str,
    context: str,
    *,
    memory_id: Optional[uuid.UUID] = None,
    # Hide these arguments from the model.
    user_id: Annotated[str, InjectedToolArg],
    store: Annotated[BaseStore, InjectedToolArg],
):
    """Save or update important information about the user in the database.

    Use this tool to remember:
    - User preferences and interests
    - Personal information they've shared
    - Important context from conversations
    - User's goals or plans
    - Any information that would be useful for future interactions

    If a memory conflicts with an existing one, then just UPDATE the
    existing one by passing in memory_id - don't create two memories
    that are the same. If the user corrects a memory, UPDATE it.

    Args:
        content: The main content of the memory. For example:
            "User expressed interest in learning about French."
        context: Additional context for the memory. For example:
            "This was mentioned while discussing career options in Europe."
        memory_id: ONLY PROVIDE IF UPDATING AN EXISTING MEMORY.
        The memory to overwrite.
    """
    mem_id = memory_id or uuid.uuid4()
    await store.aput(
        ("memories", user_id),
        key=str(mem_id),
        value={"content": content, "context": context},
    )
    return f"Stored memory {mem_id}"

Error Message and Stack Trace (if applicable)

There are no errors; the frontend (https://github.com/langchain-ai/agent-chat-ui) just isn’t displaying the user confirmation interaction component.

Description

Desc

I followed the guide: https://langchain-ai.github.io/langgraph/agents/ui/ to implement the flow, but the docs mention using InMemorySaver. When I start with langgraph dev, it says no additional configuration is needed, yet it still doesn’t work. The actual behavior is just a simple tool-call interaction. Could you show me the code from the “add human-in-the-loop” video in that doc? I can’t find an example. This is really frustrating.

Requirements:

I want the code from the “add human-in-the-loop” video in the documentation above, specifically from here: https://langchain-ai.github.io/langgraph/agents/ui/

System Info

python -m langchain_core.sys_info

System Information

OS: Darwin
OS Version: Darwin Kernel Version 23.6.0: Mon Jul 29 21:13:04 PDT 2024; root:xnu-10063.141.2~1/RELEASE_ARM64_T6020
Python Version: 3.12.8 (main, Jan 29 2025, 22:24:28) [Clang 14.0.3 (clang-1403.0.22.14.1)]

Package Information

langchain_core: 0.3.75
langchain: 0.3.20
langchain_community: 0.3.19
langsmith: 0.3.45
langchain_anthropic: 0.3.3
langchain_experimental: 0.3.4
langchain_ollama: 0.2.2
langchain_openai: 0.3.1
langchain_text_splitters: 0.3.6
langgraph_api: 0.4.7
langgraph_cli: 0.4.0
langgraph_license: Installed. No version info available.
langgraph_runtime: Installed. No version info available.
langgraph_runtime_inmem: 0.9.0
langgraph_sdk: 0.2.4

Optional packages not installed

langserve

Other Dependencies

aiohttp<4.0.0,>=3.8.3: Installed. No version info available.
anthropic: 0.49.0
async-timeout<5.0.0,>=4.0.0;: Installed. No version info available.
blockbuster<2.0.0,>=1.5.24: Installed. No version info available.
click>=8.1.7: Installed. No version info available.
cloudpickle>=3.0.0: Installed. No version info available.
cryptography<45.0,>=42.0.0: Installed. No version info available.
dataclasses-json<0.7,>=0.5.7: Installed. No version info available.
defusedxml: 0.7.1
httpx: 0.28.1
httpx-sse<1.0.0,>=0.4.0: Installed. No version info available.
httpx>=0.25.0: Installed. No version info available.
httpx>=0.25.2: Installed. No version info available.
jsonpatch<2.0,>=1.33: Installed. No version info available.
jsonschema-rs<0.30,>=0.20.0: Installed. No version info available.
langchain-anthropic;: Installed. No version info available.
langchain-aws;: Installed. No version info available.
langchain-cohere;: Installed. No version info available.
langchain-community;: Installed. No version info available.
langchain-core<1.0.0,>=0.3.34: Installed. No version info available.
langchain-core<1.0.0,>=0.3.41: Installed. No version info available.
langchain-core>=0.3.64: Installed. No version info available.
langchain-deepseek;: Installed. No version info available.
langchain-fireworks;: Installed. No version info available.
langchain-google-genai;: Installed. No version info available.
langchain-google-vertexai;: Installed. No version info available.
langchain-groq;: Installed. No version info available.
langchain-huggingface;: Installed. No version info available.
langchain-mistralai;: Installed. No version info available.
langchain-ollama;: Installed. No version info available.
langchain-openai;: Installed. No version info available.
langchain-text-splitters<1.0.0,>=0.3.6: Installed. No version info available.
langchain-together;: Installed. No version info available.
langchain-xai;: Installed. No version info available.
langchain<1.0.0,>=0.3.20: Installed. No version info available.
langgraph-api<0.5.0,>=0.3;: Installed. No version info available.
langgraph-checkpoint>=2.0.23: Installed. No version info available.
langgraph-checkpoint>=2.0.25: Installed. No version info available.
langgraph-runtime-inmem<0.10.0,>=0.9.0: Installed. No version info available.
langgraph-runtime-inmem>=0.7;: Installed. No version info available.
langgraph-sdk>=0.1.0;: Installed. No version info available.
langgraph-sdk>=0.2.0: Installed. No version info available.
langgraph>=0.2: Installed. No version info available.
langgraph>=0.4.0: Installed. No version info available.
langsmith-pyo3: Installed. No version info available.
langsmith<0.4,>=0.1.125: Installed. No version info available.
langsmith<0.4,>=0.1.17: Installed. No version info available.
langsmith>=0.3.45: Installed. No version info available.
numpy<3,>=1.26.2: Installed. No version info available.
ollama: 0.4.7
openai: 1.72.0
openai-agents: Installed. No version info available.
opentelemetry-api: 1.34.0
opentelemetry-exporter-otlp-proto-http: 1.34.0
opentelemetry-sdk: 1.34.0
orjson: 3.10.15
orjson>=3.10.1: Installed. No version info available.
orjson>=3.9.7: Installed. No version info available.
packaging: 24.2
packaging>=23.2: Installed. No version info available.
pydantic: 2.10.6
pydantic-settings<3.0.0,>=2.4.0: Installed. No version info available.
pydantic<3.0.0,>=2.7.4: Installed. No version info available.
pydantic>=2.7.4: Installed. No version info available.
pyjwt>=2.9.0: Installed. No version info available.
pytest: 8.3.3
python-dotenv>=0.8.0;: Installed. No version info available.
PyYAML>=5.3: Installed. No version info available.
requests: 2.32.3
requests-toolbelt: 1.0.0
requests<3,>=2: Installed. No version info available.
rich: 13.9.4
SQLAlchemy<3,>=1.4: Installed. No version info available.
sse-starlette<2.2.0,>=2.1.0: Installed. No version info available.
sse-starlette>=2: Installed. No version info available.
starlette>=0.37: Installed. No version info available.
starlette>=0.38.6: Installed. No version info available.
structlog<26,>=24.1.0: Installed. No version info available.
structlog>23: Installed. No version info available.
tenacity!=8.4.0,<10,>=8.1.0: Installed. No version info available.
tenacity!=8.4.0,<10.0.0,>=8.1.0: Installed. No version info available.
tenacity>=8.0.0: Installed. No version info available.
tiktoken: 0.9.0
truststore>=0.1: Installed. No version info available.
typing-extensions>=4.7: Installed. No version info available.
uvicorn>=0.26.0: Installed. No version info available.
watchfiles>=0.13: Installed. No version info available.
zstandard: 0.23.0

Originally created by @spcBackToLife on GitHub (Sep 1, 2025). ### Checked other resources - [x] This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/). - [x] I added a clear and detailed title that summarizes the issue. - [x] I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example). - [x] I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue. ### Example Code ```python graph.py """Graphs that extract memories on a schedule.""" import asyncio import logging from datetime import datetime from typing import cast from langchain.chat_models import init_chat_model from langgraph.graph import END, StateGraph from langgraph.runtime import Runtime from langgraph.store.base import BaseStore from memory_agent import tools, utils from memory_agent.context import Context from memory_agent.human_loop import add_human_in_the_loop from memory_agent.state import State logger = logging.getLogger(__name__) # Initialize the language model to be used for memory extraction llm = init_chat_model(model="gpt-4o-mini") async def call_model(state: State, runtime: Runtime[Context]) -> dict: """Extract the user's state from the conversation and update the memory.""" user_id = runtime.context.user_id model = runtime.context.model system_prompt = runtime.context.system_prompt # Retrieve the most recent memories for context memories = await cast(BaseStore, runtime.store).asearch( ("memories", user_id), query=str([m.content for m in state.messages[-3:]]), limit=10, ) # Format memories for inclusion in the prompt formatted = "\n".join( f"[{mem.key}]: {mem.value} (similarity: {mem.score})" for mem in memories ) if formatted: formatted = f""" <memories> {formatted} </memories>""" # Prepare the system prompt with user memories and current time # This helps the model understand the context and temporal relevance sys = system_prompt.format(user_info=formatted, time=datetime.now().isoformat()) # Invoke the language model with the prepared prompt and tools # "bind_tools" gives the LLM the JSON schema for all tools in the list so it knows how # to use them. msg = await llm.bind_tools( [tools.upsert_memory, add_human_in_the_loop(tools.book_hotel)] ).ainvoke( [{"role": "system", "content": sys}, *state.messages], # context=utils.split_model_and_provider(model), ) return {"messages": [msg]} async def process_tools(state: State, runtime: Runtime[Context]): # Extract tool calls from the last message tool_calls = getattr(state.messages[-1], "tool_calls", []) # Process tool calls based on their type async def process_tool_call(tc): tool_name = tc["name"] args = tc["args"] if tool_name == "upsert_memory": return await tools.upsert_memory( **args, user_id=runtime.context.user_id, store=cast(BaseStore, runtime.store), ) elif tool_name == "human_assistance": return tools.human_assistance(**args) elif tool_name == "book_hotel": return tools.book_hotel(**args) else: raise ValueError(f"Unknown tool: {tool_name}") # Concurrently execute all tool calls saved_memories = await asyncio.gather(*(process_tool_call(tc) for tc in tool_calls)) # Format the results of tool operations # This provides confirmation to the model that the actions it took were completed results = [ { "role": "tool", "content": mem, "tool_call_id": tc["id"], } for tc, mem in zip(tool_calls, saved_memories) ] return {"messages": results} def route_message(state: State): """Determine the next step based on the presence of tool calls.""" msg = state.messages[-1] if getattr(msg, "tool_calls", None): # If there are tool calls, we need to process them return "process_tools" # Otherwise, finish; user can send the next message return END # Create the graph + all nodes builder = StateGraph(State, context_schema=Context) # Define the flow of the memory extraction process builder.add_node(call_model) builder.add_edge("__start__", "call_model") builder.add_node(process_tools) builder.add_conditional_edges("call_model", route_message, ["process_tools", END]) # Right now, we're returning control to the user after processing tools # Depending on the model, you may want to route back to the model # to let it first process tools, then generate a response builder.add_edge("process_tools", "call_model") graph = builder.compile() graph.name = "MemoryAgent" __all__ = ["graph"] human_loop.py 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(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", } print("toollllllllasd") response = interrupt([request])[0] # 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 tools.py """Define he agent's tools.""" import uuid from typing import Annotated, Optional from langgraph.types import Command, interrupt from langchain_core.tools import InjectedToolArg from langgraph.store.base import BaseStore def human_assistance(query: str) -> str: """Request assistance from a human operator when you need help or cannot provide the requested service. Use this tool when: - The user asks for something outside your capabilities - You need clarification on complex requests - The user requests technical support or account help - You encounter sensitive topics requiring human judgment - The user asks for real-time information you don't have access to - You need to escalate the conversation for any reason Args: query: A clear description of what you need help with or why you're escalating """ human_response = interrupt({"query": query}) return human_response["data"] def book_hotel(hotel_name: str): """Book a hotel""" return f"Successfully booked a stay at {hotel_name}." async def upsert_memory( content: str, context: str, *, memory_id: Optional[uuid.UUID] = None, # Hide these arguments from the model. user_id: Annotated[str, InjectedToolArg], store: Annotated[BaseStore, InjectedToolArg], ): """Save or update important information about the user in the database. Use this tool to remember: - User preferences and interests - Personal information they've shared - Important context from conversations - User's goals or plans - Any information that would be useful for future interactions If a memory conflicts with an existing one, then just UPDATE the existing one by passing in memory_id - don't create two memories that are the same. If the user corrects a memory, UPDATE it. Args: content: The main content of the memory. For example: "User expressed interest in learning about French." context: Additional context for the memory. For example: "This was mentioned while discussing career options in Europe." memory_id: ONLY PROVIDE IF UPDATING AN EXISTING MEMORY. The memory to overwrite. """ mem_id = memory_id or uuid.uuid4() await store.aput( ("memories", user_id), key=str(mem_id), value={"content": content, "context": context}, ) return f"Stored memory {mem_id}" ``` ### Error Message and Stack Trace (if applicable) ```shell There are no errors; the frontend (https://github.com/langchain-ai/agent-chat-ui) just isn’t displaying the user confirmation interaction component. ``` ### Description # Desc I followed the guide: https://langchain-ai.github.io/langgraph/agents/ui/ to implement the flow, but the docs mention using InMemorySaver. When I start with langgraph dev, it says no additional configuration is needed, yet it still doesn’t work. The actual behavior is just a simple tool-call interaction. Could you show me the code from the “add human-in-the-loop” video in that doc? I can’t find an example. This is really frustrating. # Requirements: I want the code from the “add human-in-the-loop” video in the documentation above, specifically from here: https://langchain-ai.github.io/langgraph/agents/ui/ ### System Info python -m langchain_core.sys_info System Information ------------------ > OS: Darwin > OS Version: Darwin Kernel Version 23.6.0: Mon Jul 29 21:13:04 PDT 2024; root:xnu-10063.141.2~1/RELEASE_ARM64_T6020 > Python Version: 3.12.8 (main, Jan 29 2025, 22:24:28) [Clang 14.0.3 (clang-1403.0.22.14.1)] Package Information ------------------- > langchain_core: 0.3.75 > langchain: 0.3.20 > langchain_community: 0.3.19 > langsmith: 0.3.45 > langchain_anthropic: 0.3.3 > langchain_experimental: 0.3.4 > langchain_ollama: 0.2.2 > langchain_openai: 0.3.1 > langchain_text_splitters: 0.3.6 > langgraph_api: 0.4.7 > langgraph_cli: 0.4.0 > langgraph_license: Installed. No version info available. > langgraph_runtime: Installed. No version info available. > langgraph_runtime_inmem: 0.9.0 > langgraph_sdk: 0.2.4 Optional packages not installed ------------------------------- > langserve Other Dependencies ------------------ > aiohttp<4.0.0,>=3.8.3: Installed. No version info available. > anthropic: 0.49.0 > async-timeout<5.0.0,>=4.0.0;: Installed. No version info available. > blockbuster<2.0.0,>=1.5.24: Installed. No version info available. > click>=8.1.7: Installed. No version info available. > cloudpickle>=3.0.0: Installed. No version info available. > cryptography<45.0,>=42.0.0: Installed. No version info available. > dataclasses-json<0.7,>=0.5.7: Installed. No version info available. > defusedxml: 0.7.1 > httpx: 0.28.1 > httpx-sse<1.0.0,>=0.4.0: Installed. No version info available. > httpx>=0.25.0: Installed. No version info available. > httpx>=0.25.2: Installed. No version info available. > jsonpatch<2.0,>=1.33: Installed. No version info available. > jsonschema-rs<0.30,>=0.20.0: Installed. No version info available. > langchain-anthropic;: Installed. No version info available. > langchain-aws;: Installed. No version info available. > langchain-cohere;: Installed. No version info available. > langchain-community;: Installed. No version info available. > langchain-core<1.0.0,>=0.3.34: Installed. No version info available. > langchain-core<1.0.0,>=0.3.41: Installed. No version info available. > langchain-core>=0.3.64: Installed. No version info available. > langchain-deepseek;: Installed. No version info available. > langchain-fireworks;: Installed. No version info available. > langchain-google-genai;: Installed. No version info available. > langchain-google-vertexai;: Installed. No version info available. > langchain-groq;: Installed. No version info available. > langchain-huggingface;: Installed. No version info available. > langchain-mistralai;: Installed. No version info available. > langchain-ollama;: Installed. No version info available. > langchain-openai;: Installed. No version info available. > langchain-text-splitters<1.0.0,>=0.3.6: Installed. No version info available. > langchain-together;: Installed. No version info available. > langchain-xai;: Installed. No version info available. > langchain<1.0.0,>=0.3.20: Installed. No version info available. > langgraph-api<0.5.0,>=0.3;: Installed. No version info available. > langgraph-checkpoint>=2.0.23: Installed. No version info available. > langgraph-checkpoint>=2.0.25: Installed. No version info available. > langgraph-runtime-inmem<0.10.0,>=0.9.0: Installed. No version info available. > langgraph-runtime-inmem>=0.7;: Installed. No version info available. > langgraph-sdk>=0.1.0;: Installed. No version info available. > langgraph-sdk>=0.2.0: Installed. No version info available. > langgraph>=0.2: Installed. No version info available. > langgraph>=0.4.0: Installed. No version info available. > langsmith-pyo3: Installed. No version info available. > langsmith<0.4,>=0.1.125: Installed. No version info available. > langsmith<0.4,>=0.1.17: Installed. No version info available. > langsmith>=0.3.45: Installed. No version info available. > numpy<3,>=1.26.2: Installed. No version info available. > ollama: 0.4.7 > openai: 1.72.0 > openai-agents: Installed. No version info available. > opentelemetry-api: 1.34.0 > opentelemetry-exporter-otlp-proto-http: 1.34.0 > opentelemetry-sdk: 1.34.0 > orjson: 3.10.15 > orjson>=3.10.1: Installed. No version info available. > orjson>=3.9.7: Installed. No version info available. > packaging: 24.2 > packaging>=23.2: Installed. No version info available. > pydantic: 2.10.6 > pydantic-settings<3.0.0,>=2.4.0: Installed. No version info available. > pydantic<3.0.0,>=2.7.4: Installed. No version info available. > pydantic>=2.7.4: Installed. No version info available. > pyjwt>=2.9.0: Installed. No version info available. > pytest: 8.3.3 > python-dotenv>=0.8.0;: Installed. No version info available. > PyYAML>=5.3: Installed. No version info available. > requests: 2.32.3 > requests-toolbelt: 1.0.0 > requests<3,>=2: Installed. No version info available. > rich: 13.9.4 > SQLAlchemy<3,>=1.4: Installed. No version info available. > sse-starlette<2.2.0,>=2.1.0: Installed. No version info available. > sse-starlette>=2: Installed. No version info available. > starlette>=0.37: Installed. No version info available. > starlette>=0.38.6: Installed. No version info available. > structlog<26,>=24.1.0: Installed. No version info available. > structlog>23: Installed. No version info available. > tenacity!=8.4.0,<10,>=8.1.0: Installed. No version info available. > tenacity!=8.4.0,<10.0.0,>=8.1.0: Installed. No version info available. > tenacity>=8.0.0: Installed. No version info available. > tiktoken: 0.9.0 > truststore>=0.1: Installed. No version info available. > typing-extensions>=4.7: Installed. No version info available. > uvicorn>=0.26.0: Installed. No version info available. > watchfiles>=0.13: Installed. No version info available. > zstandard: 0.23.0
yindo added the bugpending labels 2026-02-20 17:42:31 -05:00
yindo closed this issue 2026-02-20 17:42:31 -05:00
Author
Owner

@casparb commented on GitHub (Sep 9, 2025):

Hey @spcBackToLife, sorry that you're having trouble. If you've deployed your graph on LangGraph Platform or on a LangGraph in-memory server (via langgraph dev), a checkpointer is automatically injected and you won't need to configure one yourself. If you've deployed it elsewhere and want HITL, you will need to configure one:

from langgraph.checkpoint.memory import InMemorySaver

graph.compile(checkpointer=InMemorySaver())

Ref

I wasn't able to run your example because I don't have the full code (missing memory_agent.context). However I was able to get HITL working in this minimal example, which I spun up with langgraph dev and interfaced with via the deployed Agent UI site:

graph.py
from __future__ import annotations

from typing import Annotated, TypedDict, Callable

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool, BaseTool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.types import Command, interrupt

def wrap_with_hitl(t: Callable | BaseTool) -> BaseTool:
    if not isinstance(t, BaseTool):
        t = tool(t)

    @tool(t.name, description=t.description, args_schema=t.args_schema)
    def _wrapped(**tool_input):
        request = {
            "action_request": {"action": t.name, "args": tool_input},
            "config": {
                "allow_accept": True,
                "allow_edit": True,
                "allow_respond": True,
                "allow_ignore": False,
            },
            "description": "Please review the tool call before execution.",
        }
        response = interrupt([request])[0]

        rtype = response.get("type")
        if rtype == "accept":
            return t.invoke(tool_input)
        elif rtype == "edit":
            edited = response.get("args", {})
            edited_args = edited.get("args", {})
            return t.invoke(edited_args)
        # ...

    return _wrapped


@tool
def book_hotel(hotel_name: str) -> str:
    """Book a hotel (demo)."""
    return f"Successfully booked a stay at {hotel_name}."

book_hotel_hitl = wrap_with_hitl(book_hotel)


class State(TypedDict):
    messages: Annotated[list, add_messages]


def build_graph():
    llm = init_chat_model("openai:gpt-4o-mini")
    tools = [book_hotel_hitl]
    llm_with_tools = llm.bind_tools(tools)

    def chatbot(state: State):
        sys = {
            "role": "system",
            "content": (
                "You are a test agent. ALWAYS call the `book_hotel` tool with"
                "hotel_name='Demo Hotel'."
            ),
        }
        message = llm_with_tools.invoke([sys, *state["messages"]])

        return {"messages": [message]}

    g = StateGraph(State)
    g.add_node("chatbot", chatbot)
    g.add_node("tools", ToolNode([book_hotel_hitl]))

    g.add_edge(START, "chatbot")
    g.add_conditional_edges("chatbot", tools_condition)
    g.add_edge("tools", "chatbot")

    return g.compile()

graph = build_graph()

Finally, if you're hosting Agent Chat UI locally, make sure to pull the latest from the repository. There was a recent fix to HITL UI components not appearing unless you refreshed the chat window: langchain-ai/agent-chat-ui#174

Closing for now, if you're still having problems then reply here and I'll re-open.

@casparb commented on GitHub (Sep 9, 2025): Hey @spcBackToLife, sorry that you're having trouble. If you've deployed your graph on LangGraph Platform or on a LangGraph in-memory server (via `langgraph dev`), a checkpointer is automatically injected and you won't need to configure one yourself. If you've deployed it elsewhere and want HITL, you will need to configure one: ```python from langgraph.checkpoint.memory import InMemorySaver graph.compile(checkpointer=InMemorySaver()) ``` [Ref](https://langchain-ai.github.io/langgraph/concepts/persistence/#using-in-langgraph) I wasn't able to run your example because I don't have the full code (missing `memory_agent.context`). However I was able to get HITL working in this minimal example, which I spun up with `langgraph dev` and interfaced with via the [deployed Agent UI site](https://agentchat.vercel.app/): <details> <summary>graph.py</summary> ```python from __future__ import annotations from typing import Annotated, TypedDict, Callable from langchain.chat_models import init_chat_model from langchain_core.tools import tool, BaseTool from langgraph.checkpoint.memory import InMemorySaver from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages from langgraph.prebuilt import ToolNode, tools_condition from langgraph.types import Command, interrupt def wrap_with_hitl(t: Callable | BaseTool) -> BaseTool: if not isinstance(t, BaseTool): t = tool(t) @tool(t.name, description=t.description, args_schema=t.args_schema) def _wrapped(**tool_input): request = { "action_request": {"action": t.name, "args": tool_input}, "config": { "allow_accept": True, "allow_edit": True, "allow_respond": True, "allow_ignore": False, }, "description": "Please review the tool call before execution.", } response = interrupt([request])[0] rtype = response.get("type") if rtype == "accept": return t.invoke(tool_input) elif rtype == "edit": edited = response.get("args", {}) edited_args = edited.get("args", {}) return t.invoke(edited_args) # ... return _wrapped @tool def book_hotel(hotel_name: str) -> str: """Book a hotel (demo).""" return f"Successfully booked a stay at {hotel_name}." book_hotel_hitl = wrap_with_hitl(book_hotel) class State(TypedDict): messages: Annotated[list, add_messages] def build_graph(): llm = init_chat_model("openai:gpt-4o-mini") tools = [book_hotel_hitl] llm_with_tools = llm.bind_tools(tools) def chatbot(state: State): sys = { "role": "system", "content": ( "You are a test agent. ALWAYS call the `book_hotel` tool with" "hotel_name='Demo Hotel'." ), } message = llm_with_tools.invoke([sys, *state["messages"]]) return {"messages": [message]} g = StateGraph(State) g.add_node("chatbot", chatbot) g.add_node("tools", ToolNode([book_hotel_hitl])) g.add_edge(START, "chatbot") g.add_conditional_edges("chatbot", tools_condition) g.add_edge("tools", "chatbot") return g.compile() graph = build_graph() ``` </details> Finally, if you're hosting Agent Chat UI locally, make sure to pull the latest from the repository. There was a recent fix to HITL UI components not appearing unless you refreshed the chat window: langchain-ai/agent-chat-ui#174 Closing for now, if you're still having problems then reply here and I'll re-open.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#956