core[patch]: support labeled json schema as tools (#18935)

This commit is contained in:
Bagatur 2024-03-11 19:51:35 -07:00 committed by GitHub
parent 950ab056eb
commit 19721246f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 49 additions and 10 deletions

View File

@ -270,7 +270,8 @@ def convert_to_openai_function(
Args: Args:
function: Either a dictionary, a pydantic.BaseModel class, or a Python function. function: Either a dictionary, a pydantic.BaseModel class, or a Python function.
If a dictionary is passed in, it is assumed to already be a valid OpenAI If a dictionary is passed in, it is assumed to already be a valid OpenAI
function. function or a JSON schema with top-level 'title' and 'description' keys
specified.
Returns: Returns:
A dict version of the passed in function which is compatible with the A dict version of the passed in function which is compatible with the
@ -278,8 +279,21 @@ def convert_to_openai_function(
""" """
from langchain_core.tools import BaseTool from langchain_core.tools import BaseTool
if isinstance(function, dict): # already in OpenAI function format
if isinstance(function, dict) and all(
k in function for k in ("name", "description", "parameters")
):
return function return function
# a JSON schema with title and description
elif isinstance(function, dict) and all(
k in function for k in ("title", "description", "properties")
):
function = function.copy()
return {
"name": function.pop("title"),
"description": function.pop("description"),
"parameters": function,
}
elif isinstance(function, type) and issubclass(function, BaseModel): elif isinstance(function, type) and issubclass(function, BaseModel):
return cast(Dict, convert_pydantic_to_openai_function(function)) return cast(Dict, convert_pydantic_to_openai_function(function))
elif isinstance(function, BaseTool): elif isinstance(function, BaseTool):
@ -288,8 +302,10 @@ def convert_to_openai_function(
return convert_python_function_to_openai_function(function) return convert_python_function_to_openai_function(function)
else: else:
raise ValueError( raise ValueError(
f"Unsupported function type {type(function)}. Functions must be passed in" f"Unsupported function\n\n{function}\n\nFunctions must be passed in"
f" as Dict, pydantic.BaseModel, or Callable." " as Dict, pydantic.BaseModel, or Callable. If they're a dict they must"
" either be in OpenAI function format or valid JSON schema with top-level"
" 'title' and 'description' keys."
) )
@ -301,13 +317,14 @@ def convert_to_openai_tool(
Args: Args:
tool: Either a dictionary, a pydantic.BaseModel class, Python function, or 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 BaseTool. If a dictionary is passed in, it is assumed to already be a valid
OpenAI tool or OpenAI function. OpenAI tool, OpenAI function, or a JSON schema with top-level 'title' and
'description' keys specified.
Returns: Returns:
A dict version of the passed in tool which is compatible with the A dict version of the passed in tool which is compatible with the
OpenAI tool-calling API. OpenAI tool-calling API.
""" """
if isinstance(tool, dict) and "type" in tool: if isinstance(tool, dict) and tool.get("type") == "function" and "function" in tool:
return tool return tool
function = convert_to_openai_function(tool) function = convert_to_openai_function(tool)
return {"type": "function", "function": function} return {"type": "function", "function": function}

View File

@ -1,4 +1,4 @@
from typing import Any, Callable, List, Literal, Optional, Type from typing import Any, Callable, Dict, List, Literal, Optional, Type
import pytest import pytest
@ -49,8 +49,29 @@ def dummy_tool() -> BaseTool:
return DummyFunction() return DummyFunction()
@pytest.fixture()
def json_schema() -> Dict:
return {
"title": "dummy_function",
"description": "dummy function",
"type": "object",
"properties": {
"arg1": {"description": "foo", "type": "integer"},
"arg2": {
"description": "one of 'bar', 'baz'",
"enum": ["bar", "baz"],
"type": "string",
},
},
"required": ["arg1", "arg2"],
}
def test_convert_to_openai_function( def test_convert_to_openai_function(
pydantic: Type[BaseModel], function: Callable, dummy_tool: BaseTool pydantic: Type[BaseModel],
function: Callable,
dummy_tool: BaseTool,
json_schema: Dict,
) -> None: ) -> None:
expected = { expected = {
"name": "dummy_function", "name": "dummy_function",
@ -69,7 +90,7 @@ def test_convert_to_openai_function(
}, },
} }
for fn in (pydantic, function, dummy_tool, expected): for fn in (pydantic, function, dummy_tool, json_schema, expected):
actual = convert_to_openai_function(fn) # type: ignore actual = convert_to_openai_function(fn) # type: ignore
assert actual == expected assert actual == expected

View File

@ -799,7 +799,8 @@ class ChatOpenAI(BaseChatModel):
the model output will be a dict. With a Pydantic class the returned the model output will be a dict. With a Pydantic class the returned
attributes will be validated, whereas with a dict they will not be. If attributes will be validated, whereas with a dict they will not be. If
`method` is "function_calling" and `schema` is a dict, then the dict `method` is "function_calling" and `schema` is a dict, then the dict
must match the OpenAI function-calling spec. must match the OpenAI function-calling spec or be a valid JSON schema
with top level 'title' and 'description' keys specified.
method: The method for steering model generation, either "function_calling" method: The method for steering model generation, either "function_calling"
or "json_mode". If "function_calling" then the schema will be converted or "json_mode". If "function_calling" then the schema will be converted
to an OpenAI function and the returned model will make use of the to an OpenAI function and the returned model will make use of the