From 7e070c6760d66ad0a490a8c3901ea3acc9f4bb84 Mon Sep 17 00:00:00 2001 From: Maruf Aytekin Date: Sun, 23 Feb 2025 23:56:37 -0500 Subject: [PATCH 1/4] [Bug Fix] Tool signature error for Anthropic #1091 --- autogen/oai/anthropic.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/autogen/oai/anthropic.py b/autogen/oai/anthropic.py index a1d1e95003..d64b11ee1d 100644 --- a/autogen/oai/anthropic.py +++ b/autogen/oai/anthropic.py @@ -365,11 +365,42 @@ def get_usage(response: ChatCompletion) -> dict: @staticmethod def convert_tools_to_functions(tools: list) -> list: + """ + Convert tool definitions into Anthropic-compatible functions, + updating nested $ref paths in property schemas. + + Args: + tools (list): List of tool definitions. + + Returns: + list: List of functions with updated $ref paths. + """ + + def update_refs(obj, defs_keys, prop_name): + """Recursively update $ref values that start with "#/$defs/". """ + if isinstance(obj, dict): + for key, value in obj.items(): + if key == "$ref" and isinstance(value, str) and value.startswith("#/$defs/"): + ref_key = value[len("#/$defs/"):] + if ref_key in defs_keys: + obj[key] = f"#/properties/{prop_name}/$defs/{ref_key}" + else: + update_refs(value, defs_keys, prop_name) + elif isinstance(obj, list): + for item in obj: + update_refs(item, defs_keys, prop_name) + functions = [] for tool in tools: if tool.get("type") == "function" and "function" in tool: - functions.append(tool["function"]) - + function = tool["function"] + parameters = function.get("parameters", {}) + properties = parameters.get("properties", {}) + for prop_name, prop_schema in properties.items(): + if "$defs" in prop_schema: + defs_keys = set(prop_schema["$defs"].keys()) + update_refs(prop_schema, defs_keys, prop_name) + functions.append(function) return functions def _add_response_format_to_system(self, params: dict[str, Any]): From e50a054ed75bde6a0dca7dd10d5c65d05238198e Mon Sep 17 00:00:00 2001 From: Maruf Aytekin Date: Mon, 24 Feb 2025 01:39:11 -0500 Subject: [PATCH 2/4] tests added --- test/oai/test_anthropic.py | 88 +++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/test/oai/test_anthropic.py b/test/oai/test_anthropic.py index 639c2f36f5..be29e8ee33 100644 --- a/test/oai/test_anthropic.py +++ b/test/oai/test_anthropic.py @@ -5,10 +5,11 @@ # Portions derived from https://github.com/microsoft/autogen are under the MIT License. # SPDX-License-Identifier: MIT # !/usr/bin/env python3 -m pytest - +import os import pytest +from autogen import ConversableAgent, register_function from autogen.import_utils import optional_import_block, skip_on_missing_imports from autogen.oai.anthropic import AnthropicClient, _calculate_cost @@ -16,7 +17,7 @@ from anthropic.types import Message, TextBlock -from typing import List +from typing import List, TypedDict from pydantic import BaseModel from typing_extensions import Literal @@ -265,3 +266,86 @@ class MathReasoning(BaseModel): with pytest.raises(ValueError, match="No valid JSON found in response for Structured Output."): anthropic_client._extract_json_response(no_json_response) + + +@skip_on_missing_imports(["anthropic"], "anthropic") +def test_convert_tools_to_functions(anthropic_client): + + config_list = { + "model": "claude-3-5-sonnet-20241022", + "api_key": os.environ["ANTHROPIC_API_KEY"], + "api_type": "anthropic", + } + + expected_tools = \ + [ + { + "function": { + "description": "What is the temperature in NYC?", + "name": "weather_tool", + "parameters": { + "properties": { + "city_list": { + "$defs": { + "city_list_class": { + "properties": { + "item1": { + "title": "Item1", + "type": "string" + }, + "item2": { + "title": "Item2", + "type": "string" + } + }, + "required": [ + "item1", + "item2" + ], + "title": "city_list_class", + "type": "object" + } + }, + "description": "city_list", + "items": { + "$ref": "#/$defs/city_list_class" + }, + "type": "array" + }, + "city_name": { + "description": "city_name", + "type": "string" + } + }, + "required": [ + "city_name", + "city_list" + ], + "type": "object" + } + }, + "type": "function" + } + ] + + class city_list_class(TypedDict): + item1: str + item2: str + + def weather_tool(city_name: str, city_list: List[city_list_class]) -> str: + return "29" + + agent = ConversableAgent( + name="weather_agent", + system_message="You get the current temperature for a given city.", + llm_config=config_list, + ) + + register_function( + weather_tool, + caller=agent, + executor=agent, + description="What is the temperature in NYC?", + ) + + assert agent.llm_config["tools"] == expected_tools From 22acb813372442380111a4c8c30c897af3b6368f Mon Sep 17 00:00:00 2001 From: Maruf Aytekin Date: Mon, 24 Feb 2025 21:04:26 -0500 Subject: [PATCH 3/4] updated tests to reflect test coverage --- autogen/oai/anthropic.py | 1 - test/oai/test_anthropic.py | 84 ++------------------------------------ 2 files changed, 4 insertions(+), 81 deletions(-) diff --git a/autogen/oai/anthropic.py b/autogen/oai/anthropic.py index d64b11ee1d..c382b6e36e 100644 --- a/autogen/oai/anthropic.py +++ b/autogen/oai/anthropic.py @@ -389,7 +389,6 @@ def update_refs(obj, defs_keys, prop_name): elif isinstance(obj, list): for item in obj: update_refs(item, defs_keys, prop_name) - functions = [] for tool in tools: if tool.get("type") == "function" and "function" in tool: diff --git a/test/oai/test_anthropic.py b/test/oai/test_anthropic.py index be29e8ee33..17bed36a1c 100644 --- a/test/oai/test_anthropic.py +++ b/test/oai/test_anthropic.py @@ -9,7 +9,6 @@ import pytest -from autogen import ConversableAgent, register_function from autogen.import_utils import optional_import_block, skip_on_missing_imports from autogen.oai.anthropic import AnthropicClient, _calculate_cost @@ -270,82 +269,7 @@ class MathReasoning(BaseModel): @skip_on_missing_imports(["anthropic"], "anthropic") def test_convert_tools_to_functions(anthropic_client): - - config_list = { - "model": "claude-3-5-sonnet-20241022", - "api_key": os.environ["ANTHROPIC_API_KEY"], - "api_type": "anthropic", - } - - expected_tools = \ - [ - { - "function": { - "description": "What is the temperature in NYC?", - "name": "weather_tool", - "parameters": { - "properties": { - "city_list": { - "$defs": { - "city_list_class": { - "properties": { - "item1": { - "title": "Item1", - "type": "string" - }, - "item2": { - "title": "Item2", - "type": "string" - } - }, - "required": [ - "item1", - "item2" - ], - "title": "city_list_class", - "type": "object" - } - }, - "description": "city_list", - "items": { - "$ref": "#/$defs/city_list_class" - }, - "type": "array" - }, - "city_name": { - "description": "city_name", - "type": "string" - } - }, - "required": [ - "city_name", - "city_list" - ], - "type": "object" - } - }, - "type": "function" - } - ] - - class city_list_class(TypedDict): - item1: str - item2: str - - def weather_tool(city_name: str, city_list: List[city_list_class]) -> str: - return "29" - - agent = ConversableAgent( - name="weather_agent", - system_message="You get the current temperature for a given city.", - llm_config=config_list, - ) - - register_function( - weather_tool, - caller=agent, - executor=agent, - description="What is the temperature in NYC?", - ) - - assert agent.llm_config["tools"] == expected_tools + tools = [{"type": "function", "function": {"description": "weather tool", "name": "weather_tool", "parameters": {"type": "object", "properties": {"city_name": {"type": "string", "description": "city_name"}, "city_list": {"$defs": {"city_list_class": {"properties": {"item1": {"title": "Item1", "type": "string"}, "item2": {"title": "Item2", "type": "string"}}, "required": ["item1", "item2"], "title": "city_list_class", "type": "object"}}, "items": {"$ref": "#/$defs/city_list_class"}, "type": "array", "description": "city_list"}}, "required": ["city_name", "city_list"]}}}] + expected = [{"description": "weather tool", "name": "weather_tool", "parameters": {"type": "object", "properties": {"city_name": {"type": "string", "description": "city_name"}, "city_list": {"$defs": {"city_list_class": {"properties": {"item1": {"title": "Item1", "type": "string"}, "item2": {"title": "Item2", "type": "string"}}, "required": ["item1", "item2"], "title": "city_list_class", "type": "object"}}, "items": {"$ref": "#/properties/city_list/$defs/city_list_class"}, "type": "array", "description": "city_list"}}, "required": ["city_name", "city_list"]}}] + actual = anthropic_client.convert_tools_to_functions(tools=tools) + assert actual == expected From 4af0c8273ea928ceb8832145272b5fed769b85ed Mon Sep 17 00:00:00 2001 From: Maruf Aytekin Date: Tue, 25 Feb 2025 07:59:55 -0500 Subject: [PATCH 4/4] pre-commit checks --- .secrets.baseline | 6 ++-- autogen/oai/anthropic.py | 5 +-- test/oai/test_anthropic.py | 66 +++++++++++++++++++++++++++++++++++--- 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 56d87d44a1..2ac6954fab 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -911,7 +911,7 @@ "filename": "test/oai/test_anthropic.py", "hashed_secret": "9e712c7fd95990477f7c83991f86583883fa7260", "is_verified": false, - "line_number": 51, + "line_number": 50, "is_secret": false }, { @@ -919,7 +919,7 @@ "filename": "test/oai/test_anthropic.py", "hashed_secret": "1250ccf39e681decf5b51332888b7ccfc9a05227", "is_verified": false, - "line_number": 71, + "line_number": 70, "is_secret": false } ], @@ -1596,5 +1596,5 @@ } ] }, - "generated_at": "2025-02-24T18:10:39Z" + "generated_at": "2025-02-25T12:38:52Z" } diff --git a/autogen/oai/anthropic.py b/autogen/oai/anthropic.py index c382b6e36e..6c67711a8b 100644 --- a/autogen/oai/anthropic.py +++ b/autogen/oai/anthropic.py @@ -377,11 +377,11 @@ def convert_tools_to_functions(tools: list) -> list: """ def update_refs(obj, defs_keys, prop_name): - """Recursively update $ref values that start with "#/$defs/". """ + """Recursively update $ref values that start with "#/$defs/".""" if isinstance(obj, dict): for key, value in obj.items(): if key == "$ref" and isinstance(value, str) and value.startswith("#/$defs/"): - ref_key = value[len("#/$defs/"):] + ref_key = value[len("#/$defs/") :] if ref_key in defs_keys: obj[key] = f"#/properties/{prop_name}/$defs/{ref_key}" else: @@ -389,6 +389,7 @@ def update_refs(obj, defs_keys, prop_name): elif isinstance(obj, list): for item in obj: update_refs(item, defs_keys, prop_name) + functions = [] for tool in tools: if tool.get("type") == "function" and "function" in tool: diff --git a/test/oai/test_anthropic.py b/test/oai/test_anthropic.py index 17bed36a1c..f9e22fedd1 100644 --- a/test/oai/test_anthropic.py +++ b/test/oai/test_anthropic.py @@ -5,7 +5,6 @@ # Portions derived from https://github.com/microsoft/autogen are under the MIT License. # SPDX-License-Identifier: MIT # !/usr/bin/env python3 -m pytest -import os import pytest @@ -16,7 +15,7 @@ from anthropic.types import Message, TextBlock -from typing import List, TypedDict +from typing import List from pydantic import BaseModel from typing_extensions import Literal @@ -269,7 +268,66 @@ class MathReasoning(BaseModel): @skip_on_missing_imports(["anthropic"], "anthropic") def test_convert_tools_to_functions(anthropic_client): - tools = [{"type": "function", "function": {"description": "weather tool", "name": "weather_tool", "parameters": {"type": "object", "properties": {"city_name": {"type": "string", "description": "city_name"}, "city_list": {"$defs": {"city_list_class": {"properties": {"item1": {"title": "Item1", "type": "string"}, "item2": {"title": "Item2", "type": "string"}}, "required": ["item1", "item2"], "title": "city_list_class", "type": "object"}}, "items": {"$ref": "#/$defs/city_list_class"}, "type": "array", "description": "city_list"}}, "required": ["city_name", "city_list"]}}}] - expected = [{"description": "weather tool", "name": "weather_tool", "parameters": {"type": "object", "properties": {"city_name": {"type": "string", "description": "city_name"}, "city_list": {"$defs": {"city_list_class": {"properties": {"item1": {"title": "Item1", "type": "string"}, "item2": {"title": "Item2", "type": "string"}}, "required": ["item1", "item2"], "title": "city_list_class", "type": "object"}}, "items": {"$ref": "#/properties/city_list/$defs/city_list_class"}, "type": "array", "description": "city_list"}}, "required": ["city_name", "city_list"]}}] + tools = [ + { + "type": "function", + "function": { + "description": "weather tool", + "name": "weather_tool", + "parameters": { + "type": "object", + "properties": { + "city_name": {"type": "string", "description": "city_name"}, + "city_list": { + "$defs": { + "city_list_class": { + "properties": { + "item1": {"title": "Item1", "type": "string"}, + "item2": {"title": "Item2", "type": "string"}, + }, + "required": ["item1", "item2"], + "title": "city_list_class", + "type": "object", + } + }, + "items": {"$ref": "#/$defs/city_list_class"}, + "type": "array", + "description": "city_list", + }, + }, + "required": ["city_name", "city_list"], + }, + }, + } + ] + expected = [ + { + "description": "weather tool", + "name": "weather_tool", + "parameters": { + "type": "object", + "properties": { + "city_name": {"type": "string", "description": "city_name"}, + "city_list": { + "$defs": { + "city_list_class": { + "properties": { + "item1": {"title": "Item1", "type": "string"}, + "item2": {"title": "Item2", "type": "string"}, + }, + "required": ["item1", "item2"], + "title": "city_list_class", + "type": "object", + } + }, + "items": {"$ref": "#/properties/city_list/$defs/city_list_class"}, + "type": "array", + "description": "city_list", + }, + }, + "required": ["city_name", "city_list"], + }, + } + ] actual = anthropic_client.convert_tools_to_functions(tools=tools) assert actual == expected