From cc97fad7e5ab245f309e3e64f7bc6191b112459a Mon Sep 17 00:00:00 2001 From: Harrison Chase Date: Tue, 26 Aug 2025 20:26:34 -0700 Subject: [PATCH] cr --- .../agent/middleware/human_in_the_loop.py | 99 ++++++++++++++++++- .../langgraph/agent/middleware/swarm.py | 13 +++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 libs/langgraph/langgraph/agent/middleware/swarm.py diff --git a/libs/langgraph/langgraph/agent/middleware/human_in_the_loop.py b/libs/langgraph/langgraph/agent/middleware/human_in_the_loop.py index a9608d2e4..08fc80596 100644 --- a/libs/langgraph/langgraph/agent/middleware/human_in_the_loop.py +++ b/libs/langgraph/langgraph/agent/middleware/human_in_the_loop.py @@ -1 +1,98 @@ -# Needs ability to jump to another node (back to model node, in case of reject or response from user) \ No newline at end of file +from langgraph.agent.types import AgentMiddleware, AgentState, AgentUpdate, AgentGoTo +from typing import Dict, Any, List, Optional, Union +from langgraph.types import interrupt +from langgraph.prebuilt.interrupt import ( + HumanInterruptConfig, + ActionRequest, + HumanInterrupt, + HumanResponse, +) + +ToolInterruptConfig = Dict[str, HumanInterruptConfig] + +class HumanInTheLoopMiddleware(AgentMiddleware): + + def __init__(self, + tool_configs: ToolInterruptConfig, + message_prefix: str = "Tool execution requires approval",): + super().__init__() + self.tool_configs = tool_configs + self.message_prefix = message_prefix + + def after_model(self, state: AgentState) -> AgentUpdate | AgentGoTo | None: + messages = state.messages + if not messages: + return + + last_message = messages[-1] + + if not hasattr(last_message, "tool_calls") or not last_message.tool_calls: + return + + # Separate tool calls that need interrupts from those that don't + interrupt_tool_calls = [] + auto_approved_tool_calls = [] + + for tool_call in last_message.tool_calls: + tool_name = tool_call["name"] + if tool_name in self.tool_configs: + interrupt_tool_calls.append(tool_call) + else: + auto_approved_tool_calls.append(tool_call) + + # If no interrupts needed, return early + if not interrupt_tool_calls: + return + + approved_tool_calls = auto_approved_tool_calls.copy() + + # Process all tool calls that need interrupts in parallel + requests = [] + + for tool_call in interrupt_tool_calls: + tool_name = tool_call["name"] + tool_args = tool_call["args"] + description = f"{self.message_prefix}\n\nTool: {tool_name}\nArgs: {tool_args}" + tool_config = self.tool_configs[tool_name] + + request: HumanInterrupt = { + "action_request": ActionRequest( + action=tool_name, + args=tool_args, + ), + "config": tool_config, + "description": description, + } + requests.append(request) + + responses: List[HumanResponse] = interrupt(requests) + + for i, response in enumerate(responses): + tool_call = interrupt_tool_calls[i] + + if response["type"] == "accept": + approved_tool_calls.append(tool_call) + elif response["type"] == "edit": + edited: ActionRequest = response["args"] + new_tool_call = { + "name": tool_call["name"], + "args": edited["args"], + "id": tool_call["id"], + } + approved_tool_calls.append(new_tool_call) + elif response["type"] == "ignore": + # NOTE: does not work with multiple interrupts + return {"goto": "__end__"} + elif response["type"] == "response": + # NOTE: does not work with multiple interrupts + tool_message = {"role": "tool", "tool_call_id": tool_call["id"], "content": response["args"]} + return { + "messages": [tool_message], + "goto": "model" + } + else: + raise ValueError(f"Unknown response type: {response['type']}") + + last_message.tool_calls = approved_tool_calls + + return {"messages": [last_message]} diff --git a/libs/langgraph/langgraph/agent/middleware/swarm.py b/libs/langgraph/langgraph/agent/middleware/swarm.py new file mode 100644 index 000000000..8af362cb0 --- /dev/null +++ b/libs/langgraph/langgraph/agent/middleware/swarm.py @@ -0,0 +1,13 @@ +from langgraph.agent.types import AgentMiddleware, AgentState, ModelRequest +from typing import Dict, Any, List, Optional, Union +from langgraph.types import interrupt + + +class SwarmMiddleWare(AgentMiddleware): + + def __init__(self, model_configs: dict[str, dict]): + super().__init__() + + def modify_model_request( + self, request: ModelRequest, state: AgentState + ) -> ModelRequest: \ No newline at end of file