core[patch], community[patch], openai[patch]: consolidate openai tool… (#16485)

… converters

One way to convert anything to an OAI function:
convert_to_openai_function
One way to convert anything to an OAI tool: convert_to_openai_tool
Corresponding bind functions on OAI models: bind_functions, bind_tools
This commit is contained in:
Bagatur
2024-01-25 13:18:46 -08:00
committed by GitHub
parent 148347e858
commit ef42d9d559
25 changed files with 1480 additions and 326 deletions

View File

@@ -1,7 +1,9 @@
"""Methods for creating function specs in the style of OpenAI Functions"""
from __future__ import annotations
import inspect
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
@@ -16,12 +18,16 @@ from typing import (
from typing_extensions import TypedDict
from langchain_core._api import deprecated
from langchain_core.pydantic_v1 import BaseModel
from langchain_core.utils.json_schema import dereference_refs
if TYPE_CHECKING:
from langchain_core.tools import BaseTool
PYTHON_TO_JSON_TYPES = {
"str": "string",
"int": "number",
"int": "integer",
"float": "number",
"bool": "boolean",
}
@@ -45,22 +51,47 @@ class ToolDescription(TypedDict):
function: FunctionDescription
def _rm_titles(kv: dict) -> dict:
new_kv = {}
for k, v in kv.items():
if k == "title":
continue
elif isinstance(v, dict):
new_kv[k] = _rm_titles(v)
else:
new_kv[k] = v
return new_kv
@deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="0.2.0",
)
def convert_pydantic_to_openai_function(
model: Type[BaseModel],
*,
name: Optional[str] = None,
description: Optional[str] = None,
rm_titles: bool = True,
) -> FunctionDescription:
"""Converts a Pydantic model to a function description for the OpenAI API."""
schema = dereference_refs(model.schema())
schema.pop("definitions", None)
title = schema.pop("title", "")
default_description = schema.pop("description", "")
return {
"name": name or schema["title"],
"description": description or schema["description"],
"parameters": schema,
"name": name or title,
"description": description or default_description,
"parameters": _rm_titles(schema) if rm_titles else schema,
}
@deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="0.2.0",
)
def convert_pydantic_to_openai_tool(
model: Type[BaseModel],
*,
@@ -132,8 +163,19 @@ def _get_python_function_arguments(function: Callable, arg_descriptions: dict) -
# Mypy error:
# "type" has no attribute "schema"
properties[arg] = arg_type.schema() # type: ignore[attr-defined]
elif arg_type.__name__ in PYTHON_TO_JSON_TYPES:
elif (
hasattr(arg_type, "__name__")
and getattr(arg_type, "__name__") in PYTHON_TO_JSON_TYPES
):
properties[arg] = {"type": PYTHON_TO_JSON_TYPES[arg_type.__name__]}
elif (
hasattr(arg_type, "__dict__")
and getattr(arg_type, "__dict__").get("__origin__", None) == Literal
):
properties[arg] = {
"enum": list(arg_type.__args__), # type: ignore
"type": PYTHON_TO_JSON_TYPES[arg_type.__args__[0].__class__.__name__], # type: ignore
}
if arg in arg_descriptions:
if arg not in properties:
properties[arg] = {}
@@ -153,6 +195,11 @@ def _get_python_function_required_args(function: Callable) -> List[str]:
return required
@deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="0.2.0",
)
def convert_python_function_to_openai_function(
function: Callable,
) -> Dict[str, Any]:
@@ -174,8 +221,49 @@ def convert_python_function_to_openai_function(
}
@deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="0.2.0",
)
def format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
"""Format tool into the OpenAI function API."""
if tool.args_schema:
return convert_pydantic_to_openai_function(
tool.args_schema, name=tool.name, description=tool.description
)
else:
return {
"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
# which is a string.
# And Open AI does not support an array type for the
# parameters.
"properties": {
"__arg1": {"title": "__arg1", "type": "string"},
},
"required": ["__arg1"],
"type": "object",
},
}
@deprecated(
"0.1.16",
alternative="langchain_core.utils.function_calling.convert_to_openai_function()",
removal="0.2.0",
)
def format_tool_to_openai_tool(tool: BaseTool) -> ToolDescription:
"""Format tool into the OpenAI function API."""
function = format_tool_to_openai_function(tool)
return {"type": "function", "function": function}
def convert_to_openai_function(
function: Union[Dict[str, Any], Type[BaseModel], Callable],
function: Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool],
) -> Dict[str, Any]:
"""Convert a raw function/class to an OpenAI function.
@@ -188,15 +276,38 @@ def convert_to_openai_function(
A dict version of the passed in function which is compatible with the
OpenAI function-calling API.
"""
from langchain_core.tools import BaseTool
if isinstance(function, dict):
return function
elif isinstance(function, type) and issubclass(function, BaseModel):
return cast(Dict, convert_pydantic_to_openai_function(function))
elif isinstance(function, BaseTool):
return format_tool_to_openai_function(function)
elif callable(function):
return convert_python_function_to_openai_function(function)
else:
raise ValueError(
f"Unsupported function type {type(function)}. Functions must be passed in"
f" as Dict, pydantic.BaseModel, or Callable."
)
def convert_to_openai_tool(
tool: Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool],
) -> Dict[str, Any]:
"""Convert a raw function/class to an OpenAI tool.
Args:
tool: Either a dictionary, a pydantic.BaseModel class, Python function, or
BaseTool. If a dictionary is passed in, it is assumed to already be a valid
OpenAI tool or OpenAI function.
Returns:
A dict version of the passed in tool which is compatible with the
OpenAI tool-calling API.
"""
if isinstance(tool, dict) and "type" in tool:
return tool
function = convert_to_openai_function(tool)
return {"type": "function", "function": function}

View File

@@ -0,0 +1,74 @@
from typing import Any, Callable, Literal, Type
import pytest
from langchain_core.pydantic_v1 import BaseModel, Field
from langchain_core.tools import BaseTool
from langchain_core.utils.function_calling import convert_to_openai_function
@pytest.fixture()
def pydantic() -> Type[BaseModel]:
class dummy_function(BaseModel):
"""dummy function"""
arg1: int = Field(..., description="foo")
arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'")
return dummy_function
@pytest.fixture()
def function() -> Callable:
def dummy_function(arg1: int, arg2: Literal["bar", "baz"]) -> None:
"""dummy function
Args:
arg1: foo
arg2: one of 'bar', 'baz'
"""
pass
return dummy_function
@pytest.fixture()
def tool() -> BaseTool:
class Schema(BaseModel):
arg1: int = Field(..., description="foo")
arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'")
class DummyFunction(BaseTool):
args_schema: Type[BaseModel] = Schema
name: str = "dummy_function"
description: str = "dummy function"
def _run(self, *args: Any, **kwargs: Any) -> Any:
pass
return DummyFunction()
def test_convert_to_openai_function(
pydantic: Type[BaseModel], function: Callable, tool: BaseTool
) -> None:
expected = {
"name": "dummy_function",
"description": "dummy function",
"parameters": {
"type": "object",
"properties": {
"arg1": {"description": "foo", "type": "integer"},
"arg2": {
"description": "one of 'bar', 'baz'",
"enum": ["bar", "baz"],
"type": "string",
},
},
"required": ["arg1", "arg2"],
},
}
for fn in (pydantic, function, tool, expected):
actual = convert_to_openai_function(fn) # type: ignore
assert actual == expected