mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-08 10:47:52 +02:00
Apply patch [skip ci]
This commit is contained in:
@@ -35,7 +35,7 @@ from langchain_core.runnables import (
|
||||
RunnableConfig,
|
||||
RunnableSequence,
|
||||
)
|
||||
from langchain_core.tools import BaseTool, InjectedToolArg
|
||||
from langchain_core.tools import BaseTool
|
||||
from langchain_core.tools import tool as create_tool
|
||||
from langchain_core.tools.base import (
|
||||
TOOL_MESSAGE_BLOCK_TYPES,
|
||||
@@ -46,7 +46,7 @@ from typing_extensions import Annotated, NotRequired, TypedDict, get_args, get_o
|
||||
|
||||
from langgraph._internal._runnable import RunnableCallable, RunnableLike
|
||||
from langgraph._internal._typing import MISSING
|
||||
from langgraph.errors import ErrorCode, create_error_message, GraphBubbleUp
|
||||
from langgraph.errors import ErrorCode, GraphBubbleUp, create_error_message
|
||||
from langgraph.graph import END, StateGraph
|
||||
from langgraph.graph.message import add_messages
|
||||
from langgraph.graph.state import CompiledStateGraph
|
||||
@@ -54,7 +54,7 @@ from langgraph.managed import RemainingSteps
|
||||
from langgraph.prebuilt.tool_node import InjectedState, InjectedStore
|
||||
from langgraph.runtime import Runtime
|
||||
from langgraph.store.base import BaseStore
|
||||
from langgraph.types import Checkpointer, Send, Command
|
||||
from langgraph.types import Checkpointer, Command, Send
|
||||
from langgraph.typing import ContextT
|
||||
from langgraph.warnings import LangGraphDeprecatedSinceV10
|
||||
|
||||
@@ -118,7 +118,13 @@ def _infer_handled_types(handler: Callable[..., str]) -> tuple[type[Exception],
|
||||
params = list(sig.parameters.values())
|
||||
if params:
|
||||
# If it's a method, the first argument is typically 'self' or 'cls'
|
||||
first_param = params[0] if params[0].name not in ("self", "cls") else params[1] if len(params) > 1 else None
|
||||
first_param = (
|
||||
params[0]
|
||||
if params[0].name not in ("self", "cls")
|
||||
else params[1]
|
||||
if len(params) > 1
|
||||
else None
|
||||
)
|
||||
if first_param and first_param.annotation != inspect.Parameter.empty:
|
||||
annotation = first_param.annotation
|
||||
# Handle Union types
|
||||
@@ -128,8 +134,12 @@ def _infer_handled_types(handler: Callable[..., str]) -> tuple[type[Exception],
|
||||
for t in types:
|
||||
if isinstance(t, type) and issubclass(t, Exception):
|
||||
exception_types.append(t)
|
||||
elif t is not type(None): # Allow None in Union for optional handling
|
||||
raise ValueError(f"Handler annotation must be Exception types, got {t}")
|
||||
elif t is not type(
|
||||
None
|
||||
): # Allow None in Union for optional handling
|
||||
raise ValueError(
|
||||
f"Handler annotation must be Exception types, got {t}"
|
||||
)
|
||||
return tuple(exception_types) if exception_types else (Exception,)
|
||||
# Handle single type
|
||||
elif isinstance(annotation, type) and issubclass(annotation, Exception):
|
||||
@@ -194,7 +204,7 @@ def _get_store_arg(tool: BaseTool) -> Optional[str]:
|
||||
|
||||
class ToolExecutor(RunnableCallable):
|
||||
"""A wrapper for executing individual tools with state/store injection and error handling.
|
||||
|
||||
|
||||
This class provides the same functionality as ToolNode but for single tool execution,
|
||||
enabling individual tool nodes in the graph instead of a single tools node.
|
||||
"""
|
||||
@@ -253,10 +263,10 @@ class ToolExecutor(RunnableCallable):
|
||||
"""Run a single tool call synchronously."""
|
||||
if invalid_tool_message := self._validate_tool_call(call):
|
||||
return invalid_tool_message
|
||||
|
||||
|
||||
# Inject state and store into the tool call
|
||||
injected_call = self._inject_tool_args(call, store)
|
||||
|
||||
|
||||
try:
|
||||
call_args = {**injected_call, **{"type": "tool_call"}}
|
||||
response = self.tool.invoke(call_args, config)
|
||||
@@ -288,13 +298,15 @@ class ToolExecutor(RunnableCallable):
|
||||
if isinstance(response, Command):
|
||||
# For now, we'll convert Command responses to ToolMessage
|
||||
# This maintains compatibility with the existing behavior
|
||||
if hasattr(response, 'update') and isinstance(response.update, dict):
|
||||
if hasattr(response, "update") and isinstance(response.update, dict):
|
||||
messages = response.update.get(self.messages_key, [])
|
||||
if messages and isinstance(messages[0], ToolMessage):
|
||||
return messages[0]
|
||||
# Fallback to creating a ToolMessage from Command
|
||||
return ToolMessage(
|
||||
content=str(response.update) if hasattr(response, 'update') else str(response),
|
||||
content=str(response.update)
|
||||
if hasattr(response, "update")
|
||||
else str(response),
|
||||
name=call["name"],
|
||||
tool_call_id=call["id"],
|
||||
)
|
||||
@@ -319,10 +331,10 @@ class ToolExecutor(RunnableCallable):
|
||||
"""Run a single tool call asynchronously."""
|
||||
if invalid_tool_message := self._validate_tool_call(call):
|
||||
return invalid_tool_message
|
||||
|
||||
|
||||
# Inject state and store into the tool call
|
||||
injected_call = self._inject_tool_args(call, store)
|
||||
|
||||
|
||||
try:
|
||||
call_args = {**injected_call, **{"type": "tool_call"}}
|
||||
response = await self.tool.ainvoke(call_args, config)
|
||||
@@ -354,13 +366,15 @@ class ToolExecutor(RunnableCallable):
|
||||
if isinstance(response, Command):
|
||||
# For now, we'll convert Command responses to ToolMessage
|
||||
# This maintains compatibility with the existing behavior
|
||||
if hasattr(response, 'update') and isinstance(response.update, dict):
|
||||
if hasattr(response, "update") and isinstance(response.update, dict):
|
||||
messages = response.update.get(self.messages_key, [])
|
||||
if messages and isinstance(messages[0], ToolMessage):
|
||||
return messages[0]
|
||||
# Fallback to creating a ToolMessage from Command
|
||||
return ToolMessage(
|
||||
content=str(response.update) if hasattr(response, 'update') else str(response),
|
||||
content=str(response.update)
|
||||
if hasattr(response, "update")
|
||||
else str(response),
|
||||
name=call["name"],
|
||||
tool_call_id=call["id"],
|
||||
)
|
||||
@@ -399,7 +413,7 @@ class ToolExecutor(RunnableCallable):
|
||||
# State injection will need to be handled at the graph level
|
||||
# For now, we only handle store injection
|
||||
injected_call = deepcopy(tool_call)
|
||||
|
||||
|
||||
# Inject store if needed
|
||||
if self.tool_to_store_arg:
|
||||
if store is None:
|
||||
@@ -411,7 +425,7 @@ class ToolExecutor(RunnableCallable):
|
||||
**injected_call["args"],
|
||||
self.tool_to_store_arg: store,
|
||||
}
|
||||
|
||||
|
||||
return injected_call
|
||||
|
||||
def inject_tool_args(
|
||||
@@ -425,18 +439,18 @@ class ToolExecutor(RunnableCallable):
|
||||
store: Optional[BaseStore],
|
||||
) -> ToolCall:
|
||||
"""Inject graph state and store into tool call arguments.
|
||||
|
||||
|
||||
This method provides compatibility with ToolNode.inject_tool_args()
|
||||
for use in routing logic.
|
||||
"""
|
||||
injected_call = deepcopy(tool_call)
|
||||
|
||||
|
||||
# Inject state arguments
|
||||
if self.tool_to_state_args:
|
||||
if isinstance(input, list):
|
||||
# Convert list to dict format for state injection
|
||||
input = {self.messages_key: input}
|
||||
|
||||
|
||||
tool_state_args = {}
|
||||
for arg_name, state_field in self.tool_to_state_args.items():
|
||||
if state_field is None:
|
||||
@@ -453,12 +467,12 @@ class ToolExecutor(RunnableCallable):
|
||||
f"Invalid input to ToolExecutor. Tool {tool_call['name']} requires "
|
||||
f"state field '{state_field}' but it was not found in input."
|
||||
)
|
||||
|
||||
|
||||
injected_call["args"] = {
|
||||
**injected_call["args"],
|
||||
**tool_state_args,
|
||||
}
|
||||
|
||||
|
||||
# Inject store if needed
|
||||
if self.tool_to_store_arg:
|
||||
if store is None:
|
||||
@@ -470,7 +484,7 @@ class ToolExecutor(RunnableCallable):
|
||||
**injected_call["args"],
|
||||
self.tool_to_store_arg: store,
|
||||
}
|
||||
|
||||
|
||||
return injected_call
|
||||
|
||||
|
||||
@@ -1216,7 +1230,7 @@ def create_react_agent(
|
||||
tool_node.inject_tool_args(call, state, store) # type: ignore[arg-type]
|
||||
for call in last_message.tool_calls
|
||||
]
|
||||
return [Send(call['name'], call) for call in tool_calls]
|
||||
return [Send(call["name"], call) for call in tool_calls]
|
||||
|
||||
# Define a new graph
|
||||
workflow = StateGraph(
|
||||
@@ -1231,7 +1245,10 @@ def create_react_agent(
|
||||
)
|
||||
# Add individual tool nodes instead of a single tools node
|
||||
for tool in tool_classes:
|
||||
workflow.add_node(tool.name, ToolExecutor(tool, handle_tool_errors=True, messages_key="messages"))
|
||||
workflow.add_node(
|
||||
tool.name,
|
||||
ToolExecutor(tool, handle_tool_errors=True, messages_key="messages"),
|
||||
)
|
||||
|
||||
# Optionally add a pre-model hook node that will be called
|
||||
# every time before the "agent" (LLM-calling node)
|
||||
@@ -1305,7 +1322,7 @@ def create_react_agent(
|
||||
tool_node.inject_tool_args(call, state, store) # type: ignore[arg-type]
|
||||
for call in pending_tool_calls
|
||||
]
|
||||
return [Send(call['name'], call) for call in pending_tool_calls]
|
||||
return [Send(call["name"], call) for call in pending_tool_calls]
|
||||
elif isinstance(messages[-1], ToolMessage):
|
||||
return entrypoint
|
||||
elif response_format is not None:
|
||||
@@ -1375,19 +1392,3 @@ __all__ = [
|
||||
"AgentStateWithStructuredResponse",
|
||||
"AgentStateWithStructuredResponsePydantic",
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user