libs/core/langchain_core/tools/base.py PYTHON 1,594 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  # noqa: TC00313from 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    PydanticDeprecationWarning,32    SkipValidation,33    ValidationError,34    validate_arguments,35)36from pydantic.fields import FieldInfo37from pydantic.v1 import BaseModel as BaseModelV138from pydantic.v1 import ValidationError as ValidationErrorV139from pydantic.v1 import validate_arguments as validate_arguments_v140from typing_extensions import override4142from langchain_core.callbacks import (43    AsyncCallbackManager,44    CallbackManager,45    Callbacks,46)47from langchain_core.messages.tool import ToolCall, ToolMessage, ToolOutputMixin48from langchain_core.runnables import (49    RunnableConfig,50    RunnableSerializable,51    ensure_config,52    patch_config,53    run_in_executor,54)55from langchain_core.runnables.config import set_config_context56from langchain_core.runnables.utils import coro_with_context57from langchain_core.utils.function_calling import (58    _parse_google_docstring,59    _py_38_safe_origin,60)61from langchain_core.utils.pydantic import (62    TypeBaseModel,63    _create_subset_model,64    get_fields,65    is_basemodel_subclass,66    is_pydantic_v1_subclass,67    is_pydantic_v2_subclass,68)6970if TYPE_CHECKING:71    import uuid72    from collections.abc import Sequence7374FILTERED_ARGS = ("run_manager", "callbacks")75TOOL_MESSAGE_BLOCK_TYPES = (76    "text",77    "image_url",78    "image",79    "json",80    "search_result",81    "custom_tool_call_output",82    "document",83    "file",84)8586_logger = logging.getLogger(__name__)878889class SchemaAnnotationError(TypeError):90    """Raised when `args_schema` is missing or has an incorrect type annotation."""919293def _is_annotated_type(typ: type[Any]) -> bool:94    """Check if a type is an `Annotated` type.9596    Args:97        typ: The type to check.9899    Returns:100        `True` if the type is an `Annotated` type, `False` otherwise.101    """102    return get_origin(typ) in {typing.Annotated, typing_extensions.Annotated}103104105def _get_annotation_description(arg_type: type) -> str | None:106    """Extract description from an `Annotated` type.107108    Checks for string annotations and `FieldInfo` objects with descriptions.109110    Args:111        arg_type: The type to extract description from.112113    Returns:114        The description string if found, `None` otherwise.115    """116    if _is_annotated_type(arg_type):117        annotated_args = get_args(arg_type)118        for annotation in annotated_args[1:]:119            if isinstance(annotation, str):120                return annotation121            if isinstance(annotation, FieldInfo) and annotation.description:122                return annotation.description123    return None124125126def _get_filtered_args(127    inferred_model: type[BaseModel],128    func: Callable,129    *,130    filter_args: Sequence[str],131    include_injected: bool = True,132) -> dict:133    """Get filtered arguments from a function's signature.134135    Args:136        inferred_model: The Pydantic model inferred from the function.137        func: The function to extract arguments from.138        filter_args: Arguments to exclude from the result.139        include_injected: Whether to include injected arguments.140141    Returns:142        Dictionary of filtered arguments with their schema definitions.143    """144    schema = inferred_model.model_json_schema()["properties"]145    valid_keys = signature(func).parameters146    return {147        k: schema[k]148        for i, (k, param) in enumerate(valid_keys.items())149        if k not in filter_args150        and (i > 0 or param.name not in {"self", "cls"})151        and (include_injected or not _is_injected_arg_type(param.annotation))152    }153154155def _parse_python_function_docstring(156    function: Callable, annotations: dict, *, error_on_invalid_docstring: bool = False157) -> tuple[str, dict]:158    """Parse function and argument descriptions from a docstring.159160    Assumes the function docstring follows Google Python style guide.161162    Args:163        function: The function to parse the docstring from.164        annotations: Type annotations for the function parameters.165        error_on_invalid_docstring: Whether to raise an error on invalid docstring.166167    Returns:168        A tuple containing the function description and argument descriptions.169    """170    docstring = inspect.getdoc(function)171    return _parse_google_docstring(172        docstring,173        list(annotations),174        error_on_invalid_docstring=error_on_invalid_docstring,175    )176177178def _validate_docstring_args_against_annotations(179    arg_descriptions: dict, annotations: dict180) -> None:181    """Validate that docstring arguments match function annotations.182183    Args:184        arg_descriptions: Arguments described in the docstring.185        annotations: Type annotations from the function signature.186187    Raises:188        ValueError: If a docstring argument is not found in function signature.189    """190    for docstring_arg in arg_descriptions:191        if docstring_arg not in annotations:192            msg = f"Arg {docstring_arg} in docstring not found in function signature."193            raise ValueError(msg)194195196def _infer_arg_descriptions(197    fn: Callable,198    *,199    parse_docstring: bool = False,200    error_on_invalid_docstring: bool = False,201) -> tuple[str, dict]:202    """Infer argument descriptions from function docstring and annotations.203204    Args:205        fn: The function to infer descriptions from.206        parse_docstring: Whether to parse the docstring for descriptions.207        error_on_invalid_docstring: Whether to raise error on invalid docstring.208209    Returns:210        A tuple containing the function description and argument descriptions.211    """212    annotations = typing.get_type_hints(fn, include_extras=True)213    if parse_docstring:214        description, arg_descriptions = _parse_python_function_docstring(215            fn, annotations, error_on_invalid_docstring=error_on_invalid_docstring216        )217    else:218        description = inspect.getdoc(fn) or ""219        arg_descriptions = {}220    if parse_docstring:221        _validate_docstring_args_against_annotations(arg_descriptions, annotations)222    for arg, arg_type in annotations.items():223        if arg in arg_descriptions:224            continue225        if desc := _get_annotation_description(arg_type):226            arg_descriptions[arg] = desc227    return description, arg_descriptions228229230def _is_pydantic_annotation(annotation: Any, pydantic_version: str = "v2") -> bool:231    """Check if a type annotation is a Pydantic model.232233    Args:234        annotation: The type annotation to check.235        pydantic_version: The Pydantic version to check against (`'v1'` or `'v2'`).236237    Returns:238        `True` if the annotation is a Pydantic model, `False` otherwise.239    """240    base_model_class = BaseModelV1 if pydantic_version == "v1" else BaseModel241    try:242        return issubclass(annotation, base_model_class)243    except TypeError:244        return False245246247def _function_annotations_are_pydantic_v1(248    signature: inspect.Signature, func: Callable249) -> bool:250    """Check if all Pydantic annotations in a function are from v1.251252    Args:253        signature: The function signature to check.254        func: The function being checked.255256    Returns:257        True if all Pydantic annotations are from v1, `False` otherwise.258259    Raises:260        NotImplementedError: If the function contains mixed v1 and v2 annotations.261    """262    any_v1_annotations = any(263        _is_pydantic_annotation(parameter.annotation, pydantic_version="v1")264        for parameter in signature.parameters.values()265    )266    any_v2_annotations = any(267        _is_pydantic_annotation(parameter.annotation, pydantic_version="v2")268        for parameter in signature.parameters.values()269    )270    if any_v1_annotations and any_v2_annotations:271        msg = (272            f"Function {func} contains a mix of Pydantic v1 and v2 annotations. "273            "Only one version of Pydantic annotations per function is supported."274        )275        raise NotImplementedError(msg)276    return any_v1_annotations and not any_v2_annotations277278279class _SchemaConfig:280    """Configuration for Pydantic models generated from function signatures."""281282    extra: str = "forbid"283    """Whether to allow extra fields in the model."""284285    arbitrary_types_allowed: bool = True286    """Whether to allow arbitrary types in the model."""287288289def create_schema_from_function(290    model_name: str,291    func: Callable,292    *,293    filter_args: Sequence[str] | None = None,294    parse_docstring: bool = False,295    error_on_invalid_docstring: bool = False,296    include_injected: bool = True,297) -> type[BaseModel]:298    """Create a Pydantic schema from a function's signature.299300    Args:301        model_name: Name to assign to the generated Pydantic schema.302        func: Function to generate the schema from.303        filter_args: Optional list of arguments to exclude from the schema.304305            Defaults to `FILTERED_ARGS`.306        parse_docstring: Whether to parse the function's docstring for descriptions307            for each argument.308        error_on_invalid_docstring: If `parse_docstring` is provided, configure309            whether to raise `ValueError` on invalid Google Style docstrings.310        include_injected: Whether to include injected arguments in the schema.311312            Defaults to `True`, since we want to include them in the schema when313            *validating* tool inputs.314315    Returns:316        A Pydantic model with the same arguments as the function.317    """318    sig = inspect.signature(func)319320    if _function_annotations_are_pydantic_v1(sig, func):321        validated = validate_arguments_v1(func, config=_SchemaConfig)  # type: ignore[call-overload]322    else:323        # https://docs.pydantic.dev/latest/usage/validation_decorator/324        with warnings.catch_warnings():325            # We are using deprecated functionality here.326            # This code should be re-written to simply construct a Pydantic model327            # using inspect.signature and create_model.328            warnings.simplefilter("ignore", category=PydanticDeprecationWarning)329            validated = validate_arguments(func, config=_SchemaConfig)  # type: ignore[operator]330331    # Let's ignore `self` and `cls` arguments for class and instance methods332    # If qualified name has a ".", then it likely belongs in a class namespace333    in_class = bool(func.__qualname__ and "." in func.__qualname__)334335    has_args = False336    has_kwargs = False337338    for param in sig.parameters.values():339        if param.kind == param.VAR_POSITIONAL:340            has_args = True341        elif param.kind == param.VAR_KEYWORD:342            has_kwargs = True343344    inferred_model = validated.model345346    if filter_args:347        filter_args_ = filter_args348    else:349        # Handle classmethods and instance methods350        existing_params: list[str] = list(sig.parameters.keys())351        if existing_params and existing_params[0] in {"self", "cls"} and in_class:352            filter_args_ = [existing_params[0], *list(FILTERED_ARGS)]353        else:354            filter_args_ = list(FILTERED_ARGS)355356        for existing_param in existing_params:357            if not include_injected and _is_injected_arg_type(358                sig.parameters[existing_param].annotation359            ):360                filter_args_.append(existing_param)361362    description, arg_descriptions = _infer_arg_descriptions(363        func,364        parse_docstring=parse_docstring,365        error_on_invalid_docstring=error_on_invalid_docstring,366    )367    # Pydantic adds placeholder virtual fields we need to strip368    valid_properties = []369    for field in get_fields(inferred_model):370        if not has_args and field == "args":371            continue372        if not has_kwargs and field == "kwargs":373            continue374375        if field == "v__duplicate_kwargs":  # Internal pydantic field376            continue377378        if field not in filter_args_:379            valid_properties.append(field)380381    return _create_subset_model(382        model_name,383        inferred_model,384        list(valid_properties),385        descriptions=arg_descriptions,386        fn_description=description,387    )388389390class ToolException(Exception):  # noqa: N818391    """Exception thrown when a tool execution error occurs.392393    This exception allows tools to signal errors without stopping the agent.394395    The error is handled according to the tool's `handle_tool_error` setting, and the396    result is returned as an observation to the agent.397    """398399400ArgsSchema = TypeBaseModel | dict[str, Any]401402_EMPTY_SET: frozenset[str] = frozenset()403404405class BaseTool(RunnableSerializable[str | dict | ToolCall, Any]):406    """Base class for all LangChain tools.407408    This abstract class defines the interface that all LangChain tools must implement.409410    Tools are components that can be called by agents to perform specific actions.411    """412413    def __init_subclass__(cls, **kwargs: Any) -> None:414        """Validate the tool class definition during subclass creation.415416        Args:417            **kwargs: Additional keyword arguments passed to the parent class.418419        Raises:420            SchemaAnnotationError: If `args_schema` has incorrect type annotation.421        """422        super().__init_subclass__(**kwargs)423424        args_schema_type = cls.__annotations__.get("args_schema", None)425426        if args_schema_type is not None and args_schema_type == BaseModel:427            # Throw errors for common mis-annotations.428            # TODO: Use get_args / get_origin and fully429            # specify valid annotations.430            typehint_mandate = """431class ChildTool(BaseTool):432    ...433    args_schema: Type[BaseModel] = SchemaClass434    ..."""435            name = cls.__name__436            msg = (437                f"Tool definition for {name} must include valid type annotations"438                f" for argument 'args_schema' to behave as expected.\n"439                f"Expected annotation of 'Type[BaseModel]'"440                f" but got '{args_schema_type}'.\n"441                f"Expected class looks like:\n"442                f"{typehint_mandate}"443            )444            raise SchemaAnnotationError(msg)445446    name: str447    """The unique name of the tool that clearly communicates its purpose."""448449    description: str450    """Used to tell the model how/when/why to use the tool.451452    You can provide few-shot examples as a part of the description.453    """454455    args_schema: Annotated[ArgsSchema | None, SkipValidation()] = Field(456        default=None, description="The tool schema."457    )458    """Pydantic model class to validate and parse the tool's input arguments.459460    Args schema should be either:461462    - A subclass of `pydantic.BaseModel`.463    - A subclass of `pydantic.v1.BaseModel` if accessing v1 namespace in pydantic 2464    - A JSON schema dict465    """466467    return_direct: bool = False468    """Whether to return the tool's output directly.469470    Setting this to `True` means that after the tool is called, the `AgentExecutor` will471    stop looping.472    """473474    verbose: bool = False475    """Whether to log the tool's progress."""476477    callbacks: Callbacks = Field(default=None, exclude=True)478    """Callbacks to be called during tool execution."""479480    tags: list[str] | None = None481    """Optional list of tags associated with the tool.482483    These tags will be associated with each call to this tool,484    and passed as arguments to the handlers defined in `callbacks`.485486    You can use these to, e.g., identify a specific instance of a tool with its use487    case.488    """489490    metadata: dict[str, Any] | None = None491    """Optional metadata associated with the tool.492493    This metadata will be associated with each call to this tool,494    and passed as arguments to the handlers defined in `callbacks`.495496    You can use these to, e.g., identify a specific instance of a tool with its usecase.497    """498499    handle_tool_error: bool | str | Callable[[ToolException], str] | None = False500    """Handle the content of the `ToolException` thrown."""501502    handle_validation_error: (503        bool | str | Callable[[ValidationError | ValidationErrorV1], str] | None504    ) = False505    """Handle the content of the `ValidationError` thrown."""506507    response_format: Literal["content", "content_and_artifact"] = "content"508    """The tool response format.509510    If `'content'` then the output of the tool is interpreted as the contents of a511    `ToolMessage`. If `'content_and_artifact'` then the output is expected to be a512    two-tuple corresponding to the `(content, artifact)` of a `ToolMessage`.513    """514515    extras: dict[str, Any] | None = None516    """Optional provider-specific extra fields for the tool.517518    This is used to pass provider-specific configuration that doesn't fit into519    standard tool fields.520521    Example:522        Anthropic-specific fields like [`cache_control`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#prompt-caching),523        [`defer_loading`](https://docs.langchain.com/oss/python/integrations/chat/anthropic#tool-search),524        or `input_examples`.525526        ```python527        @tool(extras={"defer_loading": True, "cache_control": {"type": "ephemeral"}})528        def my_tool(x: str) -> str:529            return x530        ```531    """532533    def __init__(self, **kwargs: Any) -> None:534        """Initialize the tool.535536        Raises:537            TypeError: If `args_schema` is not a subclass of pydantic `BaseModel` or538                `dict`.539        """540        if (541            "args_schema" in kwargs542            and kwargs["args_schema"] is not None543            and not is_basemodel_subclass(kwargs["args_schema"])544            and not isinstance(kwargs["args_schema"], dict)545        ):546            msg = (547                "args_schema must be a subclass of pydantic BaseModel or "548                f"a JSON schema dict. Got: {kwargs['args_schema']}."549            )550            raise TypeError(msg)551        super().__init__(**kwargs)552553    model_config = ConfigDict(554        arbitrary_types_allowed=True,555    )556557    @property558    def is_single_input(self) -> bool:559        """Check if the tool accepts only a single input argument.560561        Returns:562            `True` if the tool has only one input argument, `False` otherwise.563        """564        keys = {k for k in self.args if k != "kwargs"}565        return len(keys) == 1566567    @property568    def args(self) -> dict:569        """Get the tool's input arguments schema.570571        Returns:572            `dict` containing the tool's argument properties.573        """574        if isinstance(self.args_schema, dict):575            json_schema = self.args_schema576        elif self.args_schema and issubclass(self.args_schema, BaseModelV1):577            json_schema = self.args_schema.schema()578        else:579            input_schema = self.tool_call_schema580            if isinstance(input_schema, dict):581                json_schema = input_schema582            else:583                json_schema = input_schema.model_json_schema()584        return cast("dict", json_schema["properties"])585586    @property587    def tool_call_schema(self) -> ArgsSchema:588        """Get the schema for tool calls, excluding injected arguments.589590        Returns:591            The schema that should be used for tool calls from language models.592        """593        if isinstance(self.args_schema, dict):594            if self.description:595                return {596                    **self.args_schema,597                    "description": self.description,598                }599600            return self.args_schema601602        full_schema = self.get_input_schema()603        fields = []604        for name, type_ in get_all_basemodel_annotations(full_schema).items():605            if not _is_injected_arg_type(type_):606                fields.append(name)607        return _create_subset_model(608            self.name, full_schema, fields, fn_description=self.description609        )610611    @functools.cached_property612    def _injected_args_keys(self) -> frozenset[str]:613        # Base implementation doesn't manage injected args614        return _EMPTY_SET615616    # --- Runnable ---617618    @override619    def get_input_schema(self, config: RunnableConfig | None = None) -> type[BaseModel]:620        """The tool's input schema.621622        Args:623            config: The configuration for the tool.624625        Returns:626            The input schema for the tool.627        """628        if self.args_schema is not None:629            if isinstance(self.args_schema, dict):630                return super().get_input_schema(config)631            return self.args_schema632        return create_schema_from_function(self.name, self._run)633634    @override635    def invoke(636        self,637        input: str | dict | ToolCall,638        config: RunnableConfig | None = None,639        **kwargs: Any,640    ) -> Any:641        tool_input, kwargs = _prep_run_args(input, config, **kwargs)642        return self.run(tool_input, **kwargs)643644    @override645    async def ainvoke(646        self,647        input: str | dict | ToolCall,648        config: RunnableConfig | None = None,649        **kwargs: Any,650    ) -> Any:651        tool_input, kwargs = _prep_run_args(input, config, **kwargs)652        return await self.arun(tool_input, **kwargs)653654    # --- Tool ---655656    def _parse_input(657        self, tool_input: str | dict, tool_call_id: str | None658    ) -> str | dict[str, Any]:659        """Parse and validate tool input using the args schema.660661        Args:662            tool_input: The raw input to the tool.663            tool_call_id: The ID of the tool call, if available.664665        Returns:666            The parsed and validated input.667668        Raises:669            ValueError: If `string` input is provided with JSON schema `args_schema`.670            ValueError: If `InjectedToolCallId` is required but `tool_call_id` is not671                provided.672            TypeError: If `args_schema` is not a Pydantic `BaseModel` or dict.673        """674        input_args = self.args_schema675676        if isinstance(tool_input, str):677            if input_args is not None:678                if isinstance(input_args, dict):679                    msg = (680                        "String tool inputs are not allowed when "681                        "using tools with JSON schema args_schema."682                    )683                    raise ValueError(msg)684                key_ = next(iter(get_fields(input_args).keys()))685                if issubclass(input_args, BaseModel):686                    input_args.model_validate({key_: tool_input})687                elif issubclass(input_args, BaseModelV1):688                    input_args.parse_obj({key_: tool_input})689                else:690                    msg = f"args_schema must be a Pydantic BaseModel, got {input_args}"691                    raise TypeError(msg)692            return tool_input693694        if input_args is not None:695            if isinstance(input_args, dict):696                return tool_input697            if issubclass(input_args, BaseModel):698                # Check args_schema for InjectedToolCallId699                for k, v in get_all_basemodel_annotations(input_args).items():700                    if _is_injected_arg_type(v, injected_type=InjectedToolCallId):701                        if tool_call_id is None:702                            msg = (703                                "When tool includes an InjectedToolCallId "704                                "argument, tool must always be invoked with a full "705                                "model ToolCall of the form: {'args': {...}, "706                                "'name': '...', 'type': 'tool_call', "707                                "'tool_call_id': '...'}"708                            )709                            raise ValueError(msg)710                        tool_input[k] = tool_call_id711                result = input_args.model_validate(tool_input)712                result_dict = result.model_dump()713            elif issubclass(input_args, BaseModelV1):714                # Check args_schema for InjectedToolCallId715                for k, v in get_all_basemodel_annotations(input_args).items():716                    if _is_injected_arg_type(v, injected_type=InjectedToolCallId):717                        if tool_call_id is None:718                            msg = (719                                "When tool includes an InjectedToolCallId "720                                "argument, tool must always be invoked with a full "721                                "model ToolCall of the form: {'args': {...}, "722                                "'name': '...', 'type': 'tool_call', "723                                "'tool_call_id': '...'}"724                            )725                            raise ValueError(msg)726                        tool_input[k] = tool_call_id727                result = input_args.parse_obj(tool_input)728                result_dict = result.dict()729            else:730                msg = (731                    f"args_schema must be a Pydantic BaseModel, got {self.args_schema}"732                )733                raise NotImplementedError(msg)734735            # Include fields from tool_input, plus fields with explicit defaults.736            # This applies Pydantic defaults (like Field(default=1)) while excluding737            # synthetic "args"/"kwargs" fields that Pydantic creates for *args/**kwargs.738            field_info = get_fields(input_args)739            validated_input = {}740            for k in result_dict:741                if k in tool_input:742                    # Field was provided in input - include it (validated)743                    validated_input[k] = getattr(result, k)744                elif k in field_info and k not in {"args", "kwargs"}:745                    # Check if field has an explicit default defined in the schema.746                    # Exclude "args"/"kwargs" as these are synthetic fields for variadic747                    # parameters that should not be passed as keyword arguments.748                    fi = field_info[k]749                    # Pydantic v2 uses is_required() method, v1 uses required attribute750                    has_default = (751                        not fi.is_required()752                        if hasattr(fi, "is_required")753                        else not getattr(fi, "required", True)754                    )755                    if has_default:756                        validated_input[k] = getattr(result, k)757758            for k in self._injected_args_keys:759                if k in tool_input:760                    validated_input[k] = tool_input[k]761                elif k == "tool_call_id":762                    if tool_call_id is None:763                        msg = (764                            "When tool includes an InjectedToolCallId "765                            "argument, tool must always be invoked with a full "766                            "model ToolCall of the form: {'args': {...}, "767                            "'name': '...', 'type': 'tool_call', "768                            "'tool_call_id': '...'}"769                        )770                        raise ValueError(msg)771                    validated_input[k] = tool_call_id772773            return validated_input774775        return tool_input776777    @abstractmethod778    def _run(self, *args: Any, **kwargs: Any) -> Any:779        """Use the tool.780781        Add `run_manager: CallbackManagerForToolRun | None = None` to child782        implementations to enable tracing.783784        Returns:785            The result of the tool execution.786        """787788    async def _arun(self, *args: Any, **kwargs: Any) -> Any:789        """Use the tool asynchronously.790791        Add `run_manager: AsyncCallbackManagerForToolRun | None = None` to child792        implementations to enable tracing.793794        Returns:795            The result of the tool execution.796        """797        if kwargs.get("run_manager") and signature(self._run).parameters.get(798            "run_manager"799        ):800            kwargs["run_manager"] = kwargs["run_manager"].get_sync()801        return await run_in_executor(None, self._run, *args, **kwargs)802803    def _filter_injected_args(self, tool_input: dict) -> dict:804        """Filter out injected tool arguments from the input dictionary.805806        Injected arguments are those annotated with `InjectedToolArg` or its807        subclasses, or arguments in `FILTERED_ARGS` like `run_manager` and callbacks.808809        Args:810            tool_input: The tool input dictionary to filter.811812        Returns:813            A filtered dictionary with injected arguments removed.814        """815        # Start with filtered args from the constant816        filtered_keys = set[str](FILTERED_ARGS)817818        # Add injected args from function signature (e.g., ToolRuntime parameters)819        filtered_keys.update(self._injected_args_keys)820821        # If we have an args_schema, use it to identify injected args822        # Skip if args_schema is a dict (JSON Schema) as it's not a Pydantic model823        if self.args_schema is not None and not isinstance(self.args_schema, dict):824            try:825                annotations = get_all_basemodel_annotations(self.args_schema)826                for field_name, field_type in annotations.items():827                    if _is_injected_arg_type(field_type):828                        filtered_keys.add(field_name)829            except Exception:830                # If we can't get annotations, just use FILTERED_ARGS831                _logger.debug(832                    "Failed to get args_schema annotations for filtering.",833                    exc_info=True,834                )835836        # Filter out the injected keys from tool_input837        return {k: v for k, v in tool_input.items() if k not in filtered_keys}838839    def _to_args_and_kwargs(840        self, tool_input: str | dict, tool_call_id: str | None841    ) -> tuple[tuple, dict]:842        """Convert tool input to positional and keyword arguments.843844        Args:845            tool_input: The input to the tool.846            tool_call_id: The ID of the tool call, if available.847848        Returns:849            A tuple of `(positional_args, keyword_args)` for the tool.850851        Raises:852            TypeError: If the tool input type is invalid.853        """854        if (855            self.args_schema is not None856            and isinstance(self.args_schema, type)857            and is_basemodel_subclass(self.args_schema)858            and not get_fields(self.args_schema)859        ):860            # StructuredTool with no args861            return (), {}862        tool_input = self._parse_input(tool_input, tool_call_id)863        # For backwards compatibility, if run_input is a string,864        # pass as a positional argument.865        if isinstance(tool_input, str):866            return (tool_input,), {}867        if isinstance(tool_input, dict):868            # Make a shallow copy of the input to allow downstream code869            # to modify the root level of the input without affecting the870            # original input.871            # This is used by the tool to inject run time information like872            # the callback manager.873            return (), tool_input.copy()874        # This code path is not expected to be reachable.875        msg = f"Invalid tool input type: {type(tool_input)}"876        raise TypeError(msg)877878    def run(879        self,880        tool_input: str | dict[str, Any],881        verbose: bool | None = None,  # noqa: FBT001882        start_color: str | None = "green",883        color: str | None = "green",884        callbacks: Callbacks = None,885        *,886        tags: list[str] | None = None,887        metadata: dict[str, Any] | None = None,888        run_name: str | None = None,889        run_id: uuid.UUID | None = None,890        config: RunnableConfig | None = None,891        tool_call_id: str | None = None,892        **kwargs: Any,893    ) -> Any:894        """Run the tool.895896        Args:897            tool_input: The input to the tool.898            verbose: Whether to log the tool's progress.899            start_color: The color to use when starting the tool.900            color: The color to use when ending the tool.901            callbacks: Callbacks to be called during tool execution.902            tags: Optional list of tags associated with the tool.903            metadata: Optional metadata associated with the tool.904            run_name: The name of the run.905            run_id: The id of the run.906            config: The configuration for the tool.907            tool_call_id: The id of the tool call.908            **kwargs: Keyword arguments to be passed to tool callbacks (event handler)909910        Returns:911            The output of the tool.912913        Raises:914            ToolException: If an error occurs during tool execution.915        """916        callback_manager = CallbackManager.configure(917            callbacks,918            self.callbacks,919            self.verbose or bool(verbose),920            tags,921            self.tags,922            metadata,923            self.metadata,924        )925926        # Filter out injected arguments from callback inputs927        filtered_tool_input = (928            self._filter_injected_args(tool_input)929            if isinstance(tool_input, dict)930            else None931        )932933        # Use filtered inputs for the input_str parameter as well934        tool_input_str = (935            tool_input936            if isinstance(tool_input, str)937            else str(938                filtered_tool_input if filtered_tool_input is not None else tool_input939            )940        )941942        run_manager = callback_manager.on_tool_start(943            {"name": self.name, "description": self.description},944            tool_input_str,945            color=start_color,946            name=run_name,947            run_id=run_id,948            inputs=filtered_tool_input,949            tool_call_id=tool_call_id,950            **kwargs,951        )952953        content = None954        artifact = None955        status = "success"956        error_to_raise: Exception | KeyboardInterrupt | None = None957        try:958            child_config = patch_config(config, callbacks=run_manager.get_child())959            with set_config_context(child_config) as context:960                tool_args, tool_kwargs = self._to_args_and_kwargs(961                    tool_input, tool_call_id962                )963                if signature(self._run).parameters.get("run_manager"):964                    tool_kwargs |= {"run_manager": run_manager}965                if config_param := _get_runnable_config_param(self._run):966                    tool_kwargs |= {config_param: config}967                response = context.run(self._run, *tool_args, **tool_kwargs)968            if self.response_format == "content_and_artifact":969                msg = (970                    "Since response_format='content_and_artifact' "971                    "a two-tuple of the message content and raw tool output is "972                    f"expected. Instead, generated response is of type: "973                    f"{type(response)}."974                )975                if not isinstance(response, tuple):976                    error_to_raise = ValueError(msg)977                else:978                    try:979                        content, artifact = response980                    except ValueError:981                        error_to_raise = ValueError(msg)982            else:983                content = response984        except (ValidationError, ValidationErrorV1) as e:985            if not self.handle_validation_error:986                error_to_raise = e987            else:988                content = _handle_validation_error(e, flag=self.handle_validation_error)989                status = "error"990        except ToolException as e:991            if not self.handle_tool_error:992                error_to_raise = e993            else:994                content = _handle_tool_error(e, flag=self.handle_tool_error)995                status = "error"996        except (Exception, KeyboardInterrupt) as e:997            error_to_raise = e998999        if error_to_raise:1000            run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)1001            raise error_to_raise1002        output = _format_output(content, artifact, tool_call_id, self.name, status)1003        run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)1004        return output10051006    async def arun(1007        self,1008        tool_input: str | dict,1009        verbose: bool | None = None,  # noqa: FBT0011010        start_color: str | None = "green",1011        color: str | None = "green",1012        callbacks: Callbacks = None,1013        *,1014        tags: list[str] | None = None,1015        metadata: dict[str, Any] | None = None,1016        run_name: str | None = None,1017        run_id: uuid.UUID | None = None,1018        config: RunnableConfig | None = None,1019        tool_call_id: str | None = None,1020        **kwargs: Any,1021    ) -> Any:1022        """Run the tool asynchronously.10231024        Args:1025            tool_input: The input to the tool.1026            verbose: Whether to log the tool's progress.1027            start_color: The color to use when starting the tool.1028            color: The color to use when ending the tool.1029            callbacks: Callbacks to be called during tool execution.1030            tags: Optional list of tags associated with the tool.1031            metadata: Optional metadata associated with the tool.1032            run_name: The name of the run.1033            run_id: The id of the run.1034            config: The configuration for the tool.1035            tool_call_id: The id of the tool call.1036            **kwargs: Keyword arguments to be passed to tool callbacks10371038        Returns:1039            The output of the tool.10401041        Raises:1042            ToolException: If an error occurs during tool execution.1043        """1044        callback_manager = AsyncCallbackManager.configure(1045            callbacks,1046            self.callbacks,1047            self.verbose or bool(verbose),1048            tags,1049            self.tags,1050            metadata,1051            self.metadata,1052        )10531054        # Filter out injected arguments from callback inputs1055        filtered_tool_input = (1056            self._filter_injected_args(tool_input)1057            if isinstance(tool_input, dict)1058            else None1059        )10601061        # Use filtered inputs for the input_str parameter as well1062        tool_input_str = (1063            tool_input1064            if isinstance(tool_input, str)1065            else str(1066                filtered_tool_input if filtered_tool_input is not None else tool_input1067            )1068        )10691070        run_manager = await callback_manager.on_tool_start(1071            {"name": self.name, "description": self.description},1072            tool_input_str,1073            color=start_color,1074            name=run_name,1075            run_id=run_id,1076            inputs=filtered_tool_input,1077            tool_call_id=tool_call_id,1078            **kwargs,1079        )1080        content = None1081        artifact = None1082        status = "success"1083        error_to_raise: Exception | KeyboardInterrupt | None = None1084        try:1085            tool_args, tool_kwargs = self._to_args_and_kwargs(tool_input, tool_call_id)1086            child_config = patch_config(config, callbacks=run_manager.get_child())1087            with set_config_context(child_config) as context:1088                func_to_check = (1089                    self._run if self.__class__._arun is BaseTool._arun else self._arun  # noqa: SLF0011090                )1091                if signature(func_to_check).parameters.get("run_manager"):1092                    tool_kwargs["run_manager"] = run_manager1093                if config_param := _get_runnable_config_param(func_to_check):1094                    tool_kwargs[config_param] = config10951096                coro = self._arun(*tool_args, **tool_kwargs)1097                response = await coro_with_context(coro, context)1098            if self.response_format == "content_and_artifact":1099                msg = (1100                    "Since response_format='content_and_artifact' "1101                    "a two-tuple of the message content and raw tool output is "1102                    f"expected. Instead, generated response is of type: "1103                    f"{type(response)}."1104                )1105                if not isinstance(response, tuple):1106                    error_to_raise = ValueError(msg)1107                else:1108                    try:1109                        content, artifact = response1110                    except ValueError:1111                        error_to_raise = ValueError(msg)1112            else:1113                content = response1114        except ValidationError as e:1115            if not self.handle_validation_error:1116                error_to_raise = e1117            else:1118                content = _handle_validation_error(e, flag=self.handle_validation_error)1119                status = "error"1120        except ToolException as e:1121            if not self.handle_tool_error:1122                error_to_raise = e1123            else:1124                content = _handle_tool_error(e, flag=self.handle_tool_error)1125                status = "error"1126        except (Exception, KeyboardInterrupt) as e:1127            error_to_raise = e11281129        if error_to_raise:1130            await run_manager.on_tool_error(error_to_raise, tool_call_id=tool_call_id)1131            raise error_to_raise11321133        output = _format_output(content, artifact, tool_call_id, self.name, status)1134        await run_manager.on_tool_end(output, color=color, name=self.name, **kwargs)1135        return output113611371138def _is_tool_call(x: Any) -> bool:1139    """Check if the input is a tool call dictionary.11401141    Args:1142        x: The input to check.11431144    Returns:1145        `True` if the input is a tool call, `False` otherwise.1146    """1147    return isinstance(x, dict) and x.get("type") == "tool_call"114811491150def _handle_validation_error(1151    e: ValidationError | ValidationErrorV1,1152    *,1153    flag: Literal[True] | str | Callable[[ValidationError | ValidationErrorV1], str],1154) -> str:1155    """Handle validation errors based on the configured flag.11561157    Args:1158        e: The validation error that occurred.1159        flag: How to handle the error (`bool`, `str`, or `Callable`).11601161    Returns:1162        The error message to return.11631164    Raises:1165        ValueError: If the flag type is unexpected.1166    """1167    if isinstance(flag, bool):1168        content = "Tool input validation error"1169    elif isinstance(flag, str):1170        content = flag1171    elif callable(flag):1172        content = flag(e)1173    else:1174        msg = (1175            f"Got unexpected type of `handle_validation_error`. Expected bool, "1176            f"str or callable. Received: {flag}"1177        )1178        raise ValueError(msg)  # noqa: TRY0041179    return content118011811182def _handle_tool_error(1183    e: ToolException,1184    *,1185    flag: Literal[True] | str | Callable[[ToolException], str] | None,1186) -> str:1187    """Handle tool execution errors based on the configured flag.11881189    Args:1190        e: The tool exception that occurred.1191        flag: How to handle the error (`bool`, `str`, or `Callable`).11921193    Returns:1194        The error message to return.11951196    Raises:1197        ValueError: If the flag type is unexpected.1198    """1199    if isinstance(flag, bool):1200        content = e.args[0] if e.args else "Tool execution error"1201    elif isinstance(flag, str):1202        content = flag1203    elif callable(flag):1204        content = flag(e)1205    else:1206        msg = (1207            f"Got unexpected type of `handle_tool_error`. Expected bool, str "1208            f"or callable. Received: {flag}"1209        )1210        raise ValueError(msg)  # noqa: TRY0041211    return content121212131214def _prep_run_args(1215    value: str | dict | ToolCall,1216    config: RunnableConfig | None,1217    **kwargs: Any,1218) -> tuple[str | dict, dict]:1219    """Prepare arguments for tool execution.12201221    Args:1222        value: The input value (`str`, `dict`, or `ToolCall`).1223        config: The runnable configuration.1224        **kwargs: Additional keyword arguments.12251226    Returns:1227        A tuple of `(tool_input, run_kwargs)`.1228    """1229    config = ensure_config(config)1230    if _is_tool_call(value):1231        tool_call_id: str | None = cast("ToolCall", value)["id"]1232        tool_input: str | dict = cast("ToolCall", value)["args"].copy()1233    else:1234        tool_call_id = None1235        tool_input = cast("str | dict", value)1236    return (1237        tool_input,1238        dict(1239            callbacks=config.get("callbacks"),1240            tags=config.get("tags"),1241            metadata=config.get("metadata"),1242            run_name=config.get("run_name"),1243            run_id=config.pop("run_id", None),1244            config=config,1245            tool_call_id=tool_call_id,1246            **kwargs,1247        ),1248    )124912501251def _format_output(1252    content: Any,1253    artifact: Any,1254    tool_call_id: str | None,1255    name: str,1256    status: str,1257) -> ToolOutputMixin | Any:1258    """Format tool output as a `ToolMessage` if appropriate.12591260    Args:1261        content: The main content of the tool output.1262        artifact: Any artifact data from the tool.1263        tool_call_id: The ID of the tool call.1264        name: The name of the tool.1265        status: The execution status.12661267    Returns:1268        The formatted output, either as a `ToolMessage`, the original content,1269        or an unchanged list of `ToolOutputMixin` instances.1270    """1271    if (1272        isinstance(content, list)1273        and content1274        and all(isinstance(item, ToolOutputMixin) for item in content)1275    ):1276        return content1277    if isinstance(content, ToolOutputMixin) or tool_call_id is None:1278        return content1279    if not _is_message_content_type(content):1280        content = _stringify(content)1281    return ToolMessage(1282        content,1283        artifact=artifact,1284        tool_call_id=tool_call_id,1285        name=name,1286        status=status,1287    )128812891290def _is_message_content_type(obj: Any) -> bool:1291    """Check if object is valid message content format.12921293    Validates content for OpenAI or Anthropic format tool messages.12941295    Args:1296        obj: The object to check.12971298    Returns:1299        `True` if the object is valid message content, `False` otherwise.1300    """1301    return isinstance(obj, str) or (1302        isinstance(obj, list) and all(_is_message_content_block(e) for e in obj)1303    )130413051306def _is_message_content_block(obj: Any) -> bool:1307    """Check if object is a valid message content block.13081309    Validates content blocks for OpenAI or Anthropic format.13101311    Args:1312        obj: The object to check.13131314    Returns:1315        `True` if the object is a valid content block, `False` otherwise.1316    """1317    if isinstance(obj, str):1318        return True1319    if isinstance(obj, dict):1320        return obj.get("type", None) in TOOL_MESSAGE_BLOCK_TYPES1321    return False132213231324def _stringify(content: Any) -> str:1325    """Convert content to string, preferring JSON format.13261327    Args:1328        content: The content to stringify.13291330    Returns:1331        String representation of the content.1332    """1333    try:1334        return json.dumps(content, ensure_ascii=False)1335    except Exception:1336        return str(content)133713381339def _get_type_hints(func: Callable) -> dict[str, type] | None:1340    """Get type hints from a function, handling partial functions.13411342    Args:1343        func: The function to get type hints from.13441345    Returns:1346        `dict` of type hints, or `None` if extraction fails.1347    """1348    if isinstance(func, functools.partial):1349        func = func.func1350    try:1351        return get_type_hints(func)1352    except Exception:1353        return None135413551356def _get_runnable_config_param(func: Callable) -> str | None:1357    """Find the parameter name for `RunnableConfig` in a function.13581359    Args:1360        func: The function to check.13611362    Returns:1363        The parameter name for `RunnableConfig`, or `None` if not found.1364    """1365    type_hints = _get_type_hints(func)1366    if not type_hints:1367        return None1368    for name, type_ in type_hints.items():1369        if type_ is RunnableConfig:1370            return name1371    return None137213731374class InjectedToolArg:1375    """Annotation for tool arguments that are injected at runtime.13761377    Tool arguments annotated with this class are not included in the tool1378    schema sent to language models and are instead injected during execution.1379    """138013811382class _DirectlyInjectedToolArg:1383    """Annotation for tool arguments that are injected at runtime.13841385    Injected via direct type annotation, rather than annotated metadata.13861387    For example, `ToolRuntime` is a directly injected argument.13881389    Note the direct annotation rather than the verbose alternative:1390    `Annotated[ToolRuntime, InjectedRuntime]`13911392    ```python1393    from langchain_core.tools import tool, ToolRuntime139413951396    @tool1397    def foo(x: int, runtime: ToolRuntime) -> str:1398        # use runtime.state, runtime.context, runtime.store, etc.1399        ...1400    ```1401    """140214031404class InjectedToolCallId(InjectedToolArg):1405    """Annotation for injecting the tool call ID.14061407    This annotation is used to mark a tool parameter that should receive the tool call1408    ID at runtime.14091410    ```python1411    from typing import Annotated1412    from langchain_core.messages import ToolMessage1413    from langchain_core.tools import tool, InjectedToolCallId14141415    @tool1416    def foo(1417        x: int, tool_call_id: Annotated[str, InjectedToolCallId]1418    ) -> ToolMessage:1419        \"\"\"Return x.\"\"\"1420        return ToolMessage(1421            str(x),1422            artifact=x,1423            name="foo",1424            tool_call_id=tool_call_id1425        )1426    ```1427    """142814291430def _is_directly_injected_arg_type(type_: Any) -> bool:1431    """Check if a type annotation indicates a directly injected argument.14321433    This is currently only used for `ToolRuntime`.14341435    Checks if either the annotation itself is a subclass of `_DirectlyInjectedToolArg`1436    or the origin of the annotation is a subclass of `_DirectlyInjectedToolArg`.14371438    For example, `ToolRuntime` or `ToolRuntime[ContextT, StateT]` would both return1439    `True`.1440    """1441    return (1442        isinstance(type_, type) and issubclass(type_, _DirectlyInjectedToolArg)1443    ) or (1444        (origin := get_origin(type_)) is not None1445        and isinstance(origin, type)1446        and issubclass(origin, _DirectlyInjectedToolArg)1447    )144814491450def _is_injected_arg_type(1451    type_: type | TypeVar, injected_type: type[InjectedToolArg] | None = None1452) -> bool:1453    """Check if a type annotation indicates an injected argument.14541455    Args:1456        type_: The type annotation to check.1457        injected_type: The specific injected type to check for.14581459    Returns:1460        `True` if the type is an injected argument, `False` otherwise.1461    """1462    if injected_type is None:1463        # if no injected type is specified,1464        # check if the type is a directly injected argument1465        if _is_directly_injected_arg_type(type_):1466            return True1467        injected_type = InjectedToolArg14681469    # if the type is an Annotated type, check if annotated metadata1470    # is an intance or subclass of the injected type1471    return any(1472        isinstance(arg, injected_type)1473        or (isinstance(arg, type) and issubclass(arg, injected_type))1474        for arg in get_args(type_)[1:]1475    )147614771478def get_all_basemodel_annotations(1479    cls: TypeBaseModel | Any, *, default_to_bound: bool = True1480) -> dict[str, type | TypeVar]:1481    """Get all annotations from a Pydantic `BaseModel` and its parents.14821483    Args:1484        cls: The Pydantic `BaseModel` class.1485        default_to_bound: Whether to default to the bound of a `TypeVar` if it exists.14861487    Returns:1488        `dict` of field names to their type annotations.1489    """1490    # cls has no subscript: cls = FooBar1491    if isinstance(cls, type):1492        fields = get_fields(cls)1493        alias_map = {field.alias: name for name, field in fields.items() if field.alias}14941495        annotations: dict[str, type | TypeVar] = {}1496        for name, param in inspect.signature(cls).parameters.items():1497            # Exclude hidden init args added by pydantic Config. For example if1498            # BaseModel(extra="allow") then "extra_data" will part of init sig.1499            if name not in fields and name not in alias_map:1500                continue1501            field_name = alias_map.get(name, name)1502            annotations[field_name] = param.annotation1503        orig_bases: tuple = getattr(cls, "__orig_bases__", ())1504    # cls has subscript: cls = FooBar[int]1505    else:1506        annotations = get_all_basemodel_annotations(1507            get_origin(cls), default_to_bound=False1508        )1509        orig_bases = (cls,)15101511    # Pydantic v2 automatically resolves inherited generics, Pydantic v1 does not.1512    if not (isinstance(cls, type) and is_pydantic_v2_subclass(cls)):1513        # if cls = FooBar inherits from Baz[str], orig_bases will contain Baz[str]1514        # if cls = FooBar inherits from Baz, orig_bases will contain Baz1515        # if cls = FooBar[int], orig_bases will contain FooBar[int]1516        for parent in orig_bases:1517            # if class = FooBar inherits from Baz, parent = Baz1518            if isinstance(parent, type) and is_pydantic_v1_subclass(parent):1519                annotations.update(1520                    get_all_basemodel_annotations(parent, default_to_bound=False)1521                )1522                continue15231524            parent_origin = get_origin(parent)15251526            # if class = FooBar inherits from non-pydantic class1527            if not parent_origin:1528                continue15291530            # if class = FooBar inherits from Baz[str]:1531            # parent = class Baz[str],1532            # parent_origin = class Baz,1533            # generic_type_vars = (type vars in Baz)1534            # generic_map = {type var in Baz: str}1535            generic_type_vars: tuple = getattr(parent_origin, "__parameters__", ())1536            generic_map = dict(zip(generic_type_vars, get_args(parent), strict=False))1537            for field in getattr(parent_origin, "__annotations__", {}):1538                annotations[field] = _replace_type_vars(1539                    annotations[field], generic_map, default_to_bound=default_to_bound1540                )15411542    return {1543        k: _replace_type_vars(v, default_to_bound=default_to_bound)1544        for k, v in annotations.items()1545    }154615471548def _replace_type_vars(1549    type_: type | TypeVar,1550    generic_map: dict[TypeVar, type] | None = None,1551    *,1552    default_to_bound: bool = True,1553) -> type | TypeVar:1554    """Replace `TypeVar`s in a type annotation with concrete types.15551556    Args:1557        type_: The type annotation to process.1558        generic_map: Mapping of `TypeVar`s to concrete types.1559        default_to_bound: Whether to use `TypeVar` bounds as defaults.15601561    Returns:1562        The type with `TypeVar`s replaced.1563    """1564    generic_map = generic_map or {}1565    if isinstance(type_, TypeVar):1566        if type_ in generic_map:1567            return generic_map[type_]1568        if default_to_bound:1569            return type_.__bound__ if type_.__bound__ is not None else Any1570        return type_1571    if (origin := get_origin(type_)) and (args := get_args(type_)):1572        new_args = tuple(1573            _replace_type_vars(arg, generic_map, default_to_bound=default_to_bound)1574            for arg in args1575        )1576        return cast("type", _py_38_safe_origin(origin)[new_args])  # type: ignore[index]1577    return type_157815791580class BaseToolkit(BaseModel, ABC):1581    """Base class for toolkits containing related tools.15821583    A toolkit is a collection of related tools that can be used together to accomplish a1584    specific task or work with a particular system.1585    """15861587    @abstractmethod1588    def get_tools(self) -> list[BaseTool]:1589        """Get all tools in the toolkit.15901591        Returns:1592            List of tools contained in this toolkit.1593        """

Code quality findings 56

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):
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
return isinstance(obj, str) or (
Overuse may indicate design issues; consider polymorphism
isinstance-overuse
isinstance(obj, list) and all(_is_message_content_block(e) for e in 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.