From 52b1516d44897e3961781b1ed36f48eaa51b8b33 Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Sun, 16 Nov 2025 00:33:17 -0500 Subject: [PATCH] style(langchain): fix some middleware ref syntax (#33988) --- .../agents/middleware/file_search.py | 4 +- .../agents/middleware/model_call_limit.py | 1 + .../langchain/agents/middleware/shell_tool.py | 2 +- .../agents/middleware/summarization.py | 31 +- .../langchain/agents/middleware/todo.py | 4 - .../agents/middleware/tool_call_limit.py | 61 +- .../agents/middleware/tool_emulator.py | 50 +- .../langchain/agents/middleware/tool_retry.py | 141 ++--- .../agents/middleware/tool_selection.py | 30 +- .../langchain/agents/middleware/types.py | 549 ++++++++++-------- .../langchain_anthropic/chat_models.py | 10 +- .../middleware/anthropic_tools.py | 4 +- .../langchain_openai/chat_models/base.py | 42 +- libs/standard-tests/uv.lock | 2 +- 14 files changed, 525 insertions(+), 406 deletions(-) diff --git a/libs/langchain_v1/langchain/agents/middleware/file_search.py b/libs/langchain_v1/langchain/agents/middleware/file_search.py index 2c099f780a6..021a64bcf5d 100644 --- a/libs/langchain_v1/langchain/agents/middleware/file_search.py +++ b/libs/langchain_v1/langchain/agents/middleware/file_search.py @@ -120,9 +120,9 @@ class FilesystemFileSearchMiddleware(AgentMiddleware): Args: root_path: Root directory to search. - use_ripgrep: Whether to use ripgrep for search. + use_ripgrep: Whether to use `ripgrep` for search. - Falls back to Python if ripgrep unavailable. + Falls back to Python if `ripgrep` unavailable. max_file_size_mb: Maximum file size to search in MB. """ self.root_path = Path(root_path).resolve() diff --git a/libs/langchain_v1/langchain/agents/middleware/model_call_limit.py b/libs/langchain_v1/langchain/agents/middleware/model_call_limit.py index 87afda778ee..663b90b081e 100644 --- a/libs/langchain_v1/langchain/agents/middleware/model_call_limit.py +++ b/libs/langchain_v1/langchain/agents/middleware/model_call_limit.py @@ -133,6 +133,7 @@ class ModelCallLimitMiddleware(AgentMiddleware[ModelCallLimitState, Any]): `None` means no limit. exit_behavior: What to do when limits are exceeded. + - `'end'`: Jump to the end of the agent execution and inject an artificial AI message indicating that the limit was exceeded. diff --git a/libs/langchain_v1/langchain/agents/middleware/shell_tool.py b/libs/langchain_v1/langchain/agents/middleware/shell_tool.py index 316ffa64bc3..17a23e90ceb 100644 --- a/libs/langchain_v1/langchain/agents/middleware/shell_tool.py +++ b/libs/langchain_v1/langchain/agents/middleware/shell_tool.py @@ -389,7 +389,7 @@ class ShellToolMiddleware(AgentMiddleware[ShellToolState, Any]): shell_command: Sequence[str] | str | None = None, env: Mapping[str, Any] | None = None, ) -> None: - """Initialize the middleware. + """Initialize an instance of `ShellToolMiddleware`. Args: workspace_root: Base directory for the shell session. diff --git a/libs/langchain_v1/langchain/agents/middleware/summarization.py b/libs/langchain_v1/langchain/agents/middleware/summarization.py index 5bc3c08fcc5..83e16a3950a 100644 --- a/libs/langchain_v1/langchain/agents/middleware/summarization.py +++ b/libs/langchain_v1/langchain/agents/middleware/summarization.py @@ -87,18 +87,27 @@ class SummarizationMiddleware(AgentMiddleware): Args: model: The language model to use for generating summaries. - trigger: One or more thresholds that trigger summarization. Provide a single - `ContextSize` tuple or a list of tuples, in which case summarization runs - when any threshold is breached. Examples: `("messages", 50)`, `("tokens", 3000)`, - `[("fraction", 0.8), ("messages", 100)]`. - keep: Context retention policy applied after summarization. Provide a - `ContextSize` tuple to specify how much history to preserve. Defaults to - keeping the most recent 20 messages. Examples: `("messages", 20)`, - `("tokens", 3000)`, or `("fraction", 0.3)`. + trigger: One or more thresholds that trigger summarization. + + Provide a single `ContextSize` tuple or a list of tuples, in which case + summarization runs when any threshold is breached. + + Examples: `("messages", 50)`, `("tokens", 3000)`, `[("fraction", 0.8), + ("messages", 100)]`. + keep: Context retention policy applied after summarization. + + Provide a `ContextSize` tuple to specify how much history to preserve. + + Defaults to keeping the most recent 20 messages. + + Examples: `("messages", 20)`, `("tokens", 3000)`, or + `("fraction", 0.3)`. token_counter: Function to count tokens in messages. summary_prompt: Prompt template for generating summaries. - trim_tokens_to_summarize: Maximum tokens to keep when preparing messages for the - summarization call. Pass `None` to skip trimming entirely. + trim_tokens_to_summarize: Maximum tokens to keep when preparing messages for + the summarization call. + + Pass `None` to skip trimming entirely. """ # Handle deprecated parameters if "max_tokens_before_summary" in deprecated_kwargs: @@ -354,7 +363,7 @@ class SummarizationMiddleware(AgentMiddleware): """Find safe cutoff point that preserves AI/Tool message pairs. Returns the index where messages can be safely cut without separating - related AI and Tool messages. Returns 0 if no safe cutoff is found. + related AI and Tool messages. Returns `0` if no safe cutoff is found. """ if len(messages) <= messages_to_keep: return 0 diff --git a/libs/langchain_v1/langchain/agents/middleware/todo.py b/libs/langchain_v1/langchain/agents/middleware/todo.py index c2c0b617210..a904eccb23e 100644 --- a/libs/langchain_v1/langchain/agents/middleware/todo.py +++ b/libs/langchain_v1/langchain/agents/middleware/todo.py @@ -150,10 +150,6 @@ class TodoListMiddleware(AgentMiddleware): print(result["todos"]) # Array of todo items with status tracking ``` - - Args: - system_prompt: Custom system prompt to guide the agent on using the todo tool. - tool_description: Custom description for the write_todos tool. """ state_schema = PlanningState diff --git a/libs/langchain_v1/langchain/agents/middleware/tool_call_limit.py b/libs/langchain_v1/langchain/agents/middleware/tool_call_limit.py index 9aa2d116218..0cedb231cd2 100644 --- a/libs/langchain_v1/langchain/agents/middleware/tool_call_limit.py +++ b/libs/langchain_v1/langchain/agents/middleware/tool_call_limit.py @@ -153,38 +153,46 @@ class ToolCallLimitMiddleware( are other pending tool calls (due to parallel tool calling). Examples: - ```python title="Continue execution with blocked tools (default)" - from langchain.agents.middleware.tool_call_limit import ToolCallLimitMiddleware - from langchain.agents import create_agent + !!! example "Continue execution with blocked tools (default)" - # Block exceeded tools but let other tools and model continue - limiter = ToolCallLimitMiddleware( - thread_limit=20, - run_limit=10, - exit_behavior="continue", # default - ) + ```python + from langchain.agents.middleware.tool_call_limit import ToolCallLimitMiddleware + from langchain.agents import create_agent - agent = create_agent("openai:gpt-4o", middleware=[limiter]) - ``` + # Block exceeded tools but let other tools and model continue + limiter = ToolCallLimitMiddleware( + thread_limit=20, + run_limit=10, + exit_behavior="continue", # default + ) - ```python title="Stop immediately when limit exceeded" - # End execution immediately with an AI message - limiter = ToolCallLimitMiddleware(run_limit=5, exit_behavior="end") + agent = create_agent("openai:gpt-4o", middleware=[limiter]) + ``` - agent = create_agent("openai:gpt-4o", middleware=[limiter]) - ``` + !!! example "Stop immediately when limit exceeded" - ```python title="Raise exception on limit" - # Strict limit with exception handling - limiter = ToolCallLimitMiddleware(tool_name="search", thread_limit=5, exit_behavior="error") + ```python + # End execution immediately with an AI message + limiter = ToolCallLimitMiddleware(run_limit=5, exit_behavior="end") - agent = create_agent("openai:gpt-4o", middleware=[limiter]) + agent = create_agent("openai:gpt-4o", middleware=[limiter]) + ``` - try: - result = await agent.invoke({"messages": [HumanMessage("Task")]}) - except ToolCallLimitExceededError as e: - print(f"Search limit exceeded: {e}") - ``` + !!! example "Raise exception on limit" + + ```python + # Strict limit with exception handling + limiter = ToolCallLimitMiddleware( + tool_name="search", thread_limit=5, exit_behavior="error" + ) + + agent = create_agent("openai:gpt-4o", middleware=[limiter]) + + try: + result = await agent.invoke({"messages": [HumanMessage("Task")]}) + except ToolCallLimitExceededError as e: + print(f"Search limit exceeded: {e}") + ``` """ @@ -208,6 +216,7 @@ class ToolCallLimitMiddleware( run_limit: Maximum number of tool calls allowed per run. `None` means no limit. exit_behavior: How to handle when limits are exceeded. + - `'continue'`: Block exceeded tools with error messages, let other tools continue. Model decides when to end. - `'error'`: Raise a `ToolCallLimitExceededError` exception @@ -218,7 +227,7 @@ class ToolCallLimitMiddleware( Raises: ValueError: If both limits are `None`, if `exit_behavior` is invalid, - or if `run_limit` exceeds thread_limit. + or if `run_limit` exceeds `thread_limit`. """ super().__init__() diff --git a/libs/langchain_v1/langchain/agents/middleware/tool_emulator.py b/libs/langchain_v1/langchain/agents/middleware/tool_emulator.py index 45bb0ad5a9f..16b5e57c566 100644 --- a/libs/langchain_v1/langchain/agents/middleware/tool_emulator.py +++ b/libs/langchain_v1/langchain/agents/middleware/tool_emulator.py @@ -25,34 +25,42 @@ class LLMToolEmulator(AgentMiddleware): This middleware allows selective emulation of tools for testing purposes. By default (when `tools=None`), all tools are emulated. You can specify which - tools to emulate by passing a list of tool names or BaseTool instances. + tools to emulate by passing a list of tool names or `BaseTool` instances. Examples: - ```python title="Emulate all tools (default behavior)" - from langchain.agents.middleware import LLMToolEmulator + !!! example "Emulate all tools (default behavior)" - middleware = LLMToolEmulator() + ```python + from langchain.agents.middleware import LLMToolEmulator - agent = create_agent( - model="openai:gpt-4o", - tools=[get_weather, get_user_location, calculator], - middleware=[middleware], - ) - ``` + middleware = LLMToolEmulator() - ```python title="Emulate specific tools by name" - middleware = LLMToolEmulator(tools=["get_weather", "get_user_location"]) - ``` + agent = create_agent( + model="openai:gpt-4o", + tools=[get_weather, get_user_location, calculator], + middleware=[middleware], + ) + ``` - ```python title="Use a custom model for emulation" - middleware = LLMToolEmulator( - tools=["get_weather"], model="anthropic:claude-sonnet-4-5-20250929" - ) - ``` + !!! example "Emulate specific tools by name" - ```python title="Emulate specific tools by passing tool instances" - middleware = LLMToolEmulator(tools=[get_weather, get_user_location]) - ``` + ```python + middleware = LLMToolEmulator(tools=["get_weather", "get_user_location"]) + ``` + + !!! example "Use a custom model for emulation" + + ```python + middleware = LLMToolEmulator( + tools=["get_weather"], model="anthropic:claude-sonnet-4-5-20250929" + ) + ``` + + !!! example "Emulate specific tools by passing tool instances" + + ```python + middleware = LLMToolEmulator(tools=[get_weather, get_user_location]) + ``` """ def __init__( diff --git a/libs/langchain_v1/langchain/agents/middleware/tool_retry.py b/libs/langchain_v1/langchain/agents/middleware/tool_retry.py index 7f9cde39d4b..0eb2a2b1095 100644 --- a/libs/langchain_v1/langchain/agents/middleware/tool_retry.py +++ b/libs/langchain_v1/langchain/agents/middleware/tool_retry.py @@ -26,96 +26,96 @@ class ToolRetryMiddleware(AgentMiddleware): Supports retrying on specific exceptions and exponential backoff. Examples: - Basic usage with default settings (2 retries, exponential backoff): + !!! example "Basic usage with default settings (2 retries, exponential backoff)" - ```python - from langchain.agents import create_agent - from langchain.agents.middleware import ToolRetryMiddleware + ```python + from langchain.agents import create_agent + from langchain.agents.middleware import ToolRetryMiddleware - agent = create_agent(model, tools=[search_tool], middleware=[ToolRetryMiddleware()]) - ``` + agent = create_agent(model, tools=[search_tool], middleware=[ToolRetryMiddleware()]) + ``` - Retry specific exceptions only: + !!! example "Retry specific exceptions only" - ```python - from requests.exceptions import RequestException, Timeout + ```python + from requests.exceptions import RequestException, Timeout - retry = ToolRetryMiddleware( - max_retries=4, - retry_on=(RequestException, Timeout), - backoff_factor=1.5, - ) - ``` + retry = ToolRetryMiddleware( + max_retries=4, + retry_on=(RequestException, Timeout), + backoff_factor=1.5, + ) + ``` - Custom exception filtering: + !!! example "Custom exception filtering" - ```python - from requests.exceptions import HTTPError + ```python + from requests.exceptions import HTTPError - def should_retry(exc: Exception) -> bool: - # Only retry on 5xx errors - if isinstance(exc, HTTPError): - return 500 <= exc.status_code < 600 - return False + def should_retry(exc: Exception) -> bool: + # Only retry on 5xx errors + if isinstance(exc, HTTPError): + return 500 <= exc.status_code < 600 + return False - retry = ToolRetryMiddleware( - max_retries=3, - retry_on=should_retry, - ) - ``` + retry = ToolRetryMiddleware( + max_retries=3, + retry_on=should_retry, + ) + ``` - Apply to specific tools with custom error handling: + !!! example "Apply to specific tools with custom error handling" - ```python - def format_error(exc: Exception) -> str: - return "Database temporarily unavailable. Please try again later." + ```python + def format_error(exc: Exception) -> str: + return "Database temporarily unavailable. Please try again later." - retry = ToolRetryMiddleware( - max_retries=4, - tools=["search_database"], - on_failure=format_error, - ) - ``` + retry = ToolRetryMiddleware( + max_retries=4, + tools=["search_database"], + on_failure=format_error, + ) + ``` - Apply to specific tools using BaseTool instances: + !!! example "Apply to specific tools using `BaseTool` instances" - ```python - from langchain_core.tools import tool + ```python + from langchain_core.tools import tool - @tool - def search_database(query: str) -> str: - '''Search the database.''' - return results + @tool + def search_database(query: str) -> str: + '''Search the database.''' + return results - retry = ToolRetryMiddleware( - max_retries=4, - tools=[search_database], # Pass BaseTool instance - ) - ``` + retry = ToolRetryMiddleware( + max_retries=4, + tools=[search_database], # Pass BaseTool instance + ) + ``` - Constant backoff (no exponential growth): + !!! example "Constant backoff (no exponential growth)" - ```python - retry = ToolRetryMiddleware( - max_retries=5, - backoff_factor=0.0, # No exponential growth - initial_delay=2.0, # Always wait 2 seconds - ) - ``` + ```python + retry = ToolRetryMiddleware( + max_retries=5, + backoff_factor=0.0, # No exponential growth + initial_delay=2.0, # Always wait 2 seconds + ) + ``` - Raise exception on failure: + !!! example "Raise exception on failure" - ```python - retry = ToolRetryMiddleware( - max_retries=2, - on_failure="raise", # Re-raise exception instead of returning message - ) - ``` + ```python + retry = ToolRetryMiddleware( + max_retries=2, + on_failure="raise", # Re-raise exception instead of returning message + ) + ``` """ def __init__( @@ -136,7 +136,10 @@ class ToolRetryMiddleware(AgentMiddleware): Args: max_retries: Maximum number of retry attempts after the initial call. - Default is `2` retries (`3` total attempts). Must be `>= 0`. + + Default is `2` retries (`3` total attempts). + + Must be `>= 0`. tools: Optional list of tools or tool names to apply retry logic to. Can be a list of `BaseTool` instances or tool name strings. @@ -146,12 +149,14 @@ class ToolRetryMiddleware(AgentMiddleware): that takes an exception and returns `True` if it should be retried. Default is to retry on all exceptions. - on_failure: Behavior when all retries are exhausted. Options: + on_failure: Behavior when all retries are exhausted. + + Options: - `'return_message'`: Return a `ToolMessage` with error details, allowing the LLM to handle the failure and potentially recover. - `'raise'`: Re-raise the exception, stopping agent execution. - - Custom callable: Function that takes the exception and returns a + - **Custom callable:** Function that takes the exception and returns a string for the `ToolMessage` content, allowing custom error formatting. backoff_factor: Multiplier for exponential backoff. diff --git a/libs/langchain_v1/langchain/agents/middleware/tool_selection.py b/libs/langchain_v1/langchain/agents/middleware/tool_selection.py index 34fa8a0f044..c7548451281 100644 --- a/libs/langchain_v1/langchain/agents/middleware/tool_selection.py +++ b/libs/langchain_v1/langchain/agents/middleware/tool_selection.py @@ -93,21 +93,25 @@ class LLMToolSelectorMiddleware(AgentMiddleware): and helps the main model focus on the right tools. Examples: - ```python title="Limit to 3 tools" - from langchain.agents.middleware import LLMToolSelectorMiddleware + !!! example "Limit to 3 tools" - middleware = LLMToolSelectorMiddleware(max_tools=3) + ```python + from langchain.agents.middleware import LLMToolSelectorMiddleware - agent = create_agent( - model="openai:gpt-4o", - tools=[tool1, tool2, tool3, tool4, tool5], - middleware=[middleware], - ) - ``` + middleware = LLMToolSelectorMiddleware(max_tools=3) - ```python title="Use a smaller model for selection" - middleware = LLMToolSelectorMiddleware(model="openai:gpt-4o-mini", max_tools=2) - ``` + agent = create_agent( + model="openai:gpt-4o", + tools=[tool1, tool2, tool3, tool4, tool5], + middleware=[middleware], + ) + ``` + + !!! example "Use a smaller model for selection" + + ```python + middleware = LLMToolSelectorMiddleware(model="openai:gpt-4o-mini", max_tools=2) + ``` """ def __init__( @@ -131,7 +135,7 @@ class LLMToolSelectorMiddleware(AgentMiddleware): If the model selects more, only the first `max_tools` will be used. - No limit if not specified. + If not specified, there is no limit. always_include: Tool names to always include regardless of selection. These do not count against the `max_tools` limit. diff --git a/libs/langchain_v1/langchain/agents/middleware/types.py b/libs/langchain_v1/langchain/agents/middleware/types.py index fc93043094d..2ec6ac5dbaf 100644 --- a/libs/langchain_v1/langchain/agents/middleware/types.py +++ b/libs/langchain_v1/langchain/agents/middleware/types.py @@ -118,13 +118,17 @@ class ModelRequest: New `ModelRequest` instance with specified overrides applied. Examples: - ```python title="Create a new request with different model" - new_request = request.override(model=different_model) - ``` + !!! example "Create a new request with different model" - ```python title="Override multiple attributes" - new_request = request.override(system_prompt="New instructions", tool_choice="auto") - ``` + ```python + new_request = request.override(model=different_model) + ``` + + !!! example "Override multiple attributes" + + ```python + new_request = request.override(system_prompt="New instructions", tool_choice="auto") + ``` """ return replace(self, **overrides) @@ -224,7 +228,10 @@ class AgentMiddleware(Generic[StateT, ContextT]): return self.__class__.__name__ def before_agent(self, state: StateT, runtime: Runtime[ContextT]) -> dict[str, Any] | None: - """Logic to run before the agent execution starts.""" + """Logic to run before the agent execution starts. + + Async version is `abefore_agent` + """ async def abefore_agent( self, state: StateT, runtime: Runtime[ContextT] @@ -232,7 +239,10 @@ class AgentMiddleware(Generic[StateT, ContextT]): """Async logic to run before the agent execution starts.""" def before_model(self, state: StateT, runtime: Runtime[ContextT]) -> dict[str, Any] | None: - """Logic to run before the model is called.""" + """Logic to run before the model is called. + + Async version is `abefore_model` + """ async def abefore_model( self, state: StateT, runtime: Runtime[ContextT] @@ -240,7 +250,10 @@ class AgentMiddleware(Generic[StateT, ContextT]): """Async logic to run before the model is called.""" def after_model(self, state: StateT, runtime: Runtime[ContextT]) -> dict[str, Any] | None: - """Logic to run after the model is called.""" + """Logic to run after the model is called. + + Async version is `aafter_model` + """ async def aafter_model( self, state: StateT, runtime: Runtime[ContextT] @@ -254,6 +267,8 @@ class AgentMiddleware(Generic[StateT, ContextT]): ) -> ModelCallResult: """Intercept and control model execution via handler callback. + Async version is `awrap_model_call` + The handler callback executes the model request and returns a `ModelResponse`. Middleware can call the handler multiple times for retry logic, skip calling it to short-circuit, or modify the request/response. Multiple middleware @@ -274,49 +289,59 @@ class AgentMiddleware(Generic[StateT, ContextT]): `ModelCallResult` Examples: - ```python title="Retry on error" - def wrap_model_call(self, request, handler): - for attempt in range(3): + !!! example "Retry on error" + + ```python + def wrap_model_call(self, request, handler): + for attempt in range(3): + try: + return handler(request) + except Exception: + if attempt == 2: + raise + ``` + + !!! example "Rewrite response" + + ```python + def wrap_model_call(self, request, handler): + response = handler(request) + ai_msg = response.result[0] + return ModelResponse( + result=[AIMessage(content=f"[{ai_msg.content}]")], + structured_response=response.structured_response, + ) + ``` + + !!! example "Error to fallback" + + ```python + def wrap_model_call(self, request, handler): try: return handler(request) except Exception: - if attempt == 2: - raise - ``` + return ModelResponse(result=[AIMessage(content="Service unavailable")]) + ``` - ```python title="Rewrite response" - def wrap_model_call(self, request, handler): - response = handler(request) - ai_msg = response.result[0] - return ModelResponse( - result=[AIMessage(content=f"[{ai_msg.content}]")], - structured_response=response.structured_response, - ) - ``` + !!! example "Cache/short-circuit" - ```python title="Error to fallback" - def wrap_model_call(self, request, handler): - try: - return handler(request) - except Exception: - return ModelResponse(result=[AIMessage(content="Service unavailable")]) - ``` + ```python + def wrap_model_call(self, request, handler): + if cached := get_cache(request): + return cached # Short-circuit with cached result + response = handler(request) + save_cache(request, response) + return response + ``` - ```python title="Cache/short-circuit" - def wrap_model_call(self, request, handler): - if cached := get_cache(request): - return cached # Short-circuit with cached result - response = handler(request) - save_cache(request, response) - return response - ``` + !!! example "Simple `AIMessage` return (converted automatically)" - ```python title="Simple `AIMessage` return (converted automatically)" - def wrap_model_call(self, request, handler): - response = handler(request) - # Can return AIMessage directly for simple cases - return AIMessage(content="Simplified response") - ``` + ```python + def wrap_model_call(self, request, handler): + response = handler(request) + # Can return AIMessage directly for simple cases + return AIMessage(content="Simplified response") + ``` """ msg = ( "Synchronous implementation of wrap_model_call is not available. " @@ -358,15 +383,17 @@ class AgentMiddleware(Generic[StateT, ContextT]): `ModelCallResult` Examples: - ```python title="Retry on error" - async def awrap_model_call(self, request, handler): - for attempt in range(3): - try: - return await handler(request) - except Exception: - if attempt == 2: - raise - ``` + !!! example "Retry on error" + + ```python + async def awrap_model_call(self, request, handler): + for attempt in range(3): + try: + return await handler(request) + except Exception: + if attempt == 2: + raise + ``` """ msg = ( "Asynchronous implementation of awrap_model_call is not available. " @@ -395,6 +422,8 @@ class AgentMiddleware(Generic[StateT, ContextT]): ) -> ToolMessage | Command: """Intercept tool execution for retries, monitoring, or modification. + Async version is `awrap_tool_call` + Multiple middleware compose automatically (first defined = outermost). Exceptions propagate unless `handle_tool_errors` is configured on `ToolNode`. @@ -413,35 +442,41 @@ class AgentMiddleware(Generic[StateT, ContextT]): Each call to handler is independent and stateless. Examples: - ```python title="Modify request before execution" - def wrap_tool_call(self, request, handler): - request.tool_call["args"]["value"] *= 2 - return handler(request) - ``` + !!! example "Modify request before execution" - ```python title="Retry on error (call handler multiple times)" - def wrap_tool_call(self, request, handler): - for attempt in range(3): - try: - result = handler(request) - if is_valid(result): - return result - except Exception: - if attempt == 2: - raise - return result - ``` + ```python + def wrap_tool_call(self, request, handler): + request.tool_call["args"]["value"] *= 2 + return handler(request) + ``` - ```python title="Conditional retry based on response" - def wrap_tool_call(self, request, handler): - for attempt in range(3): - result = handler(request) - if isinstance(result, ToolMessage) and result.status != "error": - return result - if attempt < 2: - continue + !!! example "Retry on error (call handler multiple times)" + + ```python + def wrap_tool_call(self, request, handler): + for attempt in range(3): + try: + result = handler(request) + if is_valid(result): + return result + except Exception: + if attempt == 2: + raise return result - ``` + ``` + + !!! example "Conditional retry based on response" + + ```python + def wrap_tool_call(self, request, handler): + for attempt in range(3): + result = handler(request) + if isinstance(result, ToolMessage) and result.status != "error": + return result + if attempt < 2: + continue + return result + ``` """ msg = ( "Synchronous implementation of wrap_tool_call is not available. " @@ -488,27 +523,29 @@ class AgentMiddleware(Generic[StateT, ContextT]): Each call to handler is independent and stateless. Examples: - ```python title="Async retry on error" - async def awrap_tool_call(self, request, handler): - for attempt in range(3): - try: - result = await handler(request) - if is_valid(result): - return result - except Exception: - if attempt == 2: - raise - return result - ``` + !!! example "Async retry on error" - ```python - async def awrap_tool_call(self, request, handler): - if cached := await get_cache_async(request): - return ToolMessage(content=cached, tool_call_id=request.tool_call["id"]) - result = await handler(request) - await save_cache_async(request, result) - return result - ``` + ```python + async def awrap_tool_call(self, request, handler): + for attempt in range(3): + try: + result = await handler(request) + if is_valid(result): + return result + except Exception: + if attempt == 2: + raise + return result + ``` + + ```python + async def awrap_tool_call(self, request, handler): + if cached := await get_cache_async(request): + return ToolMessage(content=cached, tool_call_id=request.tool_call["id"]) + result = await handler(request) + await save_cache_async(request, result) + return result + ``` """ msg = ( "Asynchronous implementation of awrap_tool_call is not available. " @@ -599,16 +636,16 @@ def hook_config( Decorator function that marks the method with configuration metadata. Examples: - Using decorator on a class method: + !!! example "Using decorator on a class method" - ```python - class MyMiddleware(AgentMiddleware): - @hook_config(can_jump_to=["end", "model"]) - def before_model(self, state: AgentState) -> dict[str, Any] | None: - if some_condition(state): - return {"jump_to": "end"} - return None - ``` + ```python + class MyMiddleware(AgentMiddleware): + @hook_config(can_jump_to=["end", "model"]) + def before_model(self, state: AgentState) -> dict[str, Any] | None: + if some_condition(state): + return {"jump_to": "end"} + return None + ``` Alternative: Use the `can_jump_to` parameter in `before_model`/`after_model` decorators: @@ -689,25 +726,33 @@ def before_model( - `None` - No state updates or flow control Examples: - ```python title="Basic usage" - @before_model - def log_before_model(state: AgentState, runtime: Runtime) -> None: - print(f"About to call model with {len(state['messages'])} messages") - ``` + !!! example "Basic usage" - ```python title="With conditional jumping" - @before_model(can_jump_to=["end"]) - def conditional_before_model(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: - if some_condition(state): - return {"jump_to": "end"} - return None - ``` + ```python + @before_model + def log_before_model(state: AgentState, runtime: Runtime) -> None: + print(f"About to call model with {len(state['messages'])} messages") + ``` - ```python title="With custom state schema" - @before_model(state_schema=MyCustomState) - def custom_before_model(state: MyCustomState, runtime: Runtime) -> dict[str, Any]: - return {"custom_field": "updated_value"} - ``` + !!! example "With conditional jumping" + + ```python + @before_model(can_jump_to=["end"]) + def conditional_before_model( + state: AgentState, runtime: Runtime + ) -> dict[str, Any] | None: + if some_condition(state): + return {"jump_to": "end"} + return None + ``` + + !!! example "With custom state schema" + + ```python + @before_model(state_schema=MyCustomState) + def custom_before_model(state: MyCustomState, runtime: Runtime) -> dict[str, Any]: + return {"custom_field": "updated_value"} + ``` """ def decorator( @@ -834,17 +879,21 @@ def after_model( - `None` - No state updates or flow control Examples: - ```python title="Basic usage for logging model responses" - @after_model - def log_latest_message(state: AgentState, runtime: Runtime) -> None: - print(state["messages"][-1].content) - ``` + !!! example "Basic usage for logging model responses" - ```python title="With custom state schema" - @after_model(state_schema=MyCustomState, name="MyAfterModelMiddleware") - def custom_after_model(state: MyCustomState, runtime: Runtime) -> dict[str, Any]: - return {"custom_field": "updated_after_model"} - ``` + ```python + @after_model + def log_latest_message(state: AgentState, runtime: Runtime) -> None: + print(state["messages"][-1].content) + ``` + + !!! example "With custom state schema" + + ```python + @after_model(state_schema=MyCustomState, name="MyAfterModelMiddleware") + def custom_after_model(state: MyCustomState, runtime: Runtime) -> dict[str, Any]: + return {"custom_field": "updated_after_model"} + ``` """ def decorator( @@ -969,25 +1018,33 @@ def before_agent( - `None` - No state updates or flow control Examples: - ```python title="Basic usage" - @before_agent - def log_before_agent(state: AgentState, runtime: Runtime) -> None: - print(f"Starting agent with {len(state['messages'])} messages") - ``` + !!! example "Basic usage" - ```python title="With conditional jumping" - @before_agent(can_jump_to=["end"]) - def conditional_before_agent(state: AgentState, runtime: Runtime) -> dict[str, Any] | None: - if some_condition(state): - return {"jump_to": "end"} - return None - ``` + ```python + @before_agent + def log_before_agent(state: AgentState, runtime: Runtime) -> None: + print(f"Starting agent with {len(state['messages'])} messages") + ``` - ```python title="With custom state schema" - @before_agent(state_schema=MyCustomState) - def custom_before_agent(state: MyCustomState, runtime: Runtime) -> dict[str, Any]: - return {"custom_field": "initialized_value"} - ``` + !!! example "With conditional jumping" + + ```python + @before_agent(can_jump_to=["end"]) + def conditional_before_agent( + state: AgentState, runtime: Runtime + ) -> dict[str, Any] | None: + if some_condition(state): + return {"jump_to": "end"} + return None + ``` + + !!! example "With custom state schema" + + ```python + @before_agent(state_schema=MyCustomState) + def custom_before_agent(state: MyCustomState, runtime: Runtime) -> dict[str, Any]: + return {"custom_field": "initialized_value"} + ``` """ def decorator( @@ -1087,6 +1144,8 @@ def after_agent( ): """Decorator used to dynamically create a middleware with the `after_agent` hook. + Async version is `aafter_agent`. + Args: func: The function to be decorated. @@ -1114,17 +1173,21 @@ def after_agent( - `None` - No state updates or flow control Examples: - ```python title="Basic usage for logging agent completion" - @after_agent - def log_completion(state: AgentState, runtime: Runtime) -> None: - print(f"Agent completed with {len(state['messages'])} messages") - ``` + !!! example "Basic usage for logging agent completion" - ```python title="With custom state schema" - @after_agent(state_schema=MyCustomState, name="MyAfterAgentMiddleware") - def custom_after_agent(state: MyCustomState, runtime: Runtime) -> dict[str, Any]: - return {"custom_field": "finalized_value"} - ``` + ```python + @after_agent + def log_completion(state: AgentState, runtime: Runtime) -> None: + print(f"Agent completed with {len(state['messages'])} messages") + ``` + + !!! example "With custom state schema" + + ```python + @after_agent(state_schema=MyCustomState, name="MyAfterAgentMiddleware") + def custom_after_agent(state: MyCustomState, runtime: Runtime) -> dict[str, Any]: + return {"custom_field": "finalized_value"} + ``` """ def decorator( @@ -1380,49 +1443,57 @@ def wrap_model_call( `AgentMiddleware` instance if func provided, otherwise a decorator. Examples: - ```python title="Basic retry logic" - @wrap_model_call - def retry_on_error(request, handler): - max_retries = 3 - for attempt in range(max_retries): + !!! example "Basic retry logic" + + ```python + @wrap_model_call + def retry_on_error(request, handler): + max_retries = 3 + for attempt in range(max_retries): + try: + return handler(request) + except Exception: + if attempt == max_retries - 1: + raise + ``` + + !!! example "Model fallback" + + ```python + @wrap_model_call + def fallback_model(request, handler): + # Try primary model try: return handler(request) except Exception: - if attempt == max_retries - 1: - raise - ``` + pass - ```python title="Model fallback" - @wrap_model_call - def fallback_model(request, handler): - # Try primary model - try: + # Try fallback model + request.model = fallback_model_instance return handler(request) - except Exception: - pass + ``` - # Try fallback model - request.model = fallback_model_instance - return handler(request) - ``` + !!! example "Rewrite response content (full `ModelResponse`)" - ```python title="Rewrite response content (full `ModelResponse`)" - @wrap_model_call - def uppercase_responses(request, handler): - response = handler(request) - ai_msg = response.result[0] - return ModelResponse( - result=[AIMessage(content=ai_msg.content.upper())], - structured_response=response.structured_response, - ) - ``` + ```python + @wrap_model_call + def uppercase_responses(request, handler): + response = handler(request) + ai_msg = response.result[0] + return ModelResponse( + result=[AIMessage(content=ai_msg.content.upper())], + structured_response=response.structured_response, + ) + ``` - ```python title="Simple `AIMessage` return (converted automatically)" - @wrap_model_call - def simple_response(request, handler): - # AIMessage is automatically converted to ModelResponse - return AIMessage(content="Simple response") - ``` + !!! example "Simple `AIMessage` return (converted automatically)" + + ```python + @wrap_model_call + def simple_response(request, handler): + # AIMessage is automatically converted to ModelResponse + return AIMessage(content="Simple response") + ``` """ def decorator( @@ -1509,6 +1580,8 @@ def wrap_tool_call( ): """Create middleware with `wrap_tool_call` hook from a function. + Async version is `awrap_tool_call`. + Converts a function with handler callback into middleware that can intercept tool calls, implement retry logic, monitor execution, and modify responses. @@ -1527,45 +1600,53 @@ def wrap_tool_call( `AgentMiddleware` instance if func provided, otherwise a decorator. Examples: - ```python title="Retry logic" - @wrap_tool_call - def retry_on_error(request, handler): - max_retries = 3 - for attempt in range(max_retries): - try: - return handler(request) - except Exception: - if attempt == max_retries - 1: - raise - ``` + !!! example "Retry logic" - ```python title="Async retry logic" - @wrap_tool_call - async def async_retry(request, handler): - for attempt in range(3): - try: - return await handler(request) - except Exception: - if attempt == 2: - raise - ``` + ```python + @wrap_tool_call + def retry_on_error(request, handler): + max_retries = 3 + for attempt in range(max_retries): + try: + return handler(request) + except Exception: + if attempt == max_retries - 1: + raise + ``` - ```python title="Modify request" - @wrap_tool_call - def modify_args(request, handler): - request.tool_call["args"]["value"] *= 2 - return handler(request) - ``` + !!! example "Async retry logic" - ```python title="Short-circuit with cached result" - @wrap_tool_call - def with_cache(request, handler): - if cached := get_cache(request): - return ToolMessage(content=cached, tool_call_id=request.tool_call["id"]) - result = handler(request) - save_cache(request, result) - return result - ``` + ```python + @wrap_tool_call + async def async_retry(request, handler): + for attempt in range(3): + try: + return await handler(request) + except Exception: + if attempt == 2: + raise + ``` + + !!! example "Modify request" + + ```python + @wrap_tool_call + def modify_args(request, handler): + request.tool_call["args"]["value"] *= 2 + return handler(request) + ``` + + !!! example "Short-circuit with cached result" + + ```python + @wrap_tool_call + def with_cache(request, handler): + if cached := get_cache(request): + return ToolMessage(content=cached, tool_call_id=request.tool_call["id"]) + result = handler(request) + save_cache(request, result) + return result + ``` """ def decorator( diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index e9f3b82b894..08b846c067f 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -1190,7 +1190,7 @@ class ChatAnthropic(BaseChatModel): See [Claude documentation](https://docs.claude.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration-beta) for detail. - !!! note title="Extended context windows (beta)" + !!! note "Extended context windows (beta)" Claude Sonnet 4 supports a 1-million token context window, available in beta for organizations in usage tier 4 and organizations with custom rate limits. @@ -1226,7 +1226,7 @@ class ChatAnthropic(BaseChatModel): for detail. - !!! note title="Token-efficient tool use (beta)" + !!! note "Token-efficient tool use (beta)" See LangChain [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic) for more detail. @@ -1263,7 +1263,7 @@ class ChatAnthropic(BaseChatModel): Total tokens: 408 ``` - !!! note title="Context management" + !!! note "Context management" Anthropic supports a context editing feature that will automatically manage the model's context window (e.g., by clearing tool results). @@ -1283,7 +1283,7 @@ class ChatAnthropic(BaseChatModel): response = model_with_tools.invoke("Search for recent developments in AI") ``` - !!! note title="Built-in tools" + !!! note "Built-in tools" See LangChain [docs](https://docs.langchain.com/oss/python/integrations/chat/anthropic#built-in-tools) for more detail. @@ -1410,7 +1410,7 @@ class ChatAnthropic(BaseChatModel): response = model_with_tools.invoke("What are my interests?") ``` - !!! note title="Response metadata" + !!! note "Response metadata" ```python ai_msg = model.invoke(messages) diff --git a/libs/partners/anthropic/langchain_anthropic/middleware/anthropic_tools.py b/libs/partners/anthropic/langchain_anthropic/middleware/anthropic_tools.py index eb19bc83383..f185e9f7e18 100644 --- a/libs/partners/anthropic/langchain_anthropic/middleware/anthropic_tools.py +++ b/libs/partners/anthropic/langchain_anthropic/middleware/anthropic_tools.py @@ -170,7 +170,7 @@ class _StateClaudeFileToolMiddleware(AgentMiddleware): allowed_path_prefixes: Sequence[str] | None = None, system_prompt: str | None = None, ) -> None: - """Initialize the middleware. + """Initialize. Args: tool_type: Tool type identifier. @@ -651,7 +651,7 @@ class _FilesystemClaudeFileToolMiddleware(AgentMiddleware): max_file_size_mb: int = 10, system_prompt: str | None = None, ) -> None: - """Initialize the middleware. + """Initialize. Args: tool_type: Tool type identifier. diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index 166d8c1e627..0dae21a19c3 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -2785,26 +2785,32 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override] To use custom parameters specific to these providers, use the `extra_body` parameter. - ```python title="LM Studio example with TTL (auto-eviction)" - from langchain_openai import ChatOpenAI + !!! example "LM Studio example with TTL (auto-eviction)" - model = ChatOpenAI( - base_url="http://localhost:1234/v1", - api_key="lm-studio", # Can be any string - model="mlx-community/QwQ-32B-4bit", - temperature=0, - extra_body={"ttl": 300}, # Auto-evict model after 5 minutes of inactivity - ) - ``` + ```python + from langchain_openai import ChatOpenAI - ```python title="vLLM example with custom parameters" - model = ChatOpenAI( - base_url="http://localhost:8000/v1", - api_key="EMPTY", - model="meta-llama/Llama-2-7b-chat-hf", - extra_body={"use_beam_search": True, "best_of": 4}, - ) - ``` + model = ChatOpenAI( + base_url="http://localhost:1234/v1", + api_key="lm-studio", # Can be any string + model="mlx-community/QwQ-32B-4bit", + temperature=0, + extra_body={ + "ttl": 300 + }, # Auto-evict model after 5 minutes of inactivity + ) + ``` + + !!! example "vLLM example with custom parameters" + + ```python + model = ChatOpenAI( + base_url="http://localhost:8000/v1", + api_key="EMPTY", + model="meta-llama/Llama-2-7b-chat-hf", + extra_body={"use_beam_search": True, "best_of": 4}, + ) + ``` ??? info "`model_kwargs` vs `extra_body`" diff --git a/libs/standard-tests/uv.lock b/libs/standard-tests/uv.lock index 4f24c7d54cd..52271485f21 100644 --- a/libs/standard-tests/uv.lock +++ b/libs/standard-tests/uv.lock @@ -297,7 +297,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.0.4" +version = "1.0.5" source = { editable = "../core" } dependencies = [ { name = "jsonpatch" },