From 398718e1cb555e526bbf76ae163ded4c8d3cd1f8 Mon Sep 17 00:00:00 2001 From: ccurme Date: Wed, 11 Sep 2024 15:51:10 -0400 Subject: [PATCH] core[patch]: fix regression in convert_to_openai_tool with instances of Tool (#26327) ```python from langchain_core.tools import Tool from langchain_core.utils.function_calling import convert_to_openai_tool def my_function(x: int) -> int: return x + 2 tool = Tool( name="tool_name", func=my_function, description="test description", ) convert_to_openai_tool(tool) ``` Current: ``` {'type': 'function', 'function': {'name': 'tool_name', 'description': 'test description', 'parameters': {'type': 'object', 'properties': {'args': {'type': 'array', 'items': {}}, 'config': {'type': 'object', 'properties': {'tags': {'type': 'array', 'items': {'type': 'string'}}, 'metadata': {'type': 'object'}, 'callbacks': {'anyOf': [{'type': 'array', 'items': {}}, {}]}, 'run_name': {'type': 'string'}, 'max_concurrency': {'type': 'integer'}, 'recursion_limit': {'type': 'integer'}, 'configurable': {'type': 'object'}, 'run_id': {'type': 'string', 'format': 'uuid'}}}, 'kwargs': {'type': 'object'}}, 'required': ['config']}}} ``` Here: ``` {'type': 'function', 'function': {'name': 'tool_name', 'description': 'test description', 'parameters': {'properties': {'__arg1': {'title': '__arg1', 'type': 'string'}}, 'required': ['__arg1'], 'type': 'object'}}} ``` --- .../langchain_core/utils/function_calling.py | 5 ++- .../unit_tests/utils/test_function_calling.py | 39 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/libs/core/langchain_core/utils/function_calling.py b/libs/core/langchain_core/utils/function_calling.py index c5c4d82461d..67ac7ebd7f9 100644 --- a/libs/core/langchain_core/utils/function_calling.py +++ b/libs/core/langchain_core/utils/function_calling.py @@ -280,7 +280,10 @@ def format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription: Returns: The function description. """ - if tool.tool_call_schema: + from langchain_core.tools import simple + + is_simple_oai_tool = isinstance(tool, simple.Tool) and not tool.args_schema + if tool.tool_call_schema and not is_simple_oai_tool: return convert_pydantic_to_openai_function( tool.tool_call_schema, name=tool.name, description=tool.description ) 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 ba5ba4c9fa6..9570c52d4c2 100644 --- a/libs/core/tests/unit_tests/utils/test_function_calling.py +++ b/libs/core/tests/unit_tests/utils/test_function_calling.py @@ -37,7 +37,7 @@ except ImportError: from langchain_core.messages import AIMessage, HumanMessage, ToolMessage from langchain_core.pydantic_v1 import BaseModel, Field from langchain_core.runnables import Runnable, RunnableLambda -from langchain_core.tools import BaseTool, tool +from langchain_core.tools import BaseTool, StructuredTool, Tool, tool from langchain_core.utils.function_calling import ( _convert_typed_dict_to_openai_function, convert_to_openai_function, @@ -111,6 +111,20 @@ def dummy_tool() -> BaseTool: return DummyFunction() +@pytest.fixture() +def dummy_structured_tool() -> StructuredTool: + class Schema(BaseModel): + arg1: int = Field(..., description="foo") + arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'") + + return StructuredTool.from_function( + lambda x: None, + name="dummy_function", + description="dummy function", + args_schema=Schema, + ) + + @pytest.fixture() def dummy_pydantic() -> Type[BaseModel]: class dummy_function(BaseModel): @@ -233,6 +247,7 @@ class DummyWithClassMethod: def test_convert_to_openai_function( pydantic: Type[BaseModel], function: Callable, + dummy_structured_tool: StructuredTool, dummy_tool: BaseTool, json_schema: Dict, Annotated_function: Callable, @@ -263,6 +278,7 @@ def test_convert_to_openai_function( for fn in ( pydantic, function, + dummy_structured_tool, dummy_tool, json_schema, expected, @@ -295,6 +311,27 @@ def test_convert_to_openai_function( runnable_expected["parameters"] = parameters assert actual == runnable_expected + # Test simple Tool + def my_function(input_string: str) -> str: + pass + + tool = Tool( + name="dummy_function", + func=my_function, + description="test description", + ) + actual = convert_to_openai_function(tool) + expected = { + "name": "dummy_function", + "description": "test description", + "parameters": { + "properties": {"__arg1": {"title": "__arg1", "type": "string"}}, + "required": ["__arg1"], + "type": "object", + }, + } + assert actual == expected + @pytest.mark.xfail(reason="Direct pydantic v2 models not yet supported") def test_convert_to_openai_function_nested_v2() -> None: