Repository: kserve/kserve

Issue:
Proposing a new explainer framework
/kind feature

**Describe the solution you'd like**
Hi everyone, we want to propose a new explainer framework called [AIX360](https://github.com/Trusted-AI/AIX360). In our first version, we will focus on consolidating the v1alpha2 and v1beta1 API on the AIX360 Lime integration. We will add more explainer such as SHAP to our explainer Server.

For more details, visit our design doc at http://bit.ly/kubeflow-trusted-ai




Discussion:
Issue-Label Bot is automatically applying the labels:

| Label  | Probability |
| ------------- | ------------- |
| area/engprod | 0.72 |

Please mark this comment with :thumbsup: or :thumbsdown: to give our bot feedback! 
Links: [app homepage](https://github.com/marketplace/issue-label-bot), [dashboard](https://label-bot-prod.mlbot.net/data/kubeflow/kfserving) and [code](https://github.com/hamelsmu/MLapp) for this bot.

Resolving patch:
diff --git a/docs/samples/explanation/aix/mnist/query_explain.py b/docs/samples/explanation/aix/mnist/query_explain.py
--- a/docs/samples/explanation/aix/mnist/query_explain.py
+++ b/docs/samples/explanation/aix/mnist/query_explain.py
@@ -2,6 +2,7 @@
 import sys
 import requests
 import json
+import math
 from matplotlib import pyplot as plt
 import numpy as np
 from aix360.datasets import MNISTDataset
@@ -22,14 +23,20 @@
 headers = {
     'Host': sys.argv[2]
 }
+parameters = {}
 test_num = 1002
 is_file = False
 if len(sys.argv) > 3:
     try:
-        test_num = int(sys.argv[2])
+        test_num = int(sys.argv[3])
     except:
         is_file = True
 
+    if len(sys.argv) > 4:
+        try:
+            parameters = json.loads(sys.argv[4]) 
+        except:
+            raise Exception("Failed to convert to json format. ")
 if is_file:
     inputs = open(sys.argv[2])
     inputs = json.load(inputs)
@@ -45,6 +52,7 @@
     inputs = gray2rgb(inputs.reshape((-1, 28, 28)))
     inputs = np.reshape(inputs, (28,28,3))
 input_image = {"instances": [inputs.tolist()]}
+input_image.update(parameters)
 print("Sending Explain Query")
 
 x = time.time()
@@ -61,8 +69,11 @@
 masks = np.array(res_json["explanations"]["masks"])
 top_labels = np.array(res_json["explanations"]["top_labels"])
 
-fig, m_axs = plt.subplots(2,5, figsize = (12,6))
+fig, m_axs = plt.subplots(math.ceil(len(top_labels)/5),5, figsize = (12,6))
 for i, c_ax in enumerate(m_axs.flatten()):
+    if i >= len(top_labels):
+        c_ax.axis('off')
+        continue
     mask = masks[i]
     c_ax.imshow(label2rgb(mask, temp, bg_label = 0), interpolation = 'nearest')
     c_ax.set_title('Positive for {}\nActual {}'.format(top_labels[i], actual))
diff --git a/python/aixexplainer/aixserver/model.py b/python/aixexplainer/aixserver/model.py
--- a/python/aixexplainer/aixserver/model.py
+++ b/python/aixexplainer/aixserver/model.py
@@ -50,6 +50,25 @@ def _predict(self, input_im):
 
     def explain(self, request: Dict) -> Dict:
         instances = request["instances"]
+        try:
+            top_labels = (int(request["top_labels"]) 
+                          if "top_labels" in request else 
+                          self.top_labels)
+            segmentation_alg = (request["segmentation_alg"] 
+                                if "segmentation_alg" in request else 
+                                self.segmentation_alg)
+            num_samples = (int(request["num_samples"]) 
+                           if "num_samples" in request else 
+                           self.num_samples)
+            positive_only = ((request["positive_only"].lower() == "true") | (request["positive_only"].lower() == "t") 
+                             if "positive_only" in request else 
+                             self.positive_only)
+            min_weight = (float(request['min_weight']) 
+                          if "min_weight" in request else 
+                          self.min_weight)
+        except Exception as err:
+            raise Exception("Failed to specify parameters: %s", (err,))
+
         try:
             inputs = np.array(instances[0])
             logging.info("Calling explain on image of shape %s", (inputs.shape,))
@@ -59,23 +78,23 @@ def explain(self, request: Dict) -> Dict:
         try:
             if str.lower(self.explainer_type) == "limeimages":
                 explainer = LimeImageExplainer(verbose=False)
-                segmenter = SegmentationAlgorithm(self.segmentation_alg, kernel_size=1,
+                segmenter = SegmentationAlgorithm(segmentation_alg, kernel_size=1,
                                                   max_dist=200, ratio=0.2)
                 explanation = explainer.explain_instance(inputs,
                                                          classifier_fn=self._predict,
-                                                         top_labels=self.top_labels,
+                                                         top_labels=top_labels,
                                                          hide_color=0,
-                                                         num_samples=self.num_samples,
+                                                         num_samples=num_samples,
                                                          segmentation_fn=segmenter)
 
                 temp = []
                 masks = []
-                for i in range(0, self.top_labels):
+                for i in range(0, top_labels):
                     temp, mask = explanation.get_image_and_mask(explanation.top_labels[i],
-                                                                positive_only=self.positive_only,
+                                                                positive_only=positive_only,
                                                                 num_features=10,
                                                                 hide_rest=False,
-                                                                min_weight=self.min_weight)
+                                                                min_weight=min_weight)
                     masks.append(mask.tolist())
 
                 return {"explanations": {
diff --git a/python/kfserving/kfserving/storage.py b/python/kfserving/kfserving/storage.py
--- a/python/kfserving/kfserving/storage.py
+++ b/python/kfserving/kfserving/storage.py
@@ -18,6 +18,7 @@
 import mimetypes
 import os
 import re
+import json
 import shutil
 import tarfile
 import zipfile
@@ -36,6 +37,7 @@
 _LOCAL_PREFIX = "file://"
 _URI_RE = "https?://(.+)/(.+)"
 _HTTP_PREFIX = "http(s)://"
+_HEADERS_SUFFIX = "-headers"
 
 class Storage(object): # pylint: disable=too-few-public-methods
     @staticmethod
@@ -232,12 +234,21 @@ def _download_from_uri(uri, out_dir=None):
         if filename == '':
             raise ValueError('No filename contained in URI: %s' % (uri))
 
-        with requests.get(uri, stream=True) as response:
+        # Get header information from host url
+        headers = {}
+        host_uri = url.hostname
+
+        headers_json = os.getenv(host_uri + _HEADERS_SUFFIX, "{}")
+        headers = json.loads(headers_json)
+
+        with requests.get(uri, stream=True, headers=headers) as response:
             if response.status_code != 200:
                 raise RuntimeError("URI: %s returned a %s response code." % (uri, response.status_code))
             if mimetype == 'application/zip' and not response.headers.get('Content-Type', '').startswith('application/zip'):
                 raise RuntimeError("URI: %s did not respond with \'Content-Type\': \'application/zip\'" % (uri))
-            if mimetype != 'application/zip' and not response.headers.get('Content-Type', '').startswith('application/octet-stream'):
+            if mimetype == 'application/x-tar' and not response.headers.get('Content-Type', '').startswith('application/x-tar'):
+                raise RuntimeError("URI: %s did not respond with \'Content-Type\': \'application/x-tar\'" % (uri))
+            if (mimetype != 'application/zip' and mimetype != 'application/x-tar') and not response.headers.get('Content-Type', '').startswith('application/octet-stream'):
                 raise RuntimeError("URI: %s did not respond with \'Content-Type\': \'application/octet-stream\'" % (uri))
 
             if encoding == 'gzip':


Test patch:
diff --git a/pkg/credentials/https/https_secret_test.go b/pkg/credentials/https/https_secret_test.go
new file mode 100644
--- /dev/null
+++ b/pkg/credentials/https/https_secret_test.go
@@ -0,0 +1,81 @@
+/*
+Copyright 2021 kubeflow.org.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package https
+
+import (
+	"github.com/google/go-cmp/cmp"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"testing"
+)
+
+var (
+	header1      = "username"
+	header2      = "password"
+	headerValue1 = "someUsername"
+	headerValue2 = "somePassword"
+	uriHost      = "example.com"
+)
+
+func TestHTTPSSecret(t *testing.T) {
+	scenarios := map[string]struct {
+		secret   *v1.Secret
+		expected []v1.EnvVar
+	}{
+		"noUriHost": {
+			secret: &v1.Secret{
+				Data: map[string][]byte{
+					header1: []byte(headerValue1),
+					header2: []byte(headerValue2),
+				},
+			},
+			expected: []v1.EnvVar{},
+		},
+		"noHeaders": {
+			secret: &v1.Secret{
+				ObjectMeta: metav1.ObjectMeta{},
+				Data: map[string][]byte{
+					HTTPSHost: []byte(uriHost),
+				},
+			},
+			expected: []v1.EnvVar{},
+		},
+		"secretEnvs": {
+			secret: &v1.Secret{
+				ObjectMeta: metav1.ObjectMeta{},
+				Data: map[string][]byte{
+					HTTPSHost: []byte(uriHost),
+					HEADERS:   []byte(`{` + NEWLINE + header1 + ColonSeparator + headerValue1 + NEWLINE + header2 + ColonSeparator + headerValue2 + NEWLINE + `}`),
+				},
+			},
+			expected: []v1.EnvVar{
+				{
+					Name:  uriHost + HeadersSuffix,
+					Value: `{` + NEWLINE + header1 + ColonSeparator + headerValue1 + NEWLINE + header2 + ColonSeparator + headerValue2 + NEWLINE + `}`,
+				},
+			},
+		},
+	}
+
+	for name, scenario := range scenarios {
+		envs := BuildSecretEnvs(scenario.secret)
+
+		if diff := cmp.Diff(scenario.expected, envs); diff != "" {
+			t.Errorf("Test %q unexpected result (-want +got): %v", name, diff)
+		}
+	}
+}