TP-51771 : Created Maverick Client (#333)

* TP-51771| created maverick client

* TP-51771| added check for empty data response form maverick

* made maverick client generic
This commit is contained in:
Gullipalli Chetan Kumar
2023-12-20 15:12:39 +05:30
committed by GitHub
parent 1750ac3c18
commit f31c75a1fb
10 changed files with 264 additions and 124 deletions

View File

@@ -59,3 +59,4 @@ generatemocks:
cd $(CURDIR)/pkg/monitoringService && minimock -i MonitoringServiceActions -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/service/krakatoa && minimock -i IKrakatoaService -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/pkg/socketModeClient && minimock -i ISocketModeClientWrapper -s _mock.go -o $(CURDIR)/mocks
cd $(CURDIR)/pkg/maverick && minimock -i IMaverickClient -s _mock.go -o $(CURDIR)/mocks

View File

@@ -136,7 +136,7 @@ func GetDocumentService() *documentService.ActionsImpl {
}
func initRCAService() *rca.RcaService {
rcaService := rca.NewRcaService(initIncidentService(), initSlackService(), getRestClient(), initDocumentService(), initRCARepo(), initRCAInputRepo(), initUserRepo())
rcaService := rca.NewRcaService(initIncidentService(), initSlackService(), initDocumentService(), initRCARepo(), initRCAInputRepo(), initUserRepo())
return rcaService
}

View File

@@ -62,7 +62,7 @@ func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) *
rcaInputRepository := rcaInput.NewRcaInputRepository(gormClient)
restClient := rest.NewHttpRestClient()
documentService := documentService.NewActionsImpl(restClient)
rcaService := rca.NewRcaService(incidentServiceV2, slackService, restClient, documentService, rcaRepository, rcaInputRepository, userService)
rcaService := rca.NewRcaService(incidentServiceV2, slackService, documentService, rcaRepository, rcaInputRepository, userService)
slashCommandProcessor := processor.NewSlashCommandProcessor(socketModeClient, slackbotClient, rcaService)
if viper.GetString("env") != "local" {

View File

@@ -190,7 +190,7 @@ func (s *Server) rcaHandler(houstonGroup *gin.RouterGroup) {
restClient := rest.NewHttpRestClient()
documentService := documentService.NewActionsImpl(restClient)
userRepository := user.NewUserRepository(s.db)
rcaService := rca.NewRcaService(incidentServiceV2, slackService, restClient, documentService, rcaRepository, rcaInputRepository, userRepository)
rcaService := rca.NewRcaService(incidentServiceV2, slackService, documentService, rcaRepository, rcaInputRepository, userRepository)
rcaHandler := handler.NewRcaHandler(s.gin, rcaService)
houstonGroup.POST("/rca", rcaHandler.HandlePostRca)

View File

@@ -0,0 +1,93 @@
package maverick
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/spf13/viper"
"go.uber.org/zap"
"houston/common/util"
"houston/logger"
"houston/pkg/rest"
service "houston/service/request"
"io"
"net/http"
"time"
)
type MaverickClient struct {
RestClient rest.HttpRestClient
DefaultTimeout time.Duration
BaseURl string
AuthorizationToken string
}
func NewMaverickClient() *MaverickClient {
return &MaverickClient{
RestClient: rest.NewHttpRestClient(),
DefaultTimeout: viper.GetDuration("DEFAULT_MAVERICK_TIMEOUT"),
BaseURl: viper.GetString("MAVERICK_BASE_URL"),
AuthorizationToken: viper.GetString("MAVERICK_AUTHORIZATION_TOKEN"),
}
}
func (maverickClient *MaverickClient) UploadConversationURLs(details service.IncidentConversationDetails,
configuration service.MaverickConfiguration) error {
fullURL := maverickClient.BaseURl + configuration.ConfigurationUrl
requestHeaders := map[string]string{
"X-Correlation-Id": viper.GetString("MAVERICK_CORRELATION_ID"),
"Authorization": maverickClient.AuthorizationToken,
"Content-Type": util.ContentTypeJSON,
}
genAiRequestPayload := service.GenAiRequest{
Inputs: []service.Input{
{
ToolName: viper.GetString("MAVERICK_TOOL_NAME"),
ToolParametersRequest: service.ToolParametersRequest{
IncidentId: details.IncidentId,
SlackConversationDocUrl: details.SlackUrl,
GoogleMeetTranscriptUrls: details.TranscriptUrls,
},
},
},
}
incidentId := details.IncidentId
payload, marshalError := json.Marshal(genAiRequestPayload)
if marshalError != nil {
logger.Error("Error while marshalling json", zap.Any("payload", payload), zap.Error(marshalError))
return marshalError
}
currentResponse, clientErr := maverickClient.RestClient.PostWithTimeout(fullURL, *bytes.NewBuffer(payload), requestHeaders,
maverickClient.DefaultTimeout, nil)
if clientErr != nil {
logger.Error(fmt.Sprintf("Error while sending data to GenAI for incident id: %d", incidentId), zap.Error(clientErr))
return clientErr
}
if currentResponse.StatusCode != http.StatusOK {
logger.Error(fmt.Sprintf("error while sending payload to GenAI with status code %d for incident id: %d",
currentResponse.StatusCode, incidentId))
return errors.New("error while sending payload to GenAI")
}
responseBody, err := io.ReadAll(currentResponse.Body)
if err != nil {
logger.Error("error while reading response body", zap.Error(err))
return err
}
var genAiResponse service.GenAiResponse
err = json.Unmarshal(responseBody, &genAiResponse)
if err != nil {
logger.Error("error while unmarshalling response body", zap.Any("response body", responseBody), zap.Error(err))
return err
}
if len(genAiResponse.Errors) > 0 {
logger.Error(fmt.Sprintf("error occurred in response from GenAI for incident id: %d", incidentId),
zap.Any("errors", genAiResponse.Errors))
return errors.New("error occurred in response from GenAI")
}
if genAiResponse.Data.Status == "" {
return errors.New("data in response from GenAI is empty")
}
return nil
}

View File

@@ -0,0 +1,8 @@
package maverick
import service "houston/service/request"
type IMaverickClient interface {
UploadConversationURLs(details service.IncidentConversationDetails,
configuration service.MaverickConfiguration) error
}

View File

@@ -0,0 +1,123 @@
package maverick
import (
"bytes"
"encoding/json"
"errors"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"houston/logger"
"houston/mocks"
service "houston/service/request"
"io"
"net/http"
"testing"
)
const TestBaseUrl = "https://maverick.com"
type MaverickClientSuite struct {
suite.Suite
maverickClient *MaverickClient
restClient *mocks.HttpRestClientMock
}
func (suite *MaverickClientSuite) SetupSuite() {
logger.InitLogger()
viper.Set("MAVERICK_BASE_URL", TestBaseUrl)
viper.Set("MAVERICK_CORRELATION_ID", "correlationId")
viper.Set("MAVERICK_AUTHORIZATION_TOKEN", "authorizationToken")
viper.Set("DEFAULT_MAVERICK_TIMEOUT", "5s")
suite.restClient = mocks.NewHttpRestClientMock(suite.T())
suite.maverickClient = &MaverickClient{
RestClient: suite.restClient,
}
}
func TestMaverickClient(t *testing.T) {
suite.Run(t, new(MaverickClientSuite))
}
func (suite *MaverickClientSuite) TestUploadConversationURLs_RestClientFailure() {
suite.restClient.PostWithTimeoutMock.Return(nil, errors.New("error"))
details, configuration := getMockParametersForUploadConversationURLs()
err := suite.maverickClient.UploadConversationURLs(details, configuration)
assert.Error(suite.T(), err)
assert.EqualError(suite.T(), err, "error")
}
func (suite *MaverickClientSuite) TestUploadConversationURLs_StatusCodeFailure() {
suite.restClient.PostWithTimeoutMock.Return(&http.Response{StatusCode: http.StatusBadRequest}, nil)
details, configuration := getMockParametersForUploadConversationURLs()
err := suite.maverickClient.UploadConversationURLs(details, configuration)
assert.EqualError(suite.T(), err, "error while sending payload to GenAI")
}
func (suite *MaverickClientSuite) TestUploadConversationURLs_ResponseFailure() {
genAiResponse := service.GenAiResponse{
Errors: []string{"error"},
}
responseBody, _ := json.Marshal(genAiResponse)
testResponse := http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(string(responseBody)))}
suite.restClient.PostWithTimeoutMock.Return(&testResponse, nil)
details, configuration := getMockParametersForUploadConversationURLs()
err := suite.maverickClient.UploadConversationURLs(details, configuration)
assert.EqualError(suite.T(), err, "error occurred in response from GenAI")
}
func (suite *MaverickClientSuite) TestUploadConversationURLs_Success() {
genAiResponse := service.GenAiResponse{
Errors: []string{},
Data: service.Data{
Status: "Accepted",
},
}
responseBody, _ := json.Marshal(genAiResponse)
testResponse := http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(string(responseBody)))}
suite.restClient.PostWithTimeoutMock.Return(&testResponse, nil)
details, configuration := getMockParametersForUploadConversationURLs()
err := suite.maverickClient.UploadConversationURLs(details, configuration)
assert.NoError(suite.T(), err)
}
func (suite *MaverickClientSuite) TestUploadConversationURLs_ResponseUnmarshalFailure() {
testResponse := http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString("invalid response"))}
suite.restClient.PostWithTimeoutMock.Return(&testResponse, nil)
details, configuration := getMockParametersForUploadConversationURLs()
err := suite.maverickClient.UploadConversationURLs(details, configuration)
assert.Error(suite.T(), err)
}
func (suite *MaverickClientSuite) TestUploadConversationURLS_EmptyDataFailure() {
genAiResponse := service.GenAiResponse{}
responseBody, _ := json.Marshal(genAiResponse)
testResponse := http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(string(responseBody)))}
suite.restClient.PostWithTimeoutMock.Return(&testResponse, nil)
details, configuration := getMockParametersForUploadConversationURLs()
err := suite.maverickClient.UploadConversationURLs(details, configuration)
assert.EqualError(suite.T(), err, "data in response from GenAI is empty")
}
func (suite *MaverickClientSuite) TestUploadConversationURLs_EmptyStatusFailure() {
genAiResponse := service.GenAiResponse{
Data: service.Data{},
}
responseBody, _ := json.Marshal(genAiResponse)
testResponse := http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString(string(responseBody)))}
suite.restClient.PostWithTimeoutMock.Return(&testResponse, nil)
details, configuration := getMockParametersForUploadConversationURLs()
err := suite.maverickClient.UploadConversationURLs(details, configuration)
assert.EqualError(suite.T(), err, "data in response from GenAI is empty")
}
func getMockParametersForUploadConversationURLs() (service.IncidentConversationDetails, service.MaverickConfiguration) {
details := service.IncidentConversationDetails{
IncidentId: 1,
SlackUrl: "slackUrl",
TranscriptUrls: []string{},
}
configuration := service.MaverickConfiguration{
ToolName: "toolName",
ConfigurationUrl: "/configurationUrl",
}
return details, configuration
}

View File

@@ -15,7 +15,7 @@ import (
"houston/model/rcaInput"
"houston/model/user"
"houston/pkg/documentService"
"houston/pkg/rest"
"houston/pkg/maverick"
rcaRepository "houston/repository/rca"
rcaInputRepository "houston/repository/rcaInput"
"houston/service/incident"
@@ -23,7 +23,6 @@ import (
response "houston/service/response"
common "houston/service/response/common"
slackService "houston/service/slack"
"io"
"net/http"
"regexp"
"time"
@@ -32,8 +31,8 @@ import (
type RcaService struct {
incidentService incident.IIncidentService
slackService slackService.ISlackService
restClient rest.HttpRestClient
documentService documentService.ServiceActions
maverickClient maverick.IMaverickClient
rcaRepository rcaRepository.IRcaRepository
rcaInputRepository rcaInputRepository.IRcaInputRepository
userRepository user.IUserRepository
@@ -43,12 +42,13 @@ const logTag = "[post-rca]"
const SlackIdMentionPatternString = `<@(\w+)>`
func NewRcaService(incidentService incident.IIncidentService, slackService slackService.ISlackService,
restClient rest.HttpRestClient, documentService documentService.ServiceActions, rcaRepository rcaRepository.IRcaRepository, rcaInputRepository rcaInputRepository.IRcaInputRepository, userRepository user.IUserRepository) *RcaService {
documentService documentService.ServiceActions, rcaRepository rcaRepository.IRcaRepository,
rcaInputRepository rcaInputRepository.IRcaInputRepository, userRepository user.IUserRepository) *RcaService {
return &RcaService{
incidentService: incidentService,
slackService: slackService,
restClient: restClient,
documentService: documentService,
maverickClient: maverick.NewMaverickClient(),
rcaRepository: rcaRepository,
rcaInputRepository: rcaInputRepository,
userRepository: userRepository,
@@ -202,8 +202,12 @@ func (r *RcaService) SendConversationDataForGeneratingRCA(incidentId uint, chann
return createErr
}
}
err = r.uploadConversationURLs(incidentId, slackUrl, transcriptUrls)
details := service.IncidentConversationDetails{IncidentId: incidentId, SlackUrl: slackUrl, TranscriptUrls: transcriptUrls}
configuration := service.MaverickConfiguration{
ToolName: viper.GetString("MAVERICK_TOOL_NAME"),
ConfigurationUrl: viper.GetString("MAVERICK_CONFIGURATION_URL"),
}
err = r.maverickClient.UploadConversationURLs(details, configuration)
if err != nil {
logger.Error("Error while sending urls to GenAI", zap.Error(err))
return err
@@ -319,61 +323,6 @@ func (r *RcaService) replaceUsernames(conversations *[]response.ConversationResp
}
}
func (r *RcaService) uploadConversationURLs(incidentId uint, slackUrl string, transcriptUrls []string) error {
baseURL := viper.GetString("MAVERICK_BASE_URL")
fullURL := baseURL + viper.GetString("MAVERICK_CONFIGURATION_URL")
requestHeaders := map[string]string{
"X-Correlation-Id": viper.GetString("MAVERICK_CORRELATION_ID"),
"Authorization": viper.GetString("MAVERICK_AUTHORIZATION_TOKEN"),
"Content-Type": util.ContentTypeJSON,
}
timeOut := viper.GetDuration("DEFAULT_MAVERICK_TIMEOUT")
genAiRequestPayload := service.GenAiRequest{
Inputs: []service.Input{
{
ToolName: viper.GetString("MAVERICK_TOOL_NAME"),
ToolParametersRequest: service.ToolParametersRequest{
IncidentId: incidentId,
SlackConversationDocUrl: slackUrl,
GoogleMeetTranscriptUrls: transcriptUrls,
},
},
},
}
payload, marshalError := json.Marshal(genAiRequestPayload)
if marshalError != nil {
logger.Error("Error while marshalling json", zap.Any("payload", payload), zap.Error(marshalError))
return marshalError
}
currentResponse, clientErr := r.restClient.PostWithTimeout(fullURL, *bytes.NewBuffer(payload), requestHeaders, timeOut, nil)
if clientErr != nil {
logger.Error("Error while sending to GenAI", zap.Error(clientErr))
return clientErr
}
if currentResponse.StatusCode != http.StatusOK {
logger.Error(fmt.Sprintf("error while sending payload to GenAI with status code %d", currentResponse.StatusCode))
return errors.New("error while sending payload to GenAI")
}
responseBody, err := io.ReadAll(currentResponse.Body)
if err != nil {
logger.Error("error while reading response body", zap.Error(err))
return err
}
var genAiResponse service.GenAiResponse
err = json.Unmarshal(responseBody, &genAiResponse)
if err != nil {
logger.Error("error while unmarshalling response body", zap.Any("response body", responseBody), zap.Error(err))
return err
}
if len(genAiResponse.Errors) > 0 {
logger.Error("error occurred in response from GenAI", zap.Any("errors", genAiResponse.Errors))
return errors.New("error occurred in response from GenAI")
}
return nil
}
// getting title, description of an incident to be attached to the Slack conversation data
func (r *RcaService) getExtraConversationData(incidentId uint) (*response.ConversationResponse, error) {
incidentEntity, err := r.incidentService.GetIncidentById(incidentId)

View File

@@ -1,7 +1,6 @@
package rca
import (
"bytes"
"encoding/json"
"errors"
"fmt"
@@ -19,23 +18,31 @@ import (
service "houston/service/request"
response "houston/service/response"
"io"
"net/http"
"testing"
"time"
)
func setupTest(t *testing.T) (*mocks.IIncidentServiceMock, *mocks.ISlackServiceMock, *mocks.IRcaRepositoryMock, *RcaService, *mocks.IRcaInputRepositoryMock, *mocks.ServiceActionsMock, *mocks.HttpRestClientMock, *mocks.IUserRepositoryMock) {
func setupTest(t *testing.T) (*mocks.IIncidentServiceMock, *mocks.ISlackServiceMock, *mocks.IRcaRepositoryMock,
*RcaService, *mocks.IRcaInputRepositoryMock, *mocks.ServiceActionsMock, *mocks.IMaverickClientMock, *mocks.IUserRepositoryMock) {
logger.InitLogger()
incidentService := mocks.NewIIncidentServiceMock(t)
slackService := mocks.NewISlackServiceMock(t)
rcaRepository := mocks.NewIRcaRepositoryMock(t)
rcaInputRepository := mocks.NewIRcaInputRepositoryMock(t)
documentService := mocks.NewServiceActionsMock(t)
restClient := mocks.NewHttpRestClientMock(t)
maverickClient := mocks.NewIMaverickClientMock(t)
userRepository := mocks.NewIUserRepositoryMock(t)
rcaService := NewRcaService(incidentService, slackService, restClient, documentService, rcaRepository, rcaInputRepository, userRepository)
rcaService := &RcaService{
incidentService: incidentService,
slackService: slackService,
documentService: documentService,
maverickClient: maverickClient,
rcaRepository: rcaRepository,
rcaInputRepository: rcaInputRepository,
userRepository: userRepository,
}
return incidentService, slackService, rcaRepository, rcaService, rcaInputRepository, documentService, restClient, userRepository
return incidentService, slackService, rcaRepository, rcaService, rcaInputRepository, documentService, maverickClient, userRepository
}
func TestRcaService_PostRcaToIncidentChannel_Success(t *testing.T) {
@@ -204,7 +211,7 @@ func TestRcaService_PostRcaToIncidentChannel_SlackFailure_RcaFailureCase(t *test
}
func TestSendConversationDataForGeneratingRCA(t *testing.T) {
incidentService, slackService, rcaRepository, rcaService, rcaInputRepository, documentService, restClient, userRepository := setupTest(t)
incidentService, slackService, rcaRepository, rcaService, rcaInputRepository, documentService, maverickClient, userRepository := setupTest(t)
// successfully upload conversation history
testUploadResponse := &model.FileUploadURLGeneratorResponse{
FormData: model.FormData{Key: "key"},
@@ -227,15 +234,7 @@ func TestSendConversationDataForGeneratingRCA(t *testing.T) {
}).Then("download_url", nil)
//successfully uploading conversation urls
setMockEnvironmentalVariables()
genAiHeaders, genAiRequest, genAiResponse := getMockHeadersAndRequestAndResponse(1)
payload, _ := json.Marshal(genAiRequest)
responseBody, _ := json.Marshal(genAiResponse)
restClient.PostWithTimeoutMock.When("base_urlconfig_url", *bytes.NewBuffer(payload), genAiHeaders, 4*time.Second, nil).Then(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(string(responseBody))),
}, nil)
maverickClient.UploadConversationURLsMock.Return(nil)
// successfully sending conversations to gen ai
rcaInputRepository.CreateRcaInputMock.Inspect(func(rcaInput *rcaInput.RcaInputEntity) {
@@ -276,49 +275,6 @@ func TestUploadSlackConversationHistory(t *testing.T) {
assert.Equal(t, "key", expectedKey)
}
func TestUploadConversationURLsSuccess(t *testing.T) {
_, _, _, rcaService, _, _, restClient, _ := setupTest(t)
setMockEnvironmentalVariables()
genAiHeaders, genAiRequest, genAiResponse := getMockHeadersAndRequestAndResponse(1)
payload, _ := json.Marshal(genAiRequest)
responseBody, _ := json.Marshal(genAiResponse)
restClient.PostWithTimeoutMock.When("base_urlconfig_url", *bytes.NewBuffer(payload), genAiHeaders, 4*time.Second, nil).Then(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(string(responseBody))),
}, nil)
err := rcaService.uploadConversationURLs(1, "download_url", []string{})
assert.Nil(t, err)
}
func TestUploadConversationURLsFailure(t *testing.T) {
_, _, _, rcaService, _, _, restClient, _ := setupTest(t)
setMockEnvironmentalVariables()
genAiHeaders, genAiRequest, _ := getMockHeadersAndRequestAndResponse(1)
genAiResponse := service.GenAiResponse{
Errors: []string{"error"},
}
responseBody, _ := json.Marshal(genAiResponse)
payload, _ := json.Marshal(genAiRequest)
// if status code is not 200
restClient.PostWithTimeoutMock.When("base_urlconfig_url", *bytes.NewBuffer(payload), genAiHeaders, 4*time.Second, nil).Then(&http.Response{
StatusCode: http.StatusBadRequest,
}, nil)
err := rcaService.uploadConversationURLs(1, "download_url", []string{})
assert.EqualError(t, err, "error while sending payload to GenAI")
// if gen ai response has errors
viper.Set("DEFAULT_MAVERICK_TIMEOUT", 3*time.Second)
restClient.PostWithTimeoutMock.When("base_urlconfig_url", *bytes.NewBuffer(payload), genAiHeaders, 3*time.Second, nil).Then(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(string(responseBody))),
}, nil)
err = rcaService.uploadConversationURLs(1, "download_url", []string{})
assert.EqualError(t, err, "error occurred in response from GenAI")
}
func TestExtractUserIds(t *testing.T) {
_, _, _, rcaService, _, _, _, _ := setupTest(t)

View File

@@ -36,3 +36,13 @@ type GenAiResponse struct {
type Data struct {
Status string `json:"status"`
}
type MaverickConfiguration struct {
ToolName string `json:"tool_name"`
ConfigurationUrl string `json:"configuration_url"`
}
type IncidentConversationDetails struct {
IncidentId uint `json:"incident_id"`
SlackUrl string `json:"slack_url"`
TranscriptUrls []string `json:"transcript_urls"`
}