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/providers/openai/test_response_issue_1285.py b/tests/unit/instrumentation/providers/openai/test_response_issue_1285.py new file mode 100644 index 000000000..b4b514649 --- /dev/null +++ b/tests/unit/instrumentation/providers/openai/test_response_issue_1285.py @@ -0,0 +1,94 @@ +""" +Regression test for issue #1285: +'NoneType' object has no attribute '__dict__' in get_response_tool_web/file_search_attributes + +When OpenAI Agents SDK tools have optional fields (user_location, filters, ranking_options) +set to None, hasattr() returns True but .__dict__ access crashes. +""" + +import pytest +from unittest.mock import MagicMock + +from agentops.instrumentation.providers.openai.attributes.response import ( + get_response_tool_web_search_attributes, + get_response_tool_file_search_attributes, +) + + +class MockUserLocation: + """Mock user_location object with a __dict__ attribute.""" + def __init__(self): + self.type = "approximate" + self.country = "US" + + +class MockRankingOptions: + """Mock ranking_options object with a __dict__ attribute.""" + def __init__(self): + self.score_threshold = 0.5 + + +class MockFilters: + """Mock filters object with a __dict__ attribute.""" + def __init__(self): + self.type = "and" + self.filters = [] + + +class TestIssue1285: + """Test that None optional fields don't crash attribute extraction.""" + + def test_web_search_tool_with_none_user_location(self): + """user_location=None should not cause AttributeError.""" + tool = MagicMock() + tool.search_context_size = 1024 + tool.user_location = None # This is the bug: hasattr returns True, but .__dict__ crashes + + # Should not raise AttributeError + result = get_response_tool_web_search_attributes(tool, index=0) + assert isinstance(result, dict) + + def test_web_search_tool_with_valid_user_location(self): + """user_location with a real object should still work.""" + tool = MagicMock() + tool.search_context_size = 1024 + tool.user_location = MockUserLocation() + tool.user_location.__dict__ = {"type": "approximate", "country": "US"} + + result = get_response_tool_web_search_attributes(tool, index=0) + assert isinstance(result, dict) + + def test_file_search_tool_with_none_filters(self): + """filters=None should not cause AttributeError.""" + tool = MagicMock() + tool.vector_store_ids = ["vs_123"] + tool.filters = None # Bug: hasattr returns True, .__dict__ crashes + tool.max_num_results = 5 + tool.ranking_options = None # Bug: same issue + + result = get_response_tool_file_search_attributes(tool, index=0) + assert isinstance(result, dict) + + def test_file_search_tool_with_none_ranking_options(self): + """ranking_options=None should not cause AttributeError.""" + tool = MagicMock() + tool.vector_store_ids = ["vs_123"] + tool.filters = MockFilters() + tool.max_num_results = 5 + tool.ranking_options = None + + result = get_response_tool_file_search_attributes(tool, index=0) + assert isinstance(result, dict) + + def test_file_search_tool_with_valid_fields(self): + """All valid fields should still work correctly.""" + tool = MagicMock() + tool.vector_store_ids = ["vs_123"] + tool.filters = MockFilters() + tool.filters.__dict__ = {"type": "and", "filters": []} + tool.max_num_results = 5 + tool.ranking_options = MockRankingOptions() + tool.ranking_options.__dict__ = {"score_threshold": 0.5} + + result = get_response_tool_file_search_attributes(tool, index=0) + assert isinstance(result, dict)