Skip to content

Commit

Permalink
Adding UPI protocol support in standard transformer simulator (#304)
Browse files Browse the repository at this point in the history
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. Run unit tests and ensure that they are passing
2. If your change introduces any API changes, make sure to update the
e2e tests
3. Make sure documentation is updated for your PR!

-->

**What this PR does / why we need it**:
<!-- Explain here the context and why you're making the change. What is
the problem you're trying to solve. --->
Standard transformer simulator currently only works for HTTP_JSON
protocol, this PR is adding support for UPI_V1 protocol also.

**Modification**
* Request conversion to upi PredictValueRequest and update echo mock
model predictor for UPI_V1 protocol
* Update ui output of tracing
* Adding button to populate sample request and model predictor payload 
* Adding documentation button
**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->

Fixes #

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required. Enter your extended release note in
the block below.
If the PR requires additional action from users switching to the new
release, include the string "action required".

For more information about release notes, see kubernetes' guide here:
http://git.k8s.io/community/contributors/guide/release-notes.md
-->


https://user-images.githubusercontent.com/2369255/194526214-2b7c40ec-9c30-44a1-8903-53fed95a3374.mov




```release-note

```

**Checklist**

- [ ] Added unit test, integration, and/or e2e tests
- [x] Tested locally
- [ ] Updated documentation
- [ ] Update Swagger spec if the PR introduce API changes
- [ ] Regenerated Golang and Python client if the PR introduce API
changes
  • Loading branch information
tiopramayudi authored Oct 10, 2022
1 parent e4a277d commit d13c9e1
Show file tree
Hide file tree
Showing 28 changed files with 732 additions and 119 deletions.
4 changes: 4 additions & 0 deletions api/api/transformer_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/gojek/merlin/log"
"github.com/gojek/merlin/models"
"github.com/gojek/merlin/pkg/protocol"
)

// TransformerController
Expand All @@ -22,6 +23,9 @@ func (c *TransformerController) SimulateTransformer(r *http.Request, vars map[st
return BadRequest("Unable to parse request body")
}

if simulationPayload.Protocol != protocol.HttpJson && simulationPayload.Protocol != protocol.UpiV1 {
return BadRequest(`The only supported protocol are "HTTP_JSON" and "UPI_V1"`)
}
transformerResult, err := c.TransformerService.SimulateTransformer(ctx, simulationPayload)
if err != nil {
log.Errorf("Failed performing transfomer simulation %v", err)
Expand Down
66 changes: 66 additions & 0 deletions api/api/transformer_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/gojek/merlin/config"
"github.com/gojek/merlin/models"
"github.com/gojek/merlin/pkg/protocol"
"github.com/gojek/merlin/pkg/transformer/spec"
"github.com/gojek/merlin/pkg/transformer/types"
"github.com/gojek/merlin/service/mocks"
Expand Down Expand Up @@ -66,6 +67,7 @@ func TestTransformerController_SimulateTransformer(t *testing.T) {
},
},
},
Protocol: protocol.HttpJson,
PredictionConfig: &models.ModelPredictionConfig{
Mock: &models.MockResponse{
Body: types.JSONObject{
Expand Down Expand Up @@ -173,6 +175,69 @@ func TestTransformerController_SimulateTransformer(t *testing.T) {
},
},
},
{
desc: "protocol is not set",
requestBody: &models.TransformerSimulation{
Payload: types.JSONObject{
"driver_id": 2,
"service_type": 1,
},
Headers: map[string]string{
"Country-ID": "ID",
},
Config: &spec.StandardTransformerConfig{
TransformerConfig: &spec.TransformerConfig{
Preprocess: &spec.Pipeline{
Inputs: []*spec.Input{
{
Variables: []*spec.Variable{
{
Name: "driver_id",
Value: &spec.Variable_JsonPath{
JsonPath: "$.driver_id",
},
},
},
},
},
Outputs: []*spec.Output{
{
JsonOutput: &spec.JsonOutput{
JsonTemplate: &spec.JsonTemplate{
Fields: []*spec.Field{
{
FieldName: "id",
Value: &spec.Field_Expression{
Expression: "driver_id",
},
},
},
},
},
},
},
},
},
},
PredictionConfig: &models.ModelPredictionConfig{
Mock: &models.MockResponse{
Body: types.JSONObject{
"prediction": []float64{0.2, 0.4},
},
},
},
},
transformerService: func(payload *models.TransformerSimulation) *mocks.TransformerService {
mockSvc := &mocks.TransformerService{}
return mockSvc
},
want: &Response{
code: http.StatusBadRequest,
data: Error{
Message: `The only supported protocol are "HTTP_JSON" and "UPI_V1"`,
},
},
},
{
desc: "valid request body - service returning error",
requestBody: &models.TransformerSimulation{
Expand All @@ -183,6 +248,7 @@ func TestTransformerController_SimulateTransformer(t *testing.T) {
Headers: map[string]string{
"Country-ID": "ID",
},
Protocol: protocol.HttpJson,
Config: &spec.StandardTransformerConfig{
TransformerConfig: &spec.TransformerConfig{
Preprocess: &spec.Pipeline{
Expand Down
1 change: 1 addition & 0 deletions api/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type ReactAppConfig struct {
MlpURL string `envconfig:"REACT_APP_MLP_API" json:"REACT_APP_MLP_API,omitempty"`
OauthClientID string `envconfig:"REACT_APP_OAUTH_CLIENT_ID" json:"REACT_APP_OAUTH_CLIENT_ID,omitempty"`
SentryDSN string `envconfig:"REACT_APP_SENTRY_DSN" json:"REACT_APP_SENTRY_DSN,omitempty"`
UPIDocumentation string `envconfig:"REACT_APP_UPI_DOC_URL" json:"REACT_APP_UPI_DOC_URL,omitempty"`
}

type Documentations []Documentation
Expand Down
2 changes: 2 additions & 0 deletions api/models/transformer.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package models

import (
"github.com/gojek/merlin/pkg/protocol"
"github.com/gojek/merlin/pkg/transformer/spec"
"github.com/gojek/merlin/pkg/transformer/types"
"github.com/google/uuid"
Expand Down Expand Up @@ -51,6 +52,7 @@ type TransformerSimulation struct {
Headers map[string]string `json:"headers"`
Config *spec.StandardTransformerConfig `json:"config"`
PredictionConfig *ModelPredictionConfig `json:"model_prediction_config"`
Protocol protocol.Protocol `json:"protocol"`
}

// ModelPredictionConfig
Expand Down
13 changes: 3 additions & 10 deletions api/pkg/transformer/executor/mocks/transformer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 65 additions & 4 deletions api/pkg/transformer/executor/predictor.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,86 @@ package executor

import (
"context"
"encoding/json"
"fmt"

upiv1 "github.com/caraml-dev/universal-prediction-interface/gen/go/grpc/caraml/upi/v1"
prt "github.com/gojek/merlin/pkg/protocol"
"github.com/gojek/merlin/pkg/transformer/types"
"google.golang.org/protobuf/encoding/protojson"
)

// ModelPredictor
// ModelPredictor interface that handle model prediction operation
type ModelPredictor interface {
ModelPrediction(ctx context.Context, requestBody types.Payload, requestHeader map[string]string) (respBody types.Payload, respHeaders map[string]string, err error)
}

type mockModelPredictor struct {
mockResponseBody types.Payload
mockResponseHeader map[string]string
// responseConverterFunc is a function to convert incoming request to response that standard transformer accepts
responseConverterFunc func(payload types.Payload) (types.Payload, error)
}

func newEchoMockPredictor() *mockModelPredictor {
return &mockModelPredictor{}
}

func NewMockModelPredictor(respBody types.Payload, respHeader map[string]string) *mockModelPredictor {
// NewMockModelPredictor create mock model predictor given the payload header and protocol
func NewMockModelPredictor(respBody types.Payload, respHeader map[string]string, protocol prt.Protocol) *mockModelPredictor {
var converterFn func(types.Payload) (types.Payload, error)
if protocol == prt.UpiV1 {
converterFn = upiResponseConverter
} else {
converterFn = restResponseConverter
}

return &mockModelPredictor{
mockResponseBody: respBody,
mockResponseHeader: respHeader,
mockResponseBody: respBody,
mockResponseHeader: respHeader,
responseConverterFunc: converterFn,
}
}

func restResponseConverter(payload types.Payload) (types.Payload, error) {
return payload, nil
}

func upiResponseConverter(payload types.Payload) (types.Payload, error) {
switch payloadT := payload.OriginalValue().(type) {
case types.JSONObject:
byteData, err := json.Marshal(payloadT)
if err != nil {
return nil, err
}
var resp upiv1.PredictValuesResponse
if err := protojson.Unmarshal(byteData, &resp); err != nil {
return nil, err
}
return (*types.UPIPredictionResponse)(&resp), nil
case *upiv1.PredictValuesRequest:
resp := &upiv1.PredictValuesResponse{}
resp.TargetName = payloadT.TargetName
resp.PredictionResultTable = payloadT.PredictionTable
resp.PredictionContext = payloadT.PredictionContext
if payloadT.Metadata != nil {
respMetadata := &upiv1.ResponseMetadata{}
respMetadata.PredictionId = payloadT.Metadata.PredictionId
resp.Metadata = respMetadata
}
return (*types.UPIPredictionResponse)(resp), nil
case *upiv1.PredictValuesResponse:
return payload, nil
default:
return nil, fmt.Errorf("unknown type of payload %T", payloadT)
}
}

var _ ModelPredictor = (*mockModelPredictor)(nil)

// ModelPrediction return mock of model prediction
// for `UPI_V1“ protocol if the mock model prediction is not given it will create new UPI response interface and assign `prediction_result_table` with value of `prediction_table` field from request payload
// for `HTTP_JSON` protocol if the mock model prediction is not given it will return the request payload instead
func (mock *mockModelPredictor) ModelPrediction(ctx context.Context, requestBody types.Payload, requestHeader map[string]string) (respBody types.Payload, respHeaders map[string]string, err error) {
reqBodyObj, err := requestBody.AsInput()
if err != nil {
Expand All @@ -40,6 +93,14 @@ func (mock *mockModelPredictor) ModelPrediction(ctx context.Context, requestBody
respBody = mock.mockResponseBody
}

if mock.responseConverterFunc != nil {
conversionRes, err := mock.responseConverterFunc(respBody)
if err != nil {
return nil, nil, err
}
respBody = conversionRes
}

respHeaders = requestHeader
if mock.mockResponseHeader != nil {
respHeaders = mock.mockResponseHeader
Expand Down
Loading

0 comments on commit d13c9e1

Please sign in to comment.