diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 7fa5155b951..9676d6d3788 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -304,11 +304,18 @@ def __repr__(self) -> str: def _get_tool_name(tool: Any) -> str | None: """Extract a tool name from a tool object or dict tool definition.""" if isinstance(tool, Mapping): + if tool.get("type") == "function": # type: ignore + name = tool.get("name") # type: ignore + if isinstance(name, str): + return name + func = tool.get("function", None) # type: ignore if func and isinstance(func, Mapping): name = func.get("name") # type: ignore return name if isinstance(name, str) else None + return None + name = getattr(tool, "name", None) return name if isinstance(name, str) else None diff --git a/python/packages/core/tests/core/test_middleware.py b/python/packages/core/tests/core/test_middleware.py index 314860811ca..ab473306b5f 100644 --- a/python/packages/core/tests/core/test_middleware.py +++ b/python/packages/core/tests/core/test_middleware.py @@ -30,7 +30,7 @@ MiddlewareTermination, categorize_middleware, ) -from agent_framework._tools import FunctionTool +from agent_framework._tools import FunctionTool, ToolTypes class TestAgentContext: @@ -111,6 +111,84 @@ def test_init_with_custom_metadata(self, mock_function: FunctionTool) -> None: assert context.metadata == metadata +def test_remove_flat_function_mapping_by_name() -> None: + """Flat Responses-style function mappings can be removed by name.""" + lookup = { + "type": "function", + "name": "lookup", + "parameters": {"type": "object"}, + } + tools: list[ToolTypes] = [lookup] + + context = FunctionInvocationContext( + function=FunctionTool( + name="loader", + description="Load tools", + func=lambda: None, + ), + arguments={}, + tools=tools, + ) + + context.remove_tools("lookup") + + assert tools == [] + + +def test_add_rejects_duplicate_flat_function_mapping() -> None: + """Flat function mappings participate in duplicate-name checks.""" + lookup = { + "type": "function", + "name": "lookup", + "parameters": {"type": "object"}, + } + duplicate = { + "type": "function", + "name": "lookup", + "parameters": { + "type": "object", + "properties": {"query": {"type": "string"}}, + }, + } + tools: list[ToolTypes] = [lookup] + + context = FunctionInvocationContext( + function=FunctionTool( + name="loader", + description="Load tools", + func=lambda: None, + ), + arguments={}, + tools=tools, + ) + + with pytest.raises(ValueError, match="lookup"): + context.add_tools(duplicate) + + +def test_remove_name_preserves_non_function_builtin_mapping() -> None: + """A named non-function built-in mapping is not treated as a function.""" + builtin_tool = { + "type": "web_search_preview", + "name": "lookup", + } + tools: list[ToolTypes] = [builtin_tool] + + context = FunctionInvocationContext( + function=FunctionTool( + name="loader", + description="Load tools", + func=lambda: None, + ), + arguments={}, + tools=tools, + ) + + context.remove_tools("lookup") + + assert tools == [builtin_tool] + + class TestChatContext: """Test cases for ChatContext."""