diff --git a/agentops/instrumentation/providers/openai/attributes/response.py b/agentops/instrumentation/providers/openai/attributes/response.py index 195eb5bdb..5794e2a27 100644 --- a/agentops/instrumentation/providers/openai/attributes/response.py +++ b/agentops/instrumentation/providers/openai/attributes/response.py @@ -500,10 +500,10 @@ def get_response_tools_attributes(tools: List["ToolTypes"]) -> AttributeMap: def get_response_tool_web_search_attributes(tool: "WebSearchTool", index: int) -> AttributeMap: """Handles interpretation of an openai WebSearchTool object.""" parameters = {} - if hasattr(tool, "search_context_size"): + if hasattr(tool, "search_context_size") and tool.search_context_size is not None: 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__ @@ -518,16 +518,16 @@ def get_response_tool_file_search_attributes(tool: "FileSearchTool", index: int) """Handles interpretation of an openai FileSearchTool object.""" parameters = {} - if hasattr(tool, "vector_store_ids"): + if hasattr(tool, "vector_store_ids") and tool.vector_store_ids is not None: 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"): + if hasattr(tool, "max_num_results") and tool.max_num_results is not None: 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..23aabe38d 100644 --- a/tests/unit/instrumentation/openai_core/test_response_attributes.py +++ b/tests/unit/instrumentation/openai_core/test_response_attributes.py @@ -644,6 +644,80 @@ 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_search_attributes_skip_none_optional_parameters(self): + """Test extraction skips optional search parameters when values are None""" + web_search_tool = MockWebSearchTool( + {"type": "web_search_preview", "search_context_size": None, "user_location": None} + ) + file_search_tool = MockFileSearchTool( + { + "type": "file_search", + "vector_store_ids": None, + "filters": None, + "max_num_results": None, + "ranking_options": None, + } + ) + + with patch("agentops.instrumentation.providers.openai.attributes.response.WebSearchTool", MockWebSearchTool): + web_result = get_response_tool_web_search_attributes(web_search_tool, 0) + + with patch("agentops.instrumentation.providers.openai.attributes.response.FileSearchTool", MockFileSearchTool): + file_result = get_response_tool_file_search_attributes(file_search_tool, 0) + + assert MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0) not in web_result + assert MessageAttributes.TOOL_CALL_ARGUMENTS.format(i=0) not in file_result + def test_get_response_tool_computer_attributes(self): """Test extraction of attributes from computer tool""" # Create a mock computer tool