From 89368bf0947279db8187e58de67e7467f70f9ba1 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 17 Oct 2025 00:01:03 +0200 Subject: [PATCH 1/2] working fix, failing lint --- libs/core/langchain_core/tools/base.py | 22 ++++++++--------- libs/core/langchain_core/tools/structured.py | 26 ++++++++++---------- libs/core/tests/unit_tests/test_tools.py | 15 +++++++++++ 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/libs/core/langchain_core/tools/base.py b/libs/core/langchain_core/tools/base.py index e016344ba9348..917adba57f96a 100644 --- a/libs/core/langchain_core/tools/base.py +++ b/libs/core/langchain_core/tools/base.py @@ -189,25 +189,23 @@ def _infer_arg_descriptions( *, parse_docstring: bool = False, error_on_invalid_docstring: bool = False, -) -> tuple[str, dict]: - """Infer argument descriptions from function docstring and annotations. - - Args: - fn: The function to infer descriptions from. - parse_docstring: Whether to parse the docstring for descriptions. - error_on_invalid_docstring: Whether to raise error on invalid docstring. - - Returns: - A tuple containing the function description and argument descriptions. - """ +) -> tuple[str | None, dict]: + """Infer argument descriptions from function docstring and annotations.""" annotations = typing.get_type_hints(fn, include_extras=True) if parse_docstring: description, arg_descriptions = _parse_python_function_docstring( fn, annotations, error_on_invalid_docstring=error_on_invalid_docstring ) else: - description = inspect.getdoc(fn) or "" + description = inspect.getdoc(fn) arg_descriptions = {} + + if inspect.isclass(fn) and description: + for parent in fn.__bases__: + if inspect.getdoc(parent) == description: + description = None + break + if parse_docstring: _validate_docstring_args_against_annotations(arg_descriptions, annotations) for arg, arg_type in annotations.items(): diff --git a/libs/core/langchain_core/tools/structured.py b/libs/core/langchain_core/tools/structured.py index 632c450c3b380..d7390f09bdfde 100644 --- a/libs/core/langchain_core/tools/structured.py +++ b/libs/core/langchain_core/tools/structured.py @@ -129,10 +129,10 @@ def from_function( coroutine: Callable[..., Awaitable[Any]] | None = None, name: str | None = None, description: str | None = None, - return_direct: bool = False, # noqa: FBT001,FBT002 - args_schema: ArgsSchema | None = None, - infer_schema: bool = True, # noqa: FBT001,FBT002 *, + return_direct: bool = False, + args_schema: ArgsSchema | None = None, + infer_schema: bool = True, response_format: Literal["content", "content_and_artifact"] = "content", parse_docstring: bool = False, error_on_invalid_docstring: bool = False, @@ -189,7 +189,6 @@ def add(a: int, b: int) -> int: raise ValueError(msg) name = name or source_function.__name__ if args_schema is None and infer_schema: - # schema name is appended within function args_schema = create_schema_from_function( name, source_function, @@ -197,6 +196,7 @@ def add(a: int, b: int) -> int: error_on_invalid_docstring=error_on_invalid_docstring, filter_args=_filter_schema_args(source_function), ) + description_ = description if description is None and not parse_docstring: description_ = source_function.__doc__ or None @@ -213,20 +213,20 @@ def add(a: int, b: int) -> int: elif isinstance(args_schema, dict): description_ = args_schema.get("description") else: - msg = ( - "Invalid args_schema: expected BaseModel or dict, " - f"got {args_schema}" - ) + msg = f"""Invalid args_schema: expected BaseModel or + dict, got {args_schema}""" raise TypeError(msg) + if description_ is None: - msg = "Function must have a docstring if description not provided." - raise ValueError(msg) + if is_basemodel_subclass(source_function): + description_ = "" + else: + msg = "Function must have a docstring if description not provided." + raise ValueError(msg) + if description is None: - # Only apply if using the function's docstring description_ = textwrap.dedent(description_).strip() - # Description example: - # search_api(query: str) - Searches the API for the query. description_ = f"{description_.strip()}" return cls( name=name, diff --git a/libs/core/tests/unit_tests/test_tools.py b/libs/core/tests/unit_tests/test_tools.py index 1e385bf74fb64..338ae44b92d3a 100644 --- a/libs/core/tests/unit_tests/test_tools.py +++ b/libs/core/tests/unit_tests/test_tools.py @@ -2757,3 +2757,18 @@ def test_tool( "type": "array", } } + + +def test_child_tool_does_not_inherit_docstring() -> None: + """Test that a tool subclass does not inherit its parent's docstring.""" + + class MyTool(BaseModel): + """Parent Tool.""" + + foo: str + + @tool + class ChildTool(MyTool): + bar: str + + assert ChildTool.description == "" # type: ignore[attr-defined] From 5fe1542098c15ee1dbd62e6af4871081551b70bb Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 17 Oct 2025 00:06:26 +0200 Subject: [PATCH 2/2] fix(core): prevent inherited docstrings for class-based tools --- libs/core/langchain_core/tools/base.py | 3 +++ libs/core/langchain_core/tools/structured.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/core/langchain_core/tools/base.py b/libs/core/langchain_core/tools/base.py index 917adba57f96a..094ca96171150 100644 --- a/libs/core/langchain_core/tools/base.py +++ b/libs/core/langchain_core/tools/base.py @@ -192,6 +192,9 @@ def _infer_arg_descriptions( ) -> tuple[str | None, dict]: """Infer argument descriptions from function docstring and annotations.""" annotations = typing.get_type_hints(fn, include_extras=True) + description: str | None + arg_descriptions: dict + if parse_docstring: description, arg_descriptions = _parse_python_function_docstring( fn, annotations, error_on_invalid_docstring=error_on_invalid_docstring diff --git a/libs/core/langchain_core/tools/structured.py b/libs/core/langchain_core/tools/structured.py index d7390f09bdfde..b27c33ead2838 100644 --- a/libs/core/langchain_core/tools/structured.py +++ b/libs/core/langchain_core/tools/structured.py @@ -2,6 +2,7 @@ from __future__ import annotations +import inspect import textwrap from collections.abc import Awaitable, Callable from inspect import signature @@ -218,7 +219,9 @@ def add(a: int, b: int) -> int: raise TypeError(msg) if description_ is None: - if is_basemodel_subclass(source_function): + if inspect.isclass(source_function) and is_basemodel_subclass( + source_function + ): description_ = "" else: msg = "Function must have a docstring if description not provided."