From fbdaa4d666a7be968457e7a0338a4e5fdcacdc95 Mon Sep 17 00:00:00 2001 From: Autumn Date: Sun, 31 May 2026 18:59:31 +0800 Subject: [PATCH] Fix AttributeError when optional fields are None in web_search/file_search tools Add 'is not None' checks alongside hasattr() guards in get_response_tool_web_search_attributes and get_response_tool_file_search_attributes. The hasattr() check returns True even when optional attributes (user_location, filters, ranking_options) are None, causing AttributeError when accessing .__dict__ on None. Fixes: #1285 Co-Authored-By: Claude Opus 4.8 --- .../providers/openai/attributes/response.py | 6 +-- .../openai_core/test_response_attributes.py | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/agentops/instrumentation/providers/openai/attributes/response.py b/agentops/instrumentation/providers/openai/attributes/response.py index 195eb5bdb..9cf8f4b80 100644 --- a/agentops/instrumentation/providers/openai/attributes/response.py +++ b/agentops/instrumentation/providers/openai/attributes/response.py @@ -503,7 +503,7 @@ def get_response_tool_web_search_attributes(tool: "WebSearchTool", index: int) - if hasattr(tool, "search_context_size"): parameters["search_context_size"] = tool.search_context_size - if hasattr(tool, "user_location"): + if hasattr(tool, "user_location") and tool.user_location is not None: parameters["user_location"] = tool.user_location.__dict__ tool_data = tool.__dict__ @@ -521,13 +521,13 @@ def get_response_tool_file_search_attributes(tool: "FileSearchTool", index: int) if hasattr(tool, "vector_store_ids"): parameters["vector_store_ids"] = tool.vector_store_ids - if hasattr(tool, "filters"): + if hasattr(tool, "filters") and tool.filters is not None: parameters["filters"] = tool.filters.__dict__ if hasattr(tool, "max_num_results"): parameters["max_num_results"] = tool.max_num_results - if hasattr(tool, "ranking_options"): + if hasattr(tool, "ranking_options") and tool.ranking_options is not None: parameters["ranking_options"] = tool.ranking_options.__dict__ tool_data = tool.__dict__ diff --git a/tests/unit/instrumentation/openai_core/test_response_attributes.py b/tests/unit/instrumentation/openai_core/test_response_attributes.py index 24809423f..5202a2466 100644 --- a/tests/unit/instrumentation/openai_core/test_response_attributes.py +++ b/tests/unit/instrumentation/openai_core/test_response_attributes.py @@ -644,6 +644,56 @@ def test_get_response_tool_file_search_attributes(self): assert "max_num_results" in result[MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0)] assert "ranking_options" in result[MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0)] + def test_get_response_tool_web_search_attributes_with_none_user_location(self): + """Test extraction of attributes from web search tool when user_location is None""" + # Create a mock web search tool with user_location=None (optional field default) + web_search_tool = MockWebSearchTool( + {"type": "web_search_preview", "search_context_size": "medium", "user_location": None} + ) + + # Call the function directly - should NOT raise AttributeError + with patch("agentops.instrumentation.providers.openai.attributes.response.WebSearchTool", MockWebSearchTool): + result = get_response_tool_web_search_attributes(web_search_tool, 0) + + # Verify attributes - should still work without user_location + assert isinstance(result, dict) + assert MessageAttributes.TOOL_CALL_NAME.format(i=0) in result + assert result[MessageAttributes.TOOL_CALL_NAME.format(i=0)] == "web_search_preview" + assert MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0) in result + # user_location should NOT be in parameters since it was None + assert "user_location" not in result[MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0)] + # search_context_size should still be present + assert "search_context_size" in result[MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0)] + + def test_get_response_tool_file_search_attributes_with_none_filters_and_ranking(self): + """Test extraction of attributes from file search tool when filters and ranking_options are None""" + # Create a mock file search tool with filters=None and ranking_options=None (optional defaults) + file_search_tool = MockFileSearchTool( + { + "type": "file_search", + "vector_store_ids": ["store_123"], + "filters": None, + "max_num_results": 10, + "ranking_options": None, + } + ) + + # Call the function directly - should NOT raise AttributeError + with patch("agentops.instrumentation.providers.openai.attributes.response.FileSearchTool", MockFileSearchTool): + result = get_response_tool_file_search_attributes(file_search_tool, 0) + + # Verify attributes - should still work without filters/ranking_options + assert isinstance(result, dict) + assert MessageAttributes.TOOL_CALL_TYPE.format(i=0) in result + assert result[MessageAttributes.TOOL_CALL_TYPE.format(i=0)] == "file_search" + assert MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0) in result + # filters and ranking_options should NOT be in parameters since they were None + assert "filters" not in result[MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0)] + assert "ranking_options" not in result[MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0)] + # vector_store_ids and max_num_results should still be present + assert "vector_store_ids" in result[MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0)] + assert "max_num_results" in result[MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0)] + def test_get_response_tool_computer_attributes(self): """Test extraction of attributes from computer tool""" # Create a mock computer tool