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 # 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 """
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.