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 oai tool description optional #27756

Merged
merged 6 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
66 changes: 42 additions & 24 deletions libs/core/langchain_core/utils/function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def convert_to_openai_function(
A dictionary, Pydantic BaseModel class, TypedDict class, a LangChain
Tool object, or a Python function. If a dictionary is passed in, it is
assumed to already be a valid OpenAI function, a JSON schema with
top-level 'title' and 'description' keys specified, an Anthropic format
top-level 'title' key specified, an Anthropic format
tool, or an Amazon Bedrock Converse format tool.
strict:
If True, model output is guaranteed to exactly match the JSON Schema
Expand All @@ -365,41 +365,45 @@ def convert_to_openai_function(

.. versionchanged:: 0.3.14

Support for Amazon Bedrock Converse format tools added.
- Support for Amazon Bedrock Converse format tools added.
- 'description' and 'parameters' keys are now optional. Only 'name' is
required and guaranteed to be part of the output.
"""
from langchain_core.tools import BaseTool

# already in OpenAI function format
if isinstance(function, dict) and all(
k in function for k in ("name", "description", "parameters")
):
oai_function = 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()
oai_function = {
"name": function.pop("title"),
"description": function.pop("description"),
"parameters": function,
}
# an Anthropic format tool
elif isinstance(function, dict) and all(
k in function for k in ("name", "description", "input_schema")
if isinstance(function, dict) and all(
k in function for k in ("name", "input_schema")
):
oai_function = {
"name": function["name"],
"description": function["description"],
"parameters": function["input_schema"],
}
if "description" in function:
oai_function["description"] = function["description"]
# an Amazon Bedrock Converse format tool
elif isinstance(function, dict) and "toolSpec" in function:
oai_function = {
"name": function["toolSpec"]["name"],
"description": function["toolSpec"]["description"],
"parameters": function["toolSpec"]["inputSchema"]["json"],
}
if "description" in function["toolSpec"]:
oai_function["description"] = function["toolSpec"]["description"]
# already in OpenAI function format
elif isinstance(function, dict) and "name" in function:
oai_function = {
k: v
for k, v in function.items()
if k in ("name", "description", "parameters", "strict")
}
# a JSON schema with title and description
elif isinstance(function, dict) and "title" in function:
function_copy = function.copy()
oai_function = {"name": function_copy.pop("title")}
if "description" in function_copy:
oai_function["description"] = function_copy.pop("description")
if function_copy and "properties" in function_copy:
oai_function["parameters"] = function_copy
elif isinstance(function, type) and is_basemodel_subclass(function):
oai_function = cast(dict, convert_pydantic_to_openai_function(function))
elif is_typeddict(function):
Expand All @@ -420,6 +424,9 @@ def convert_to_openai_function(
raise ValueError(msg)

if strict is not None:
if "strict" in oai_function and oai_function["strict"] != strict:
msg = ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we update this error message?

raise ValueError(msg)
oai_function["strict"] = strict
if strict:
# As of 08/06/24, OpenAI requires that additionalProperties be supplied and
Expand All @@ -438,12 +445,16 @@ def convert_to_openai_tool(
) -> dict[str, Any]:
"""Convert a tool-like object to an OpenAI tool schema.

OpenAI tool schema reference:
https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools

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, OpenAI function, a JSON schema with top-level 'title' and
'description' keys specified, or an Anthropic format tool.
BaseTool. If a dictionary is passed in, it is
assumed to already be a valid OpenAI function, a JSON schema with
top-level 'title' key specified, an Anthropic format
tool, or an Amazon Bedrock Converse format tool.
strict:
If True, model output is guaranteed to exactly match the JSON Schema
provided in the function definition. If None, ``strict`` argument will not
Expand All @@ -460,6 +471,13 @@ def convert_to_openai_tool(
.. versionchanged:: 0.3.13

Support for Anthropic format tools added.

.. versionchanged:: 0.3.14

- Support for Amazon Bedrock Converse format tools added.
-
'description' and 'parameters' keys are now optional.
Only 'name' is required and guaranteed to be part of the 'function' dict.
"""
if isinstance(tool, dict) and tool.get("type") == "function" and "function" in tool:
return tool
Expand Down
124 changes: 124 additions & 0 deletions libs/core/tests/unit_tests/utils/test_function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,130 @@ def my_function(arg1: Nested) -> None:
assert actual == expected


json_schema_no_description_no_params = {
"title": "dummy_function",
}


json_schema_no_description = {
"title": "dummy_function",
"type": "object",
"properties": {
"arg1": {"description": "foo", "type": "integer"},
"arg2": {
"description": "one of 'bar', 'baz'",
"enum": ["bar", "baz"],
"type": "string",
},
},
"required": ["arg1", "arg2"],
}


anthropic_tool_no_description = {
"name": "dummy_function",
"input_schema": {
"type": "object",
"properties": {
"arg1": {"description": "foo", "type": "integer"},
"arg2": {
"description": "one of 'bar', 'baz'",
"enum": ["bar", "baz"],
"type": "string",
},
},
"required": ["arg1", "arg2"],
},
}


bedrock_converse_tool_no_description = {
"toolSpec": {
"name": "dummy_function",
"inputSchema": {
"json": {
"type": "object",
"properties": {
"arg1": {"description": "foo", "type": "integer"},
"arg2": {
"description": "one of 'bar', 'baz'",
"enum": ["bar", "baz"],
"type": "string",
},
},
"required": ["arg1", "arg2"],
}
},
}
}


openai_function_no_description = {
"name": "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"],
},
}


openai_function_no_description_no_params = {
"name": "dummy_function",
}


@pytest.mark.parametrize(
"func",
[
anthropic_tool_no_description,
json_schema_no_description,
bedrock_converse_tool_no_description,
openai_function_no_description,
],
)
def test_convert_to_openai_function_no_description(func: dict) -> None:
expected = {
"name": "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"],
},
}
actual = convert_to_openai_function(func)
assert actual == expected


@pytest.mark.parametrize(
"func",
[
json_schema_no_description_no_params,
openai_function_no_description_no_params,
],
)
def test_convert_to_openai_function_no_description_no_params(func: dict) -> None:
expected = {
"name": "dummy_function",
}
actual = convert_to_openai_function(func)
assert actual == expected


@pytest.mark.xfail(
reason="Pydantic converts Optional[str] to str in .model_json_schema()"
)
Expand Down
Loading