core[patch]: remove deprecated functions from tool binding hotpath (#29015)

(Inspired by https://github.com/langchain-ai/langchain/issues/26918)

We rely on some deprecated public functions in the hot path for tool
binding (`convert_pydantic_to_openai_function`,
`convert_python_function_to_openai_function`, and
`format_tool_to_openai_function`). My understanding is that what is
deprecated is not the functionality they implement, but use of them in
the public API -- we expect to continue to rely on them.

Here we update these functions to be private and not deprecated. We keep
the public, deprecated functions as simple wrappers that can be safely
deleted.

The `@deprecated` wrapper adds considerable latency due to its use of
the `inspect` module. This update speeds up `bind_tools` by a factor of
~100x:

Before:

![Screenshot 2025-01-03 at 11 22
55 AM](https://github.com/user-attachments/assets/94b1c433-ce12-406f-b64c-ca7103badfe0)

After:

![Screenshot 2025-01-03 at 11 23
41 AM](https://github.com/user-attachments/assets/02d0deab-82e4-45ca-8cc7-a20b91a5b5db)

---------

Co-authored-by: Erick Friis <erick@langchain.dev>
This commit is contained in:
ccurme 2025-01-03 14:29:01 -05:00 committed by GitHub
parent a86904e735
commit 4bb391fd4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -75,12 +75,7 @@ def _rm_titles(kv: dict, prev_key: str = "") -> dict:
return new_kv return new_kv
@deprecated( def _convert_pydantic_to_openai_function(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="1.0",
)
def convert_pydantic_to_openai_function(
model: type, model: type,
*, *,
name: Optional[str] = None, name: Optional[str] = None,
@ -121,6 +116,13 @@ def convert_pydantic_to_openai_function(
} }
convert_pydantic_to_openai_function = deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="1.0",
)(_convert_pydantic_to_openai_function)
@deprecated( @deprecated(
"0.1.16", "0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_tool()", alternative="langchain_core.utils.function_calling.convert_to_openai_tool()",
@ -144,7 +146,7 @@ def convert_pydantic_to_openai_tool(
Returns: Returns:
The tool description. The tool description.
""" """
function = convert_pydantic_to_openai_function( function = _convert_pydantic_to_openai_function(
model, name=name, description=description model, name=name, description=description
) )
return {"type": "function", "function": function} return {"type": "function", "function": function}
@ -155,12 +157,7 @@ def _get_python_function_name(function: Callable) -> str:
return function.__name__ return function.__name__
@deprecated( def _convert_python_function_to_openai_function(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="1.0",
)
def convert_python_function_to_openai_function(
function: Callable, function: Callable,
) -> FunctionDescription: ) -> FunctionDescription:
"""Convert a Python function to an OpenAI function-calling API compatible dict. """Convert a Python function to an OpenAI function-calling API compatible dict.
@ -186,13 +183,20 @@ def convert_python_function_to_openai_function(
error_on_invalid_docstring=False, error_on_invalid_docstring=False,
include_injected=False, include_injected=False,
) )
return convert_pydantic_to_openai_function( return _convert_pydantic_to_openai_function(
model, model,
name=func_name, name=func_name,
description=model.__doc__, description=model.__doc__,
) )
convert_python_function_to_openai_function = deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="1.0",
)(_convert_python_function_to_openai_function)
def _convert_typed_dict_to_openai_function(typed_dict: type) -> FunctionDescription: def _convert_typed_dict_to_openai_function(typed_dict: type) -> FunctionDescription:
visited: dict = {} visited: dict = {}
from pydantic.v1 import BaseModel from pydantic.v1 import BaseModel
@ -201,7 +205,7 @@ def _convert_typed_dict_to_openai_function(typed_dict: type) -> FunctionDescript
type[BaseModel], type[BaseModel],
_convert_any_typed_dicts_to_pydantic(typed_dict, visited=visited), _convert_any_typed_dicts_to_pydantic(typed_dict, visited=visited),
) )
return convert_pydantic_to_openai_function(model) # type: ignore return _convert_pydantic_to_openai_function(model) # type: ignore
_MAX_TYPED_DICT_RECURSION = 25 _MAX_TYPED_DICT_RECURSION = 25
@ -272,12 +276,7 @@ def _convert_any_typed_dicts_to_pydantic(
return type_ return type_
@deprecated( def _format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="1.0",
)
def format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
"""Format tool into the OpenAI function API. """Format tool into the OpenAI function API.
Args: Args:
@ -290,7 +289,7 @@ def format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
is_simple_oai_tool = isinstance(tool, simple.Tool) and not tool.args_schema is_simple_oai_tool = isinstance(tool, simple.Tool) and not tool.args_schema
if tool.tool_call_schema and not is_simple_oai_tool: if tool.tool_call_schema and not is_simple_oai_tool:
return convert_pydantic_to_openai_function( return _convert_pydantic_to_openai_function(
tool.tool_call_schema, name=tool.name, description=tool.description tool.tool_call_schema, name=tool.name, description=tool.description
) )
else: else:
@ -312,6 +311,13 @@ def format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
} }
format_tool_to_openai_function = deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="1.0",
)(_format_tool_to_openai_function)
@deprecated( @deprecated(
"0.1.16", "0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_tool()", alternative="langchain_core.utils.function_calling.convert_to_openai_tool()",
@ -326,7 +332,7 @@ def format_tool_to_openai_tool(tool: BaseTool) -> ToolDescription:
Returns: Returns:
The tool description. The tool description.
""" """
function = format_tool_to_openai_function(tool) function = _format_tool_to_openai_function(tool)
return {"type": "function", "function": function} return {"type": "function", "function": function}
@ -408,15 +414,15 @@ def convert_to_openai_function(
if function_copy and "properties" in function_copy: if function_copy and "properties" in function_copy:
oai_function["parameters"] = function_copy oai_function["parameters"] = function_copy
elif isinstance(function, type) and is_basemodel_subclass(function): elif isinstance(function, type) and is_basemodel_subclass(function):
oai_function = cast(dict, convert_pydantic_to_openai_function(function)) oai_function = cast(dict, _convert_pydantic_to_openai_function(function))
elif is_typeddict(function): elif is_typeddict(function):
oai_function = cast( oai_function = cast(
dict, _convert_typed_dict_to_openai_function(cast(type, function)) dict, _convert_typed_dict_to_openai_function(cast(type, function))
) )
elif isinstance(function, BaseTool): elif isinstance(function, BaseTool):
oai_function = cast(dict, format_tool_to_openai_function(function)) oai_function = cast(dict, _format_tool_to_openai_function(function))
elif callable(function): elif callable(function):
oai_function = cast(dict, convert_python_function_to_openai_function(function)) oai_function = cast(dict, _convert_python_function_to_openai_function(function))
else: else:
msg = ( msg = (
f"Unsupported function\n\n{function}\n\nFunctions must be passed in" f"Unsupported function\n\n{function}\n\nFunctions must be passed in"