Overuse may indicate design issues; consider polymorphism
if isinstance(annotation, str):
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 """
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.