Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core[patch]: make Tool.description optional #27759

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libs/core/langchain_core/tools/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ class ChildTool(BaseTool):

name: str
"""The unique name of the tool that clearly communicates its purpose."""
description: str
description: Optional[str] = None
"""Used to tell the model how/when/why to use the tool.

You can provide few-shot examples as a part of the description.
Expand Down
10 changes: 4 additions & 6 deletions libs/core/langchain_core/tools/structured.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
class StructuredTool(BaseTool):
"""Tool that can operate on any number of inputs."""

description: str = ""
description: Optional[str] = ""
args_schema: Annotated[TypeBaseModel, SkipValidation()] = Field(
..., description="The tool schema."
)
Expand Down Expand Up @@ -185,16 +185,14 @@ def add(a: int, b: int) -> int:
description_ = source_function.__doc__ or None
if description_ is None and args_schema:
description_ = args_schema.__doc__ or None
if description_ is None:
msg = "Function must have a docstring if description not provided."
raise ValueError(msg)
if description is None:
if description is None and description_ is not 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()}"
if description_:
description_ = f"{description_.strip()}"
return cls(
name=name,
func=func,
Expand Down
18 changes: 13 additions & 5 deletions libs/core/langchain_core/utils/function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
)

from pydantic import BaseModel
from typing_extensions import TypedDict, get_args, get_origin, is_typeddict
from typing_extensions import NotRequired, TypedDict, get_args, get_origin, is_typeddict

from langchain_core._api import deprecated
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage
Expand All @@ -45,10 +45,16 @@ class FunctionDescription(TypedDict):

name: str
"""The name of the function."""
description: str
description: NotRequired[str]
"""A description of the function."""
parameters: dict
parameters: NotRequired[dict]
"""The parameters of the function."""
strict: NotRequired[Optional[bool]]
"""Whether to enable strict schema adherence when generating the function call.

If set to True, the model will follow the exact schema defined in the parameters
field. Only a subset of JSON Schema is supported when strict is True.
"""


class ToolDescription(TypedDict):
Expand Down Expand Up @@ -294,9 +300,8 @@ def format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
tool.tool_call_schema, name=tool.name, description=tool.description
)
else:
return {
oai_function = {
"name": tool.name,
"description": tool.description,
"parameters": {
# This is a hack to get around the fact that some tools
# do not expose an args_schema, and expect an argument
Expand All @@ -310,6 +315,9 @@ def format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
"type": "object",
},
}
if tool.description:
oai_function["description"] = tool.description
return cast(FunctionDescription, oai_function)


@deprecated(
Expand Down
1 change: 1 addition & 0 deletions libs/core/tests/unit_tests/runnables/test_runnable.py
Original file line number Diff line number Diff line change
Expand Up @@ -4623,6 +4623,7 @@ async def test_tool_from_runnable() -> None:
assert await chain_tool.arun({"question": "What up"}) == await chain.ainvoke(
{"question": "What up"}
)
assert chain_tool.description
assert chain_tool.description.endswith(repr(chain))
assert _schema(chain_tool.args_schema) == chain.get_input_jsonschema()
assert _schema(chain_tool.args_schema) == {
Expand Down
20 changes: 14 additions & 6 deletions libs/core/tests/unit_tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,13 +651,21 @@ def search_api(


def test_missing_docstring() -> None:
"""Test error is raised when docstring is missing."""
# expect to throw a value error if there's no docstring
with pytest.raises(ValueError, match="Function must have a docstring"):
"""Test error is not raised when docstring is missing."""

@tool
def search_api(query: str) -> str:
return "API result"
@tool
def search_api(query: str) -> str:
return "API result"

assert search_api.name == "search_api"
assert search_api.description is None
assert search_api.args_schema
assert search_api.args_schema.model_json_schema() == {
"properties": {"query": {"title": "Query", "type": "string"}},
"required": ["query"],
"title": "search_api",
"type": "object",
}


def test_create_tool_positional_args() -> None:
Expand Down
Loading