Source code for langgraph_agent_toolkit.core.tool_selection

"""Configure optional LangChain tool selection without shared request state."""

from collections.abc import Awaitable, Callable, Sequence

from langchain.agents.middleware import AgentMiddleware, LLMToolSelectorMiddleware, ProviderToolSearchMiddleware
from langchain.agents.middleware.types import ModelRequest, ModelResponse
from langchain_core.language_models import BaseChatModel
from langchain_core.tools import BaseTool

from langgraph_agent_toolkit.core.tool_selection_config import ToolSelectionConfig


[docs] class ToolSelectionMiddleware(AgentMiddleware): """Adapt native selectors to the tools in each model request. Each call creates a native middleware instance. Required Deep Agents tools stay visible when they exist in that request. Concurrent calls do not change one another's tool lists or selection configuration. """ transformers = LLMToolSelectorMiddleware.transformers
[docs] def __init__( self, config: ToolSelectionConfig, *, selector_model: BaseChatModel | None = None, protected_tools: Sequence[str] = (), ) -> None: super().__init__() self.config = config.model_copy(deep=True) self.selector_model = selector_model self.protected_tools = frozenset(protected_tools)
def _prepare(self, request: ModelRequest) -> tuple[ModelRequest, AgentMiddleware | None]: if self.config.mode == "off" or not request.tools: return request, None base_tools = [item for item in request.tools if isinstance(item, BaseTool)] available = {item.name for item in base_tools} missing = set(self.config.always_include) - available if missing: raise ValueError(f"Tool selection always_include names are not available: {', '.join(sorted(missing))}") required = set(self.config.always_include) | (available & self.protected_tools) candidates = [item for item in base_tools if item.name not in required] if self.config.mode == "provider": tools = [ item.model_copy(update={"extras": {**item.extras, "defer_loading": False}}) if isinstance(item, BaseTool) and item.name in required and isinstance(item.extras, dict) and item.extras.get("defer_loading") is True else item for item in request.tools ] middleware = ProviderToolSearchMiddleware(searchable_tools=candidates) if candidates else None return request.override(tools=tools), middleware if len(candidates) <= self.config.max_tools: return request, None return request, LLMToolSelectorMiddleware( model=self.selector_model, max_tools=self.config.max_tools, always_include=sorted(required), max_retries=self.config.max_retries, on_parsing_failure=self.config.on_parsing_failure, )
[docs] def wrap_model_call(self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse: """Select tools for one synchronous model request.""" request, middleware = self._prepare(request) return middleware.wrap_model_call(request, handler) if middleware is not None else handler(request)
[docs] async def awrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], Awaitable[ModelResponse]] ) -> ModelResponse: """Select tools without blocking the event loop.""" request, middleware = self._prepare(request) if middleware is None: return await handler(request) return await middleware.awrap_model_call(request, handler)
[docs] def create_tool_selection_middleware( config: ToolSelectionConfig | None = None, *, selector_model: BaseChatModel | None = None, protected_tools: Sequence[str] = (), ) -> list[AgentMiddleware]: """Resolve settings and optional managed model configuration at graph setup.""" from langgraph_agent_toolkit.core.settings import settings config = config if config is not None else settings.TOOL_SELECTION if config.mode == "off": return [] if config.mode == "llm" and config.model_config_key is not None: if selector_model is not None: raise ValueError("Pass selector_model or model_config_key, not both") configured = settings.get_model_config(config.model_config_key) if configured is None: raise ValueError(f"Tool selection model_config_key is not in MODEL_CONFIGS: {config.model_config_key!r}") from langgraph_agent_toolkit.core.models.factory import CompletionModelFactory selector_model = CompletionModelFactory.get_model_from_config( configured, configurable_fields=(), config_prefix="" ) return [ToolSelectionMiddleware(config, selector_model=selector_model, protected_tools=protected_tools)]
__all__ = ["ToolSelectionConfig", "ToolSelectionMiddleware", "create_tool_selection_middleware"]