From 968dccee0458d78db2a60c9c30d3dfb041399af4 Mon Sep 17 00:00:00 2001 From: Bagatur <22008038+baskaryan@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:27:06 -0700 Subject: [PATCH] core[patch]: convert_to_openai_tool Anthropic support (#27591) --- libs/core/langchain_core/messages/utils.py | 4 +- .../langchain_core/utils/function_calling.py | 52 ++++++++++++------- .../unit_tests/utils/test_function_calling.py | 22 ++++++++ 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/libs/core/langchain_core/messages/utils.py b/libs/core/langchain_core/messages/utils.py index 23996393412..9bf617af17d 100644 --- a/libs/core/langchain_core/messages/utils.py +++ b/libs/core/langchain_core/messages/utils.py @@ -883,8 +883,6 @@ def convert_to_openai_messages( ) -> Union[dict, list[dict]]: """Convert LangChain messages into OpenAI message dicts. - .. versionadded:: 0.3.11 - Args: messages: Message-like object or iterable of objects whose contents are in OpenAI, Anthropic, Bedrock Converse, or VertexAI formats. @@ -937,6 +935,8 @@ def convert_to_openai_messages( # {'role': 'assistant', 'content': 'thats nice'} # ] + .. versionadded:: 0.3.11 + """ # noqa: E501 if text_format not in ("string", "block"): err = f"Unrecognized {text_format=}, expected one of 'string' or 'block'." diff --git a/libs/core/langchain_core/utils/function_calling.py b/libs/core/langchain_core/utils/function_calling.py index 9bb62c92b9e..e06aa04266d 100644 --- a/libs/core/langchain_core/utils/function_calling.py +++ b/libs/core/langchain_core/utils/function_calling.py @@ -336,30 +336,32 @@ def convert_to_openai_function( strict: Optional[bool] = None, ) -> dict[str, Any]: """Convert a raw function/class to an OpenAI function. - - .. versionchanged:: 0.2.29 - - ``strict`` arg added. - Args: 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 or a JSON schema with - top-level 'title' and 'description' keys specified. + assumed to already be a valid OpenAI function, a JSON schema with + top-level 'title' and 'description' keys specified, or an Anthropic 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 be included in function definition. - .. versionadded:: 0.2.29 - Returns: A dict version of the passed in function which is compatible with the OpenAI function-calling API. Raises: ValueError: If function is not in a supported format. + + .. versionchanged:: 0.2.29 + + ``strict`` arg added. + + .. versionchanged:: 0.3.13 + + Support for Anthropic format tools added. """ from langchain_core.tools import BaseTool @@ -378,6 +380,15 @@ def convert_to_openai_function( "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") + ): + oai_function = { + "name": function["name"], + "description": function["description"], + "parameters": function["input_schema"], + } elif isinstance(function, type) and is_basemodel_subclass(function): oai_function = cast(dict, convert_pydantic_to_openai_function(function)) elif is_typeddict(function): @@ -414,34 +425,35 @@ def convert_to_openai_tool( *, strict: Optional[bool] = None, ) -> dict[str, Any]: - """Convert a raw function/class to an OpenAI tool. - - .. versionchanged:: 0.2.29 - - ``strict`` arg added. + """Convert a tool-like object to an OpenAI tool schema. 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, or a JSON schema with top-level 'title' and - 'description' keys specified. + OpenAI tool, OpenAI function, a JSON schema with top-level 'title' and + 'description' keys specified, or an Anthropic 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 be included in tool definition. - .. versionadded:: 0.2.29 - Returns: A dict version of the passed in tool which is compatible with the OpenAI tool-calling API. + + .. versionchanged:: 0.2.29 + + ``strict`` arg added. + + .. versionchanged:: 0.3.13 + + Support for Anthropic format tools added. """ if isinstance(tool, dict) and tool.get("type") == "function" and "function" in tool: return tool oai_function = convert_to_openai_function(tool, strict=strict) - oai_tool: dict[str, Any] = {"type": "function", "function": oai_function} - return oai_tool + return {"type": "function", "function": oai_function} def tool_example_to_messages( diff --git a/libs/core/tests/unit_tests/utils/test_function_calling.py b/libs/core/tests/unit_tests/utils/test_function_calling.py index bd4694e0d52..a74ddf7ec15 100644 --- a/libs/core/tests/unit_tests/utils/test_function_calling.py +++ b/libs/core/tests/unit_tests/utils/test_function_calling.py @@ -210,6 +210,26 @@ def json_schema() -> dict: } +@pytest.fixture() +def anthropic_tool() -> dict: + return { + "name": "dummy_function", + "description": "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"], + }, + } + + class Dummy: def dummy_function(self, arg1: int, arg2: Literal["bar", "baz"]) -> None: """dummy function @@ -237,6 +257,7 @@ def test_convert_to_openai_function( dummy_structured_tool: StructuredTool, dummy_tool: BaseTool, json_schema: dict, + anthropic_tool: dict, annotated_function: Callable, dummy_pydantic: type[BaseModel], runnable: Runnable, @@ -268,6 +289,7 @@ def test_convert_to_openai_function( dummy_structured_tool, dummy_tool, json_schema, + anthropic_tool, expected, Dummy.dummy_function, DummyWithClassMethod.dummy_function,