libs/core/langchain_core/tools/base.py PYTHON 1,712 lines View on github.com → Search inside
1"""Base classes and utilities for LangChain tools."""23from __future__ import annotations45import functools6import inspect7import json8import logging9import typing10import warnings11from abc import ABC, abstractmethod12from collections.abc import Callable, Mapping, Sequence13from inspect import signature14from typing import (15    TYPE_CHECKING,16    Annotated,17    Any,18    Literal,19    TypeVar,20    cast,21    get_args,22    get_origin,23    get_type_hints,24)2526import typing_extensions27from pydantic import (28    BaseModel,29    ConfigDict,30    Field,31    PrivateAttr,32    PydanticDeprecationWarning,33    SkipValidation,34    ValidationError,35    validate_arguments,36)37from pydantic.fields import FieldInfo38from pydantic.v1 import BaseModel as BaseModelV139from pydantic.v1 import ValidationError as ValidationErrorV140from pydantic.v1 import validate_arguments as validate_arguments_v141from typing_extensions import Self, override4243from langchain_core.callbacks import (44    AsyncCallbackManager,45    CallbackManager,46    Callbacks,47)48from langchain_core.messages.tool import ToolCall, ToolMessage, ToolOutputMixin49from langchain_core.runnables import (50    RunnableConfig,51    RunnableSerializable,52    ensure_config,53    patch_config,54    run_in_executor,55)56from langchain_core.runnables.config import set_config_context57from langchain_core.runnables.utils import coro_with_context58from langchain_core.utils.function_calling import (59    _parse_google_docstring,60    _py_38_safe_origin,61)62from langchain_core.utils.pydantic import (63    TypeBaseModel,64    _create_subset_model,65    get_fields,66    is_basemodel_subclass,67    is_pydantic_v1_subclass,68    is_pydantic_v2_subclass,69    model_json_schema,70)7172if TYPE_CHECKING:73    import uuid7475FILTERED_ARGS = ("run_manager", "callbacks")76TOOL_MESSAGE_BLOCK_TYPES = (77    "text",78    "image_url",79    "image",80    "json",81    "search_result",82    "custom_tool_call_output",83    "document",84    "file",85)8687_logger = logging.getLogger(__name__)888990class SchemaAnnotationError(TypeError):91    """Raised when `args_schema` is missing or has an incorrect type annotation."""929394def _is_annotated_type(typ: type[Any]) -> bool:95    """Check if a type is an `Annotated` type.9697    Args:98        typ: The type to check.99100    Returns:101        `True` if the type is an `Annotated` type, `False` otherwise.102    """103    return get_origin(typ) in {typing.Annotated, typing_extensions.Annotated}104105106def _get_annotation_description(arg_type: type) -> str | None:107    """Extract description from an `Annotated` type.108109    Checks for string annotations and `FieldInfo` objects with descriptions.110111    Args:112        arg_type: The type to extract description from.113114    Returns:115        The description string if found, `None` otherwise.116    """117    if _is_annotated_type(arg_type):118        annotated_args = get_args(arg_type)119        for annotation in annotated_args[1:]:120            if isinstance(annotation, str):121                return annotation122            if isinstance(annotation, FieldInfo) and annotation.description:123                return annotation.description124    return None125126127def _parse_python_function_docstring(128    function: Callable[..., Any],129    annotations: dict[str, Any],130    *,131    error_on_invalid_docstring: bool = False,132) -> tuple[str, dict[str, str]]:133    """Parse function and argument descriptions from a docstring.134135    Assumes the function docstring follows Google Python style guide.136137    Args:138        function: The function to parse the docstring from.139        annotations: Type annotations for the function parameters.140        error_on_invalid_docstring: Whether to raise an error on invalid docstring.141142    Returns:143        A tuple containing the function description and argument descriptions.144    """145    docstring = inspect.getdoc(function)146    return _parse_google_docstring(147        docstring,148        list(annotations),149        error_on_invalid_docstring=error_on_invalid_docstring,150    )151152153def _validate_docstring_args_against_annotations(154    arg_descriptions: dict[str, str], annotations: dict[str, Any]155) -> None:156    """Validate that docstring arguments match function annotations.157158    Args:159        arg_descriptions: Arguments described in the docstring.160        annotations: Type annotations from the function signature.161162    Raises:163        ValueError: If a docstring argument is not found in function signature.164    """165    for docstring_arg in arg_descriptions:166        if docstring_arg not in annotations:167            msg = f"Arg {docstring_arg} in docstring not found in function signature."168            raise ValueError(msg)169170171def _infer_arg_descriptions(172    fn: Callable[..., Any],173    *,174    parse_docstring: bool = False,175    error_on_invalid_docstring: bool = False,176) -> tuple[str, dict[str, str]]:177    """Infer argument descriptions from function docstring and annotations.178179    Args:180        fn: The function to infer descriptions from.181        parse_docstring: Whether to parse the docstring for descriptions.182        error_on_invalid_docstring: Whether to raise error on invalid docstring.183184    Returns:185        A tuple containing the function description and argument descriptions.186    """187    annotations = typing.get_type_hints(fn, include_extras=True)188    if parse_docstring:189        description, arg_descriptions = _parse_python_function_docstring(190            fn, annotations, error_on_invalid_docstring=error_on_invalid_docstring191        )192    else:193        description = inspect.getdoc(fn) or ""194        arg_descriptions = {}195    if parse_docstring:196        _validate_docstring_args_against_annotations(arg_descriptions, annotations)197    for arg, arg_type in annotations.items():198        if arg in arg_descriptions:199            continue200        if desc := _get_annotation_description(arg_type):201            arg_descriptions[arg] = desc202    return description, arg_descriptions203204205def _is_pydantic_annotation(annotation: Any, pydantic_version: str = "v2") -> bool:206    """Check if a type annotation is a Pydantic model.207208    Args:209        annotation: The type annotation to check.210        pydantic_version: The Pydantic version to check against (`'v1'` or `'v2'`).211212    Returns:213        `True` if the annotation is a Pydantic model, `False` otherwise.214    """215    base_model_class = BaseModelV1 if pydantic_version == "v1" else BaseModel216    try:217        return issubclass(annotation, base_model_class)218    except TypeError:219        return False220221222def _function_annotations_are_pydantic_v1(223    signature: inspect.Signature, func: Callable[..., Any]224) -> bool:225    """Check if all Pydantic annotations in a function are from v1.226227    Args:228        signature: The function signature to check.229        func: The function being checked.230231    Returns:232        True if all Pydantic annotations are from v1, `False` otherwise.233234    Raises:235        NotImplementedError: If the function contains mixed v1 and v2 annotations.236    """237    any_v1_annotations = any(238        _is_pydantic_annotation(parameter.annotation, pydantic_version="v1")239        for parameter in signature.parameters.values()240    )241    any_v2_annotations = any(242        _is_pydantic_annotation(parameter.annotation, pydantic_version="v2")243        for parameter in signature.parameters.values()244    )245    if any_v1_annotations and any_v2_annotations:246        msg = (247            f"Function {func} contains a mix of Pydantic v1 and v2 annotations. "248            "Only one version of Pydantic annotations per function is supported."249        )250        raise NotImplementedError(msg)251    return any_v1_annotations and not any_v2_annotations252253254class _SchemaConfig:255    """Configuration for Pydantic models generated from function signatures."""256257    extra: str = "forbid"258    """Whether to allow extra fields in the model."""259260    arbitrary_types_allowed: bool = True261    """Whether to allow arbitrary types in the model."""262263264def create_schema_from_function(265    model_name: str,266    func: Callable[..., Any],267    *,268    filter_args: Sequence[str] | None = None,269    parse_docstring: bool = False,270    error_on_invalid_docstring: bool = False,271    include_injected: bool = True,272) -> TypeBaseModel:273    """Create a Pydantic schema from a function's signature.274275    Args:276        model_name: Name to assign to the generated Pydantic schema.277        func: Function to generate the schema from.278        filter_args: Optional list of arguments to exclude from the schema.279280            Defaults to `FILTERED_ARGS`.281        parse_docstring: Whether to parse the function's docstring for descriptions282            for each argument.283        error_on_invalid_docstring: If `parse_docstring` is provided, configure284            whether to raise `ValueError` on invalid Google Style docstrings.285        include_injected: Whether to include injected arguments in the schema.286287            Defaults to `True`, since we want to include them in the schema when288            *validating* tool inputs.289290    Returns:291        A Pydantic model with the same arguments as the function.292    """293    sig = inspect.signature(func)294295    if _function_annotations_are_pydantic_v1(sig, func):296        validated = validate_arguments_v1(func, config=_SchemaConfig)  # type: ignore[call-overload]297    else:298        # https://docs.pydantic.dev/latest/usage/validation_decorator/299        with warnings.catch_warnings():300            # We are using deprecated functionality here.301            # This code should be re-written to simply construct a Pydantic model302            # using inspect.signature and create_model.303            warnings.simplefilter("ignore", category=PydanticDeprecationWarning)304            validated = validate_arguments(func, config=_SchemaConfig)  # type: ignore[operator]305306    # Let's ignore `self` and `cls` arguments for class and instance methods307    # If qualified name has a ".", then it likely belongs in a class namespace308    in_class = bool(func.__qualname__ and "." in func.__qualname__)309310    has_args = False311    has_kwargs = False312313    for param in sig.parameters.values():314        if param.kind == param.VAR_POSITIONAL:315            has_args = True316        elif param.kind == param.VAR_KEYWORD:317            has_kwargs = True318319    inferred_model = validated.model320321    if filter_args:322        filter_args_ = filter_args323    else:324        # Handle classmethods and instance methods325        existing_params: list[str] = list(sig.parameters.keys())326        if existing_params and existing_params[0] in {"self", "cls"} and in_class:327            filter_args_ = [existing_params[0], *list(FILTERED_ARGS)]328        else:329            filter_args_ = list(FILTERED_ARGS)330331        for existing_param in existing_params:332            if not include_injected and _is_injected_arg_type(333                sig.parameters[existing_param].annotation334            ):335                filter_args_.append(existing_param)336337    description, arg_descriptions = _infer_arg_descriptions(338        func,339        parse_docstring=parse_docstring,340        error_on_invalid_docstring=error_on_invalid_docstring,341    )342    # Pydantic adds placeholder virtual fields we need to strip343    valid_properties = []344    for field in get_fields(inferred_model):345        if not has_args and field == "args":346            continue347        if not has_kwargs and field == "kwargs":348            continue349350        if field == "v__duplicate_kwargs":  # Internal pydantic field351            continue352353        if field not in filter_args_:354            valid_properties.append(field)355356    return _create_subset_model(357        model_name,358        inferred_model,359        list(valid_properties),360        descriptions=arg_descriptions,361        fn_description=description,362    )363364365class ToolException(Exception):  # noqa: N818366    """Exception thrown when a tool execution error occurs.367368    This exception allows tools to signal errors without stopping the agent.369370    The error is handled according to the tool's `handle_tool_error` setting, and the371    result is returned as an observation to the agent.372    """373374375ArgsSchema = TypeBaseModel | dict[str, Any]376MessageContentBlock = str | dict[str, Any]377"""A single message content block: plain text or a structured block.378379A dict block is only considered valid at runtime when its `type` key is one of380`TOOL_MESSAGE_BLOCK_TYPES` (see `_is_message_content_block`); the static type381intentionally stays broad because block payloads vary by provider format.382"""383ToolExceptionHandlerOutput = str | Sequence[MessageContentBlock]384"""Content returned by a `handle_tool_error` callable.385386Error handlers may return plain text or a sequence of structured message387content blocks. When the original tool call includes a `tool_call_id`, this388content is normalized to the content of a `ToolMessage` with `status="error"`.389"""390391_EMPTY_SET: frozenset[str] = frozenset()392393394_TOOL_CALL_SCHEMA_FIELDS = frozenset({"name", "description", "args_schema"})395"""Fields the memoized `tool_call_schema` is built from; reassignment clears it."""396397398def _patch_json_schema_cache(model_cls: type) -> None:399    """Patch `model_json_schema` (or `schema` for pydantic v1) to cache.400401    Pydantic regenerates the full JSON-schema dict on every402    `model_json_schema()` call  there is no per-class cache.  When the403    model class is stable (memoized on a `BaseTool` instance), this patch404    caches the dict on the class so repeated calls return instantly.405406    Only calls with all-default arguments are cached; any explicit arguments407    bypass the cache and delegate to the original method.408    """409    method_name = (410        "model_json_schema" if hasattr(model_cls, "model_json_schema") else "schema"411    )412    orig = getattr(model_cls, method_name)413414    def _cached_json_schema(cls: type, *args: Any, **kwargs: Any) -> dict[str, Any]:415        if not args and not kwargs:416            cached = cls.__dict__.get("_json_schema_cache")417            if cached is not None:418                return cast("dict[str, Any]", cached)419        result = orig(*args, **kwargs)420        if not args and not kwargs:421            cls._json_schema_cache = result  # type: ignore[attr-defined]422        return cast("dict[str, Any]", result)423424    setattr(model_cls, method_name, classmethod(_cached_json_schema))425426427class BaseTool(RunnableSerializable[str | dict[str, Any] | ToolCall, Any]):428    """Base class for all LangChain tools.429430    This abstract class defines the interface that all LangChain tools must implement.431432    Tools are components that can be called by agents to perform specific actions.433    """434435    def __init_subclass__(cls, **kwargs: Any) -> None:436        """Validate the tool class definition during subclass creation.437438        Args:439            **kwargs: Additional keyword arguments passed to the parent class.440441        Raises:442            SchemaAnnotationError: If `args_schema` has incorrect type annotation.443        """444        super().__init_subclass__(**kwargs)445446        args_schema_type = cls.__annotations__.get("args_schema", None)447448        if args_schema_type is not None and args_schema_type == BaseModel:449            # Throw errors for common mis-annotations.450            # TODO: Use get_args / get_origin and fully451            # specify valid annotations.452            typehint_mandate = """453class ChildTool(BaseTool):454    ...455    args_schema: Type[BaseModel] = SchemaClass456    ..."""457            name = cls.__name__458            msg = (459                f"Tool definition for {name} must include valid type annotations"460                f" for argument 'args_schema' to behave as expected.\n"461                f"Expected annotation of 'Type[BaseModel]'"462                f" but got '{args_schema_type}'.\n"463                f"Expected class looks like:\n"464                f"{typehint_mandate}"465            )466            raise SchemaAnnotationError(msg)467468    name: str469    """The unique name of the tool that clearly communicates its purpose."""470471    description: str472    """Used to tell the model how/when/why to use the tool.473474    You can provide few-shot examples as a part of the description.475    """476477    args_schema: Annotated[ArgsSchema | None, SkipValidation()] = Field(478        default=None, description="The tool schema."479    )480    """Pydantic model class to validate and parse the tool's input arguments.481482    Args schema should be either:483484    - A subclass of `pydantic.BaseModel`.485    - A subclass of `pydantic.v1.BaseModel` if accessing v1 namespace in pydantic 2486    - A JSON schema dict487    """488489    return_direct: bool = False490    """Whether to return the tool's output directly.491492    Setting this to `True` means that after the tool is called, the `AgentExecutor` will493    stop looping.494    """495496    verbose: bool = False497    """Whether to log the tool's progress."""498499    callbacks: Callbacks = Field(default=None, exclude=True)500    """Callbacks to be called during tool execution."""501502    tags: list[str] | None = None503    """Optional list of tags associated with the tool.504505    These tags will be associated with each call to this tool,506    and passed as arguments to the handlers defined in `callbacks`.507508    You can use these to, e.g., identify a specific instance of a tool with its use509    case.510    """511512    metadata: dict[str, Any] | None = None513    """Optional metadata associated with the tool.514515    This metadata will be associated with each call to this tool,516    and passed as arguments to the handlers defined in `callbacks`.517518    You can use these to, e.g., identify a specific instance of a tool with its usecase.519    """520521    handle_tool_error: (522        bool | str | Callable[[ToolException], ToolExceptionHandlerOutput] | None523    ) = False524    """Handle `ToolException` raised by tool execution.525526    If `False`, the exception is re-raised. If `True`, the exception message is527    returned as tool output. If a string is passed, that string is returned528    as tool output. If a callable is passed, it receives the exception and529    its return value is used as the tool output.530531    Callable handlers may return either a string or a list of message532    content blocks. If the tool was invoked with a `tool_call_id`, the handled533    content is wrapped in a `ToolMessage` with `status="error"`.534    """535536    handle_validation_error: (537        bool | str | Callable[[ValidationError | ValidationErrorV1], str] | None538    ) = False539    """Handle the content of the `ValidationError` thrown."""540541    response_format: Literal["content", "content_and_artifact"] = "content"542    """The tool response format.543544    If `'content'` then the output of the tool is interpreted as the contents of a545    `ToolMessage`. If `'content_and_artifact'` then the output is expected to be a546    two-tuple corresponding to the `(content, artifact)` of a `ToolMessage`.547    """548549    extras: dict[str, Any] | None = None550    """Optional provider-specific extra fields for the tool.551552    This is used to pass provider-specific configuration that doesn't fit into553    standard tool fields.554555    Example:556        Anthropic-specific fields like [`cache_control`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#prompt-caching),557        [`defer_loading`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#tool-search),558        or `input_examples`.559560        ```python561        @tool(extras={"defer_loading": True, "cache_control": {"type": "ephemeral"}})562        def my_tool(x: str) -> str:563            return x564        ```565    """566567    def __init__(self, **kwargs: Any) -> None:568        """Initialize the tool.569570        Raises:571            TypeError: If `args_schema` is not a subclass of pydantic `BaseModel` or572                `dict`.573        """574        if (575            "args_schema" in kwargs576            and kwargs["args_schema"] is not None577            and not is_basemodel_subclass(kwargs["args_schema"])578            and not isinstance(kwargs["args_schema"], dict)579        ):580            msg = (581                "args_schema must be a subclass of pydantic BaseModel or "582                f"a JSON schema dict. Got: {kwargs['args_schema']}."583            )584            raise TypeError(msg)585        super().__init__(**kwargs)586587    model_config = ConfigDict(588        arbitrary_types_allowed=True,589    )590591    @property592    def is_single_input(self) -> bool:593        """Check if the tool accepts only a single input argument.594595        Returns:596            `True` if the tool has only one input argument, `False` otherwise.597        """598        keys = {k for k in self.args if k != "kwargs"}599        return len(keys) == 1600601    @property602    def args(self) -> dict[str, Any]:603        """Get the tool's input arguments schema.604605        Returns:606            `dict` containing the tool's argument properties.607        """608        if isinstance(self.args_schema, dict):609            json_schema = self.args_schema610        else:611            input_schema = self.tool_call_schema612            if isinstance(input_schema, dict):613                json_schema = input_schema614            else:615                json_schema = model_json_schema(input_schema)616        return cast("dict[str, Any]", json_schema["properties"])617618    _tool_call_schema_memo: ArgsSchema | None = PrivateAttr(default=None)619    """Memoized `tool_call_schema` result.620621    Building the subset model is expensive, and pydantic does not cache622    `model_json_schema()` per class, so agent loops would otherwise pay full623    schema generation for every tool on every model call. The subset model624    class is memoized here and its `model_json_schema`/`schema` method is625    patched to cache the generated dict, so both costs are paid only once per626    tool instance.627    Cleared whenever `name`, `description`, or `args_schema` is reassigned (see628    `__setattr__` and `model_copy`).629    """630631    @override632    def __setattr__(self, name: str, value: Any) -> None:633        """Clear the tool-call schema memo when an input to it is reassigned."""634        super().__setattr__(name, value)635        if name in _TOOL_CALL_SCHEMA_FIELDS and self.__pydantic_private__ is not None:636            self._tool_call_schema_memo = None637638    @override639    def model_copy(640        self, *, update: Mapping[str, Any] | None = None, deep: bool = False641    ) -> Self:642        """Copy the tool, clearing the schema memo if `update` affects it.643644        `model_copy` writes `update` directly to the copy's `__dict__` without645        going through `__setattr__`, and private attributes (including the646        memo) carry over to the copy, so the memo is cleared here when the647        update touches one of the fields the schema is built from.648        """649        copied = super().model_copy(update=update, deep=deep)650        if update and not _TOOL_CALL_SCHEMA_FIELDS.isdisjoint(update):651            copied._tool_call_schema_memo = None  # noqa: SLF001652        return copied653654    def __getstate__(self) -> dict[Any, Any]:655        """Drop the tool-call schema memo when pickling.656657        The memoized subset model is a dynamically created class that cannot be658        pickled by reference; it is rebuilt lazily on next access.659        """660        state = super().__getstate__()661        private = state.get("__pydantic_private__")662        if private and private.get("_tool_call_schema_memo") is not None:663            state = dict(state)664            state["__pydantic_private__"] = {665                **private,666                "_tool_call_schema_memo": None,667            }668        return state669670    @property671    def tool_call_schema(self) -> ArgsSchema:672        """Get the schema for tool calls, excluding injected arguments.673674        Returns:675            The schema that should be used for tool calls from language models.676677            The returned model class is memoized per tool instance (invalidated678            when `name`, `description`, or `args_schema` is reassigned) so679            repeated access does not regenerate the class. The class's680            `model_json_schema` method is also patched to cache the generated681            schema dict, since pydantic does not cache it per class.682        """683        if isinstance(self.args_schema, dict):684            if self.description:685                return {686                    **self.args_schema,687                    "description": self.description,688                }689690            return self.args_schema691692        if (memo := self._tool_call_schema_memo) is not None:693            return memo694695        full_schema = self.get_input_schema()696        fields = []697        for name, type_ in get_all_basemodel_annotations(full_schema).items():698            if not _is_injected_arg_type(type_):699                fields.append(name)700        subset_model = _create_subset_model(701            self.name, full_schema, fields, fn_description=self.description702        )703        _patch_json_schema_cache(subset_model)704        self._tool_call_schema_memo = subset_model705        return subset_model706707    @functools.cached_property708    def _injected_args_keys(self) -> frozenset[str]:709        # Base implementation doesn't manage injected args710        return _EMPTY_SET711712    # --- Runnable ---713714    @override715    def get_input_schema(self, config: RunnableConfig | None = None) -> TypeBaseModel:716        """The tool's input schema.717718        Args:719            config: The configuration for the tool.720721        Returns:722            The input schema for the tool.723        """724        if self.args_schema is not None:725            if isinstance(self.args_schema, dict):726                return super().get_input_schema(config)727            return self.args_schema728        return create_schema_from_function(self.name, self._run)729730    @override731    def invoke(732        self,733        input: str | dict[str, Any] | ToolCall,734        config: RunnableConfig | None = None,735        **kwargs: Any,736    ) -> Any:737        tool_input, kwargs = _prep_run_args(input, config, **kwargs)738        return self.run(tool_input, **kwargs)739740    @override741    async def ainvoke(742        self,743        input: str | dict[str, Any] | ToolCall,744        config: RunnableConfig | None = None,745        **kwargs: Any,746    ) -> Any:747        tool_input, kwargs = _prep_run_args(input, config, **kwargs)748        return await self.arun(tool_input, **kwargs)749750    # --- Tool ---751752    def _parse_input(753        self, tool_input: str | dict[str, Any], tool_call_id: str | None754    ) -> str | dict[str, Any]:755        """Parse and validate tool input using the args schema.756757        Args:758            tool_input: The raw input to the tool.759            tool_call_id: The ID of the tool call, if available.760761        Returns:762            The parsed and validated input.763764        Raises:765            ValueError: If `string` input is provided with JSON schema `args_schema`.766            ValueError: If `InjectedToolCallId` is required but `tool_call_id` is not767                provided.768            TypeError: If `args_schema` is not a Pydantic `BaseModel` or dict.769        """770        input_args = self.args_schema771772        if isinstance(tool_input, str):773            if input_args is not None:774                if isinstance(input_args, dict):775                    msg = (776                        "String tool inputs are not allowed when "777                        "using tools with JSON schema args_schema."778                    )779                    raise ValueError(msg)780                key_ = next(iter(get_fields(input_args).keys()))781                if issubclass(input_args, BaseModel):782                    input_args.model_validate({key_: tool_input})783                elif issubclass(input_args, BaseModelV1):784                    input_args.parse_obj({key_: tool_input})785                else:786                    msg = f"args_schema must be a Pydantic BaseModel, got {input_args}"  # type: ignore[unreachable]787                    raise TypeError(msg)788            return tool_input789790        if input_args is not None:791            if isinstance(input_args, dict):792                return tool_input793            result: BaseModel | BaseModelV1794            if issubclass(input_args, BaseModel):795                # Check args_schema for InjectedToolCallId796                for k, v in get_all_basemodel_annotations(input_args).items():797                    if _is_injected_arg_type(v, injected_type=InjectedToolCallId):798                        if tool_call_id is None:799                            msg = (800                                "When tool includes an InjectedToolCallId "801                                "argument, tool must always be invoked with a full "802                                "model ToolCall of the form: {'args': {...}, "803                                "'name': '...', 'type': 'tool_call', "804                                "'tool_call_id': '...'}"805                            )806                            raise ValueError(msg)807                        tool_input[k] = tool_call_id808                result_v2 = input_args.model_validate(tool_input)809                result_dict = result_v2.model_dump()810                result = result_v2811            elif issubclass(input_args, BaseModelV1):812                # Check args_schema for InjectedToolCallId813                for k, v in get_all_basemodel_annotations(input_args).items():814                    if _is_injected_arg_type(v, injected_type=InjectedToolCallId):815                        if tool_call_id is None:816                            msg = (817                                "When tool includes an InjectedToolCallId "818                                "argument, tool must always be invoked with a full "819                                "model ToolCall of the form: {'args': {...}, "820                                "'name': '...', 'type': 'tool_call', "821                                "'tool_call_id': '...'}"822                            )823                            raise ValueError(msg)824                        tool_input[k] = tool_call_id825                result_v1 = input_args.parse_obj(tool_input)826                result_dict = result_v1.dict()827                result = result_v1828            else:829                msg = (  # type: ignore[unreachable]830                    f"args_schema must be a Pydantic BaseModel, got {self.args_schema}"831                )832                raise NotImplementedError(msg)833834            # Include fields from tool_input, plus fields with explicit defaults.835            # This applies Pydantic defaults (like Field(default=1)) while excluding836            # synthetic "args"/"kwargs" fields that Pydantic creates for *args/**kwargs.837            field_info = get_fields(input_args)838            validated_input = {}839            for k in result_dict:840                if k in tool_input:841                    # Field was provided in input - include it (validated)842                    validated_input[k] = getattr(result, k)843                elif k in field_info and k not in {"args", "kwargs"}:844                    # Check if field has an explicit default defined in the schema.845                    # Exclude "args"/"kwargs" as these are synthetic fields for variadic846                    # parameters that should not be passed as keyword arguments.847                    fi = field_info[k]848                    # Pydantic v2 uses is_required() method, v1 uses required attribute849                    has_default = (850                        not fi.is_required()851                        if hasattr(fi, "is_required")852                        else not getattr(fi, "required", True)853                    )854                    if has_default:855                        validated_input[k] = getattr(result, k)856857            for k in self._injected_args_keys:858                if k in tool_input:859                    validated_input[k] = tool_input[k]860                elif k == "tool_call_id":861                    if tool_call_id is None:862                        msg = (863                            "When tool includes an InjectedToolCallId "864                            "argument, tool must always be invoked with a full "865                            "model ToolCall of the form: {'args': {...}, "866                            "'name': '...', 'type': 'tool_call', "867                            "'tool_call_id': '...'}"868                        )869                        raise ValueError(msg)870                    validated_input[k] = tool_call_id871872            return validated_input873874        return tool_input875876    @abstractmethod877    def _run(self, *args: Any, **kwargs: Any) -> Any:878        """Use the tool.879880        Add `run_manager: CallbackManagerForToolRun | None = None` to child881        implementations to enable tracing.882883        Returns:884            The result of the tool execution.885        """886887    async def _arun(self, *args: Any, **kwargs: Any) -> Any:888        """Use the tool asynchronously.889890        Add `run_manager: AsyncCallbackManagerForToolRun | None = None` to child891        implementations to enable tracing.892893        Returns:894            The result of the tool execution.895        """896        if kwargs.get("run_manager") and signature(self._run).parameters.get(897            "run_manager"898        ):899            kwargs["run_manager"] = kwargs["run_manager"].get_sync()900        return await run_in_executor(None, self._run, *args, **kwargs)901902    def _filter_injected_args(self, tool_input: dict[str, Any]) -> dict[str, Any]:903        """Filter out injected tool arguments from the input dictionary.904905        Injected arguments are those annotated with `InjectedToolArg` or its906        subclasses, or arguments in `FILTERED_ARGS` like `run_manager` and callbacks.907908        Args:909            tool_input: The tool input dictionary to filter.910911        Returns:912            A filtered dictionary with injected arguments removed.913        """914        # Start with filtered args from the constant915        filtered_keys = set[str](FILTERED_ARGS)916917        # Add injected args from function signature (e.g., ToolRuntime parameters)918        filtered_keys.update(self._injected_args_keys)919920        # If we have an args_schema, use it to identify injected args921        # Skip if args_schema is a dict (JSON Schema) as it's not a Pydantic model922        if self.args_schema is not None and not isinstance(self.args_schema, dict):923            try:924                annotations = get_all_basemodel_annotations(self.args_schema)925                for field_name, field_type in annotations.items():926                    if _is_injected_arg_type(field_type):927                        filtered_keys.add(field_name)928            except Exception:929                # If we can't get annotations, just use FILTERED_ARGS930                _logger.debug(931                    "Failed to get args_schema annotations for filtering.",932                    exc_info=True,933                )934935        # Filter out the injected keys from tool_input936        return {k: v for k, v in tool_input.items() if k not in filtered_keys}937938    def _to_args_and_kwargs(939        self, tool_input: str | dict[str, Any], tool_call_id: str | None940    ) -> tuple[tuple[str, ...], dict[str, Any]]:941        """Convert tool input to positional and keyword arguments.942943        Args:944            tool_input: The input to the tool.945            tool_call_id: The ID of the tool call, if available.946947        Returns:948            A tuple of `(positional_args, keyword_args)` for the tool.949950        Raises:951            TypeError: If the tool input type is invalid.952        """953        if (954            self.args_schema is not None955            and isinstance(self.args_schema, type)956            and is_basemodel_subclass(self.args_schema)957            and not get_fields(self.args_schema)958        ):959            # StructuredTool with no args960            return (), {}961        tool_input = self._parse_input(tool_input, tool_call_id)962        # For backwards compatibility, if run_input is a string,963        # pass as a positional argument.964        if isinstance(tool_input, str):965            return (tool_input,), {}966        if isinstance(tool_input, dict):967            # Make a shallow copy of the input to allow downstream code968            # to modify the root level of the input without affecting the969            # original input.970            # This is used by the tool to inject run time information like971            # the callback manager.972            return (), tool_input.copy()973        # This code path is not expected to be reachable.974        msg = f"Invalid tool input type: {type(tool_input)}"  # type: ignore[unreachable]975        raise TypeError(msg)976977    def run(978        self,979        tool_input: str | dict[str, Any],980        verbose: bool | None = None,  # noqa: FBT001981        start_color: str | None = "green",982        color: str | None = "green",983        callbacks: Callbacks = None,984        *,985        tags: list[str] | None = None,986        metadata: dict[str, Any] | None = None,987        run_name: str | None = None,988        run_id: uuid.UUID | None = None,989        config: RunnableConfig | None = None,990        tool_call_id: str | None = None,991        **kwargs: Any,992    ) -> Any:993        """Run the tool.994995        Args:996            tool_input: The input to the tool.997            verbose: Whether to log the tool's progress.998            start_color: The color to use when starting the tool.999            color: The color to use when ending the tool.1000            callbacks: Callbacks to be called during tool execution.1001            tags: Optional list of tags associated with the tool.1002            metadata: Optional metadata associated with the tool.1003            run_name: The name of the run.1004            run_id: The id of the run.1005            config: The configuration for the tool.1006            tool_call_id: The id of the tool call.1007            **kwargs: Keyword arguments to be passed to tool callbacks (event handler)10081009        Returns:1010            The output of the tool.10111012        Raises:1013            ToolException: If an error occurs during tool execution.1014        """1015        callback_manager = CallbackManager.configure(1016            callbacks,1017            self.callbacks,1018            self.verbose or bool(verbose),1019            tags,1020            self.tags,1021            metadata,1022            self.metadata,1023        )10241025        # Filter out injected arguments from callback inputs1026        filtered_tool_input = (1027            self._filter_injected_args(tool_input)1028            if isinstance(tool_input, dict)1029            else None1030        )10311032        # Use filtered inputs for the input_str parameter as well1033        tool_input_str = (1034            tool_input1035            if isinstance(tool_input, str)1036            else str(1037                filtered_tool_input if filtered_tool_input is not None else tool_input1038            )1039        )10401041        run_manager = callback_manager.on_tool_start(1042            {"name": self.name, "description": self.description},1043            tool_input_str,1044            color=start_color,1045            name=run_name,1046            run_id=run_id,1047            inputs=filtered_tool_input,1048            tool_call_id=tool_call_id,1049            **kwargs,1050        )10511052        content = None1053        artifact = None1054        status = "success"1055        error_to_raise: Exception | KeyboardInterrupt | None = None1056        try:1057            child_config = patch_config(config, callbacks=run_manager.get_child())1058            with set_config_context(child_config) as context:1059                tool_args, tool_kwargs = self._to_args_and_kwargs(1060                    tool_input, tool_call_id1061                )1062                if signature(self._run).parameters.get("run_manager"):1063                    tool_kwargs |= {"run_manager": run_manager}1064                if config_param := _get_runnable_config_param(self._run):1065                    tool_kwargs |= {config_param: config}1066                response = context.run(self._run, *tool_args, **tool_kwargs)1067            if self.response_format == "content_and_artifact":1068                msg = (1069                    "Since response_format='content_and_artifact' "1070                    "a two-tuple of the message content and raw tool output is "1071                    f"expected. Instead, generated response is of type: "1072                    f"{type(response)}."1073                )1074                if not isinstance(response, tuple):1075                    error_to_raise = ValueError(msg)1076                else:1077                    try:1078                        content, artifact = response1079                    except ValueError:1080                        error_to_raise = ValueError(msg)1081            else:1082                content = response1083        except (ValidationError, ValidationErrorV1) as e:1084            if not self.handle_validation_error:1085                error_to_raise = e1086            else:1087                content = _handle_validation_error(e, flag=self.handle_validation_error)1088                status = "error"1089        except ToolException as e:1090            if not self.handle_tool_error:1091                error_to_raise = e1092            else:1093                content = _handle_tool_error(e, flag=self.handle_tool_error)1094                status = "error"1095        except (Exception, KeyboardInterrupt) as e:1096            error_to_raise = e10971098        if error_to_raise:1099            run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)1100            raise error_to_raise1101        output = _format_output(content, artifact, tool_call_id, self.name, status)1102        run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)1103        return output11041105    async def arun(1106        self,1107        tool_input: str | dict[str, Any],1108        verbose: bool | None = None,  # noqa: FBT0011109        start_color: str | None = "green",1110        color: str | None = "green",1111        callbacks: Callbacks = None,1112        *,1113        tags: list[str] | None = None,1114        metadata: dict[str, Any] | None = None,1115        run_name: str | None = None,1116        run_id: uuid.UUID | None = None,1117        config: RunnableConfig | None = None,1118        tool_call_id: str | None = None,1119        **kwargs: Any,1120    ) -> Any:1121        """Run the tool asynchronously.11221123        Args:1124            tool_input: The input to the tool.1125            verbose: Whether to log the tool's progress.1126            start_color: The color to use when starting the tool.1127            color: The color to use when ending the tool.1128            callbacks: Callbacks to be called during tool execution.1129            tags: Optional list of tags associated with the tool.1130            metadata: Optional metadata associated with the tool.1131            run_name: The name of the run.1132            run_id: The id of the run.1133            config: The configuration for the tool.1134            tool_call_id: The id of the tool call.1135            **kwargs: Keyword arguments to be passed to tool callbacks11361137        Returns:1138            The output of the tool.11391140        Raises:1141            ToolException: If an error occurs during tool execution.1142        """1143        callback_manager = AsyncCallbackManager.configure(1144            callbacks,1145            self.callbacks,1146            self.verbose or bool(verbose),1147            tags,1148            self.tags,1149            metadata,1150            self.metadata,1151        )11521153        # Filter out injected arguments from callback inputs1154        filtered_tool_input = (1155            self._filter_injected_args(tool_input)1156            if isinstance(tool_input, dict)1157            else None1158        )11591160        # Use filtered inputs for the input_str parameter as well1161        tool_input_str = (1162            tool_input1163            if isinstance(tool_input, str)1164            else str(1165                filtered_tool_input if filtered_tool_input is not None else tool_input1166            )1167        )11681169        run_manager = await callback_manager.on_tool_start(1170            {"name": self.name, "description": self.description},1171            tool_input_str,1172            color=start_color,1173            name=run_name,1174            run_id=run_id,1175            inputs=filtered_tool_input,1176            tool_call_id=tool_call_id,1177            **kwargs,1178        )1179        content = None1180        artifact = None1181        status = "success"1182        error_to_raise: Exception | KeyboardInterrupt | None = None1183        try:1184            tool_args, tool_kwargs = self._to_args_and_kwargs(tool_input, tool_call_id)1185            child_config = patch_config(config, callbacks=run_manager.get_child())1186            with set_config_context(child_config) as context:1187                func_to_check = (1188                    self._run if self.__class__._arun is BaseTool._arun else self._arun  # noqa: SLF0011189                )1190                if signature(func_to_check).parameters.get("run_manager"):1191                    tool_kwargs["run_manager"] = run_manager1192                if config_param := _get_runnable_config_param(func_to_check):1193                    tool_kwargs[config_param] = config11941195                coro = self._arun(*tool_args, **tool_kwargs)1196                response = await coro_with_context(coro, context)1197            if self.response_format == "content_and_artifact":1198                msg = (1199                    "Since response_format='content_and_artifact' "1200                    "a two-tuple of the message content and raw tool output is "1201                    f"expected. Instead, generated response is of type: "1202                    f"{type(response)}."1203                )1204                if not isinstance(response, tuple):1205                    error_to_raise = ValueError(msg)1206                else:1207                    try:1208                        content, artifact = response1209                    except ValueError:1210                        error_to_raise = ValueError(msg)1211            else:1212                content = response1213        except ValidationError as e:1214            if not self.handle_validation_error:1215                error_to_raise = e1216            else:1217                content = _handle_validation_error(e, flag=self.handle_validation_error)1218                status = "error"1219        except ToolException as e:1220            if not self.handle_tool_error:1221                error_to_raise = e1222            else:1223                content = _handle_tool_error(e, flag=self.handle_tool_error)1224                status = "error"1225        except (Exception, KeyboardInterrupt) as e:1226            error_to_raise = e12271228        if error_to_raise:1229            await run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)1230            raise error_to_raise12311232        output = _format_output(content, artifact, tool_call_id, self.name, status)1233        await run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)1234        return output123512361237def _is_tool_call(x: Any) -> bool:1238    """Check if the input is a tool call dictionary.12391240    Args:1241        x: The input to check.12421243    Returns:1244        `True` if the input is a tool call, `False` otherwise.1245    """1246    return isinstance(x, dict) and x.get("type") == "tool_call"124712481249def _handle_validation_error(1250    e: ValidationError | ValidationErrorV1,1251    *,1252    flag: Literal[True] | str | Callable[[ValidationError | ValidationErrorV1], str],1253) -> str:1254    """Handle validation errors based on the configured flag.12551256    Args:1257        e: The validation error that occurred.1258        flag: How to handle the error (`bool`, `str`, or `Callable`).12591260    Returns:1261        The error message to return.12621263    Raises:1264        ValueError: If the flag type is unexpected.1265    """1266    if isinstance(flag, bool):1267        content = "Tool input validation error"1268    elif isinstance(flag, str):1269        content = flag1270    elif callable(flag):1271        content = flag(e)1272    else:1273        msg = (  # type: ignore[unreachable]1274            f"Got unexpected type of `handle_validation_error`. Expected bool, "1275            f"str or callable. Received: {flag}"1276        )1277        raise ValueError(msg)  # noqa: TRY0041278    return content127912801281def _handle_tool_error(1282    e: ToolException,1283    *,1284    flag: Literal[True]1285    | str1286    | Callable[[ToolException], ToolExceptionHandlerOutput]1287    | None,1288) -> ToolExceptionHandlerOutput:1289    """Convert a `ToolException` into handled tool output content.12901291    Args:1292        e: The tool exception that occurred.1293        flag: How to handle the error. `True` uses the exception message, a string1294            replaces the message, and a callable computes replacement content from1295            the exception.12961297    Returns:1298        The handled error content. This may be plain text or structured message1299            content blocks; callers pass it through normal tool1300            output formatting.13011302    Raises:1303        ValueError: If the flag type is unexpected.1304    """1305    if isinstance(flag, bool):1306        content = e.args[0] if e.args else "Tool execution error"1307    elif isinstance(flag, str):1308        content = flag1309    elif callable(flag):1310        content = flag(e)1311    else:1312        msg = (1313            f"Got unexpected type of `handle_tool_error`. Expected bool, str "1314            f"or callable. Received: {flag}"1315        )1316        raise ValueError(msg)  # noqa: TRY0041317    return content131813191320def _prep_run_args(1321    value: str | dict[str, Any] | ToolCall,1322    config: RunnableConfig | None,1323    **kwargs: Any,1324) -> tuple[str | dict[str, Any], dict[str, Any]]:1325    """Prepare arguments for tool execution.13261327    Args:1328        value: The input value (`str`, `dict`, or `ToolCall`).1329        config: The runnable configuration.1330        **kwargs: Additional keyword arguments.13311332    Returns:1333        A tuple of `(tool_input, run_kwargs)`.1334    """1335    config = ensure_config(config)1336    tool_input: str | dict[str, Any]1337    if _is_tool_call(value):1338        tool_call_id: str | None = cast("ToolCall", value)["id"]1339        tool_input = cast("ToolCall", value)["args"].copy()1340    else:1341        tool_call_id = None1342        tool_input = cast("str | dict[str, Any]", value)1343    return (1344        tool_input,1345        dict(1346            callbacks=config.get("callbacks"),1347            tags=config.get("tags"),1348            metadata=config.get("metadata"),1349            run_name=config.get("run_name"),1350            run_id=config.pop("run_id", None),1351            config=config,1352            tool_call_id=tool_call_id,1353            **kwargs,1354        ),1355    )135613571358def _format_output(1359    content: Any,1360    artifact: Any,1361    tool_call_id: str | None,1362    name: str,1363    status: str,1364) -> ToolOutputMixin | Any:1365    """Format tool output as a `ToolMessage` if appropriate.13661367    Args:1368        content: The main content of the tool output.1369        artifact: Any artifact data from the tool.1370        tool_call_id: The ID of the tool call.1371        name: The name of the tool.1372        status: The execution status.13731374    Returns:1375        The formatted output, either as a `ToolMessage`, the original content,1376        or an unchanged list of `ToolOutputMixin` instances.1377    """1378    if (1379        isinstance(content, list)1380        and content1381        and all(isinstance(item, ToolOutputMixin) for item in content)1382    ):1383        return content1384    if isinstance(content, ToolOutputMixin) or tool_call_id is None:1385        return content1386    normalized_content = _normalize_message_content(content)1387    content = _stringify(content) if normalized_content is None else normalized_content1388    return ToolMessage(1389        content,1390        artifact=artifact,1391        tool_call_id=tool_call_id,1392        name=name,1393        status=status,1394    )139513961397def _normalize_message_content(obj: Any) -> str | list[MessageContentBlock] | None:1398    """Coerce valid message content to the shape expected by `ToolMessage`.13991400    A string passes through unchanged; any `Sequence` of valid content blocks1401    (e.g. a list or tuple) is materialized into a `list`. Returning `None`1402    signals the caller (`_format_output`) that `obj` is not message content and1403    should be stringified instead.14041405    Args:1406        obj: The object to normalize.14071408    Returns:1409        The normalized content, or `None` if `obj` is not valid message content.1410    """1411    if isinstance(obj, str):1412        return obj1413    # Validate lazily before materializing: `all` short-circuits on the first1414    # invalid element, so a large non-content sequence (e.g. `range(10**12)`)1415    # falls back to stringification without allocating it.1416    if isinstance(obj, Sequence) and all(_is_message_content_block(e) for e in obj):1417        return list(obj)1418    return None141914201421def _is_message_content_block(obj: Any) -> bool:1422    """Check if object is a valid message content block.14231424    Validates content blocks for OpenAI or Anthropic format.14251426    Args:1427        obj: The object to check.14281429    Returns:1430        `True` if the object is a valid content block, `False` otherwise.1431    """1432    if isinstance(obj, str):1433        return True1434    if isinstance(obj, dict):1435        return obj.get("type", None) in TOOL_MESSAGE_BLOCK_TYPES1436    return False143714381439def _stringify(content: Any) -> str:1440    """Convert content to string, preferring JSON format.14411442    Args:1443        content: The content to stringify.14441445    Returns:1446        String representation of the content.1447    """1448    try:1449        return json.dumps(content, ensure_ascii=False)1450    except Exception:1451        return str(content)145214531454def _get_type_hints(func: Callable[..., Any]) -> dict[str, type] | None:1455    """Get type hints from a function, handling partial functions.14561457    Args:1458        func: The function to get type hints from.14591460    Returns:1461        `dict` of type hints, or `None` if extraction fails.1462    """1463    if isinstance(func, functools.partial):1464        func = func.func1465    try:1466        return get_type_hints(func)1467    except Exception:1468        return None146914701471def _get_runnable_config_param(func: Callable[..., Any]) -> str | None:1472    """Find the parameter name for `RunnableConfig` in a function.14731474    Args:1475        func: The function to check.14761477    Returns:1478        The parameter name for `RunnableConfig`, or `None` if not found.1479    """1480    type_hints = _get_type_hints(func)1481    if not type_hints:1482        return None1483    for name, type_ in type_hints.items():1484        if type_ is RunnableConfig:1485            return name1486    return None148714881489class InjectedToolArg:1490    """Annotation for tool arguments that are injected at runtime.14911492    Tool arguments annotated with this class are not included in the tool1493    schema sent to language models and are instead injected during execution.1494    """149514961497class _DirectlyInjectedToolArg:1498    """Annotation for tool arguments that are injected at runtime.14991500    Injected via direct type annotation, rather than annotated metadata.15011502    For example, `ToolRuntime` is a directly injected argument.15031504    Note the direct annotation rather than the verbose alternative:1505    `Annotated[ToolRuntime, InjectedRuntime]`15061507    ```python1508    from langchain_core.tools import tool, ToolRuntime150915101511    @tool1512    def foo(x: int, runtime: ToolRuntime) -> str:1513        # use runtime.state, runtime.context, runtime.store, etc.1514        ...1515    ```1516    """151715181519class InjectedToolCallId(InjectedToolArg):1520    """Annotation for injecting the tool call ID.15211522    This annotation is used to mark a tool parameter that should receive the tool call1523    ID at runtime.15241525    ```python1526    from typing import Annotated1527    from langchain_core.messages import ToolMessage1528    from langchain_core.tools import tool, InjectedToolCallId15291530    @tool1531    def foo(1532        x: int, tool_call_id: Annotated[str, InjectedToolCallId]1533    ) -> ToolMessage:1534        \"\"\"Return x.\"\"\"1535        return ToolMessage(1536            str(x),1537            artifact=x,1538            name="foo",1539            tool_call_id=tool_call_id1540        )1541    ```1542    """154315441545def _is_directly_injected_arg_type(type_: Any) -> bool:1546    """Check if a type annotation indicates a directly injected argument.15471548    This is currently only used for `ToolRuntime`.15491550    Checks if either the annotation itself is a subclass of `_DirectlyInjectedToolArg`1551    or the origin of the annotation is a subclass of `_DirectlyInjectedToolArg`.15521553    For example, `ToolRuntime` or `ToolRuntime[ContextT, StateT]` would both return1554    `True`.1555    """1556    return (1557        isinstance(type_, type) and issubclass(type_, _DirectlyInjectedToolArg)1558    ) or (1559        (origin := get_origin(type_)) is not None1560        and isinstance(origin, type)1561        and issubclass(origin, _DirectlyInjectedToolArg)1562    )156315641565def _is_injected_arg_type(1566    type_: type | TypeVar, injected_type: type[InjectedToolArg] | None = None1567) -> bool:1568    """Check if a type annotation indicates an injected argument.15691570    Args:1571        type_: The type annotation to check.1572        injected_type: The specific injected type to check for.15731574    Returns:1575        `True` if the type is an injected argument, `False` otherwise.1576    """1577    if injected_type is None:1578        # if no injected type is specified,1579        # check if the type is a directly injected argument1580        if _is_directly_injected_arg_type(type_):1581            return True1582        injected_type = InjectedToolArg15831584    # if the type is an Annotated type, check if annotated metadata1585    # is an intance or subclass of the injected type1586    return any(1587        isinstance(arg, injected_type)1588        or (isinstance(arg, type) and issubclass(arg, injected_type))1589        for arg in get_args(type_)[1:]1590    )159115921593def get_all_basemodel_annotations(1594    cls: TypeBaseModel | Any, *, default_to_bound: bool = True1595) -> dict[str, type | TypeVar]:1596    """Get all annotations from a Pydantic `BaseModel` and its parents.15971598    Args:1599        cls: The Pydantic `BaseModel` class.1600        default_to_bound: Whether to default to the bound of a `TypeVar` if it exists.16011602    Returns:1603        `dict` of field names to their type annotations.1604    """1605    orig_bases: tuple[type, ...]1606    # cls has no subscript: cls = FooBar1607    if isinstance(cls, type):1608        fields = get_fields(cls)1609        alias_map = {field.alias: name for name, field in fields.items() if field.alias}16101611        annotations: dict[str, type | TypeVar] = {}1612        for name, param in inspect.signature(cls).parameters.items():1613            # Exclude hidden init args added by pydantic Config. For example if1614            # BaseModel(extra="allow") then "extra_data" will part of init sig.1615            if name not in fields and name not in alias_map:1616                continue1617            field_name = alias_map.get(name, name)1618            annotations[field_name] = param.annotation1619        orig_bases = getattr(cls, "__orig_bases__", ())1620    # cls has subscript: cls = FooBar[int]1621    else:1622        annotations = get_all_basemodel_annotations(1623            get_origin(cls), default_to_bound=False1624        )1625        orig_bases = (cls,)16261627    # Pydantic v2 automatically resolves inherited generics, Pydantic v1 does not.1628    if not (isinstance(cls, type) and is_pydantic_v2_subclass(cls)):1629        # if cls = FooBar inherits from Baz[str], orig_bases will contain Baz[str]1630        # if cls = FooBar inherits from Baz, orig_bases will contain Baz1631        # if cls = FooBar[int], orig_bases will contain FooBar[int]1632        for parent in orig_bases:1633            # if class = FooBar inherits from Baz, parent = Baz1634            if isinstance(parent, type) and is_pydantic_v1_subclass(parent):1635                annotations.update(1636                    get_all_basemodel_annotations(parent, default_to_bound=False)1637                )1638                continue16391640            parent_origin = get_origin(parent)16411642            # if class = FooBar inherits from non-pydantic class1643            if not parent_origin:1644                continue16451646            # if class = FooBar inherits from Baz[str]:1647            # parent = class Baz[str],1648            # parent_origin = class Baz,1649            # generic_type_vars = (type vars in Baz)1650            # generic_map = {type var in Baz: str}1651            generic_type_vars: tuple[TypeVar, ...] = getattr(1652                parent_origin, "__parameters__", ()1653            )1654            generic_map = dict(zip(generic_type_vars, get_args(parent), strict=False))1655            for field in getattr(parent_origin, "__annotations__", {}):1656                annotations[field] = _replace_type_vars(1657                    annotations[field], generic_map, default_to_bound=default_to_bound1658                )16591660    return {1661        k: _replace_type_vars(v, default_to_bound=default_to_bound)1662        for k, v in annotations.items()1663    }166416651666def _replace_type_vars(1667    type_: type | TypeVar,1668    generic_map: dict[TypeVar, type] | None = None,1669    *,1670    default_to_bound: bool = True,1671) -> type | TypeVar:1672    """Replace `TypeVar`s in a type annotation with concrete types.16731674    Args:1675        type_: The type annotation to process.1676        generic_map: Mapping of `TypeVar`s to concrete types.1677        default_to_bound: Whether to use `TypeVar` bounds as defaults.16781679    Returns:1680        The type with `TypeVar`s replaced.1681    """1682    generic_map = generic_map or {}1683    if isinstance(type_, TypeVar):1684        if type_ in generic_map:1685            return generic_map[type_]1686        if default_to_bound:1687            return type_.__bound__ if type_.__bound__ is not None else Any1688        return type_1689    if (origin := get_origin(type_)) and (args := get_args(type_)):1690        new_args = tuple(1691            _replace_type_vars(arg, generic_map, default_to_bound=default_to_bound)1692            for arg in args1693        )1694        return cast("type", _py_38_safe_origin(origin)[new_args])  # type: ignore[index]1695    return type_169616971698class BaseToolkit(BaseModel, ABC):1699    """Base class for toolkits containing related tools.17001701    A toolkit is a collection of related tools that can be used together to accomplish a1702    specific task or work with a particular system.1703    """17041705    @abstractmethod1706    def get_tools(self) -> list[BaseTool]:1707        """Get all tools in the toolkit.17081709        Returns:1710            List of tools contained in this toolkit.1711        """

Code quality findings 58

Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(annotation, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(annotation, FieldInfo) and annotation.description:
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
list(annotations),
Ensure functions have docstrings for documentation
missing-docstring
def create_schema_from_function(
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
existing_params: list[str] = list(sig.parameters.keys())
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
filter_args_ = list(FILTERED_ARGS)
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
list(valid_properties),
Ensure functions have docstrings for documentation
missing-docstring
def my_tool(x: str) -> str:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and not isinstance(kwargs["args_schema"], dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(self.args_schema, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(input_schema, dict):
Ensure functions have docstrings for documentation
missing-docstring
def model_copy(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(self.args_schema, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(self.args_schema, dict):
Ensure functions have docstrings for documentation
missing-docstring
def invoke(
Ensure functions have docstrings for documentation
missing-docstring
async def ainvoke(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool_input, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(input_args, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(input_args, dict):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if self.args_schema is not None and not isinstance(self.args_schema, dict):
Catch specific exceptions instead of Exception to avoid masking bugs
broad-except
except Exception:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(self.args_schema, type)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool_input, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool_input, dict):
Ensure functions have docstrings for documentation
missing-docstring
def run(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool_input, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool_input, str)
Ensure try blocks have corresponding except or finally blocks
try-without-except
try:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(response, tuple):
Ensure functions have docstrings for documentation
missing-docstring
async def arun(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool_input, dict)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(tool_input, str)
Ensure try blocks have corresponding except or finally blocks
try-without-except
try:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if not isinstance(response, tuple):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
return isinstance(x, dict) and x.get("type") == "tool_call"
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(flag, bool):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(flag, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(flag, bool):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
elif isinstance(flag, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(content, list)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(content, ToolOutputMixin) or tool_call_id is None:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(obj, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(obj, Sequence) and all(_is_message_content_block(e) for e in obj):
Avoid unnecessary list conversions; use generators where possible
unnecessary-list
return list(obj)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(obj, str):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(obj, dict):
Catch specific exceptions instead of Exception to avoid masking bugs
broad-except
except Exception:
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(func, functools.partial):
Catch specific exceptions instead of Exception to avoid masking bugs
broad-except
except Exception:
Ensure functions have docstrings for documentation
missing-docstring
def foo(x: int, runtime: ToolRuntime) -> str:
Ensure functions have docstrings for documentation
missing-docstring
def foo(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(type_, type) and issubclass(type_, _DirectlyInjectedToolArg)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
and isinstance(origin, type)
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(arg, injected_type)
Ensure functions have docstrings for documentation
missing-docstring
def get_all_basemodel_annotations(
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(cls, type):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(parent, type) and is_pydantic_v1_subclass(parent):
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
if isinstance(type_, TypeVar):

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.