diff --git a/proto/prediction.proto b/proto/prediction.proto index fdd5a8d24b..5a7536cc7b 100644 --- a/proto/prediction.proto +++ b/proto/prediction.proto @@ -19,6 +19,7 @@ message SeldonMessage { DefaultData data = 3; bytes binData = 4; string strData = 5; + google.protobuf.Value jsonData = 6; } } @@ -126,4 +127,4 @@ service Seldon { rpc SendFeedback(Feedback) returns (SeldonMessage) {}; } -// [END Services] \ No newline at end of file +// [END Services] diff --git a/python/seldon_core/proto/prediction.proto b/python/seldon_core/proto/prediction.proto index fdd5a8d24b..5a7536cc7b 100644 --- a/python/seldon_core/proto/prediction.proto +++ b/python/seldon_core/proto/prediction.proto @@ -19,6 +19,7 @@ message SeldonMessage { DefaultData data = 3; bytes binData = 4; string strData = 5; + google.protobuf.Value jsonData = 6; } } @@ -126,4 +127,4 @@ service Seldon { rpc SendFeedback(Feedback) returns (SeldonMessage) {}; } -// [END Services] \ No newline at end of file +// [END Services] diff --git a/python/seldon_core/proto/prediction_pb2.py b/python/seldon_core/proto/prediction_pb2.py index 567322877f..7554c22520 100644 --- a/python/seldon_core/proto/prediction_pb2.py +++ b/python/seldon_core/proto/prediction_pb2.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: proto/prediction.proto @@ -21,7 +22,7 @@ package='seldon.protos', syntax='proto3', serialized_options=_b('\n\020io.seldon.protosB\020PredictionProtosZ DefaultData: ... + @property + def jsonData(self) -> google___protobuf___struct_pb2___Value: ... + def __init__(self, status : typing___Optional[Status] = None, meta : typing___Optional[Meta] = None, data : typing___Optional[DefaultData] = None, binData : typing___Optional[bytes] = None, strData : typing___Optional[typing___Text] = None, + jsonData : typing___Optional[google___protobuf___struct_pb2___Value] = None, ) -> None: ... @classmethod def FromString(cls, s: bytes) -> SeldonMessage: ... def MergeFrom(self, other_msg: google___protobuf___message___Message) -> None: ... def CopyFrom(self, other_msg: google___protobuf___message___Message) -> None: ... if sys.version_info >= (3,): - def HasField(self, field_name: typing_extensions___Literal[u"binData",u"data",u"data_oneof",u"meta",u"status",u"strData"]) -> bool: ... - def ClearField(self, field_name: typing_extensions___Literal[u"binData",u"data",u"data_oneof",u"meta",u"status",u"strData"]) -> None: ... + def HasField(self, field_name: typing_extensions___Literal[u"binData",u"data",u"data_oneof",u"jsonData",u"meta",u"status",u"strData"]) -> bool: ... + def ClearField(self, field_name: typing_extensions___Literal[u"binData",u"data",u"data_oneof",u"jsonData",u"meta",u"status",u"strData"]) -> None: ... else: - def HasField(self, field_name: typing_extensions___Literal[u"binData",b"binData",u"data",b"data",u"data_oneof",b"data_oneof",u"meta",b"meta",u"status",b"status",u"strData",b"strData"]) -> bool: ... - def ClearField(self, field_name: typing_extensions___Literal[b"binData",b"data",b"data_oneof",b"meta",b"status",b"strData"]) -> None: ... - def WhichOneof(self, oneof_group: typing_extensions___Literal[u"data_oneof",b"data_oneof"]) -> typing_extensions___Literal["data","binData","strData"]: ... + def HasField(self, field_name: typing_extensions___Literal[u"binData",b"binData",u"data",b"data",u"data_oneof",b"data_oneof",u"jsonData",b"jsonData",u"meta",b"meta",u"status",b"status",u"strData",b"strData"]) -> bool: ... + def ClearField(self, field_name: typing_extensions___Literal[b"binData",b"data",b"data_oneof",b"jsonData",b"meta",b"status",b"strData"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions___Literal[u"data_oneof",b"data_oneof"]) -> typing_extensions___Literal["data","binData","strData","jsonData"]: ... class DefaultData(google___protobuf___message___Message): names = ... # type: google___protobuf___internal___containers___RepeatedScalarFieldContainer[typing___Text] diff --git a/python/seldon_core/utils.py b/python/seldon_core/utils.py index 2d67227eeb..4570feaf17 100644 --- a/python/seldon_core/utils.py +++ b/python/seldon_core/utils.py @@ -1,5 +1,6 @@ import json from google.protobuf import json_format +from google.protobuf.json_format import MessageToDict, ParseDict from seldon_core.proto import prediction_pb2 from seldon_core.flask_utils import SeldonMicroserviceException import numpy as np @@ -114,7 +115,7 @@ def feedback_to_json(message_proto: prediction_pb2.Feedback) -> Dict: return message_dict -def get_data_from_proto(request: prediction_pb2.SeldonMessage) -> Union[np.ndarray, str, bytes]: +def get_data_from_proto(request: prediction_pb2.SeldonMessage) -> Union[np.ndarray, str, bytes, dict]: """ Extract the data payload from the SeldonMessage @@ -136,6 +137,8 @@ def get_data_from_proto(request: prediction_pb2.SeldonMessage) -> Union[np.ndarr return request.binData elif data_type == "strData": return request.strData + elif data_type == "jsonData": + return MessageToDict(request.jsonData) else: raise SeldonMicroserviceException("Unknown data in SeldonMessage") @@ -300,7 +303,7 @@ def array_to_list_value(array: np.ndarray, lv: Optional[ListValue] = None) -> Li def construct_response(user_model: SeldonComponent, is_request: bool, client_request: prediction_pb2.SeldonMessage, - client_raw_response: Union[np.ndarray, str, bytes]) -> prediction_pb2.SeldonMessage: + client_raw_response: Union[np.ndarray, str, bytes, dict]) -> prediction_pb2.SeldonMessage: """ Parameters @@ -349,6 +352,9 @@ def construct_response(user_model: SeldonComponent, is_request: bool, client_req return prediction_pb2.SeldonMessage(data=data, meta=meta) elif isinstance(client_raw_response, str): return prediction_pb2.SeldonMessage(strData=client_raw_response, meta=meta) + elif isinstance(client_raw_response, dict): + jsonDataResponse = ParseDict(client_raw_response, prediction_pb2.SeldonMessage().jsonData) + return prediction_pb2.SeldonMessage(jsonData=jsonDataResponse, meta=meta) elif isinstance(client_raw_response, (bytes, bytearray)): return prediction_pb2.SeldonMessage(binData=client_raw_response, meta=meta) else: @@ -356,7 +362,7 @@ def construct_response(user_model: SeldonComponent, is_request: bool, client_req def extract_request_parts(request: prediction_pb2.SeldonMessage) -> Tuple[ - Union[np.ndarray, str, bytes], Dict, prediction_pb2.DefaultData, str]: + Union[np.ndarray, str, bytes, dict], Dict, prediction_pb2.DefaultData, str]: """ Parameters diff --git a/python/tests/test_combiner_microservice.py b/python/tests/test_combiner_microservice.py index 231df77d15..0b5577c0ed 100644 --- a/python/tests/test_combiner_microservice.py +++ b/python/tests/test_combiner_microservice.py @@ -111,8 +111,6 @@ def test_aggreate_invalid_message(): j = json.loads(rv.data) print(j) assert j["status"]["reason"] == "MICROSERVICE_BAD_DATA" - assert j["status"][ - "info"] == 'Invalid JSON: Message type "seldon.protos.SeldonMessageList" has no field named "wrong".\n Available Fields(except extensions): ' def test_aggreate_no_list(): @@ -124,8 +122,6 @@ def test_aggreate_no_list(): j = json.loads(rv.data) print(j) assert j["status"]["reason"] == "MICROSERVICE_BAD_DATA" - assert j["status"][ - "info"] == "Invalid JSON: Failed to parse seldonMessages field: repeated field seldonMessages must be in [] which is {'data': {'ndarray': [1]}}." def test_aggreate_bad_messages(): @@ -137,8 +133,6 @@ def test_aggreate_bad_messages(): j = json.loads(rv.data) print(j) assert j["status"]["reason"] == "MICROSERVICE_BAD_DATA" - assert j["status"][ - "info"] == 'Invalid JSON: Failed to parse seldonMessages field: Message type "seldon.protos.SeldonMessage" has no field named "data2".\n Available Fields(except extensions): ' def test_aggreate_ok_2messages(): diff --git a/python/tests/test_utils.py b/python/tests/test_utils.py index 778f30f1ba..cdb05e2fea 100644 --- a/python/tests/test_utils.py +++ b/python/tests/test_utils.py @@ -7,6 +7,7 @@ from seldon_core.proto import prediction_pb2 from seldon_core.flask_utils import SeldonMicroserviceException import seldon_core.utils as scu +from google.protobuf.struct_pb2 import Value class UserObject(object): @@ -90,6 +91,18 @@ def test_create_response_strdata(): assert len(sm.strData) > 0 +def test_create_response_jsondata(): + user_model = UserObject() + request_data = np.array([[5, 6, 7]]) + datadef = scu.array_to_grpc_datadef("ndarray", request_data) + request = prediction_pb2.SeldonMessage(data=datadef) + raw_response = {"output": "data"} + sm = scu.construct_response(user_model, True, request, raw_response) + assert sm.data.WhichOneof("data_oneof") == None + emptyValue = Value() + assert sm.jsonData != emptyValue + + def test_create_reponse_list(): user_model = UserObject() request_data = np.array([[5, 6, 7]]) @@ -161,6 +174,16 @@ def test_json_to_seldon_message_str_data(): assert arr == "my string data" +def test_json_to_seldon_message_json_data(): + data = {"jsonData": {"some": "value"}} + requestProto = scu.json_to_seldon_message(data) + assert len(requestProto.data.tensor.values) == 0 + assert requestProto.WhichOneof("data_oneof") == "jsonData" + (json_data, meta, datadef, _) = scu.extract_request_parts(requestProto) + assert not isinstance(json_data, np.ndarray) + assert json_data == {"some": "value"} + + def test_json_to_seldon_message_bad_data(): with pytest.raises(SeldonMicroserviceException): data = {"foo": "bar"} diff --git a/util/loadtester/scripts/proto/prediction.proto b/util/loadtester/scripts/proto/prediction.proto index a504509558..5a7536cc7b 100644 --- a/util/loadtester/scripts/proto/prediction.proto +++ b/util/loadtester/scripts/proto/prediction.proto @@ -1,11 +1,13 @@ syntax = "proto3"; import "google/protobuf/struct.proto"; +import "tensorflow/core/framework/tensor.proto"; package seldon.protos; option java_package = "io.seldon.protos"; option java_outer_classname = "PredictionProtos"; +option go_package = "github.com/seldonio/seldon-core/examples/wrappers/go/pkg/api"; // [START Messages] @@ -17,6 +19,7 @@ message SeldonMessage { DefaultData data = 3; bytes binData = 4; string strData = 5; + google.protobuf.Value jsonData = 6; } } @@ -25,6 +28,7 @@ message DefaultData { oneof data_oneof { Tensor tensor = 2; google.protobuf.ListValue ndarray = 3; + tensorflow.TensorProto tftensor = 4; } } @@ -50,6 +54,7 @@ message Metric { string key = 1; MetricType type = 2; float value = 3; + map tags = 4; } message SeldonMessageList { @@ -122,4 +127,4 @@ service Seldon { rpc SendFeedback(Feedback) returns (SeldonMessage) {}; } -// [END Services] \ No newline at end of file +// [END Services]