-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/ai/explain #754
base: main
Are you sure you want to change the base?
Feat/ai/explain #754
Changes from all commits
47a2b22
a90890a
fc4452e
037478d
c4bc87c
101fa65
87cefdf
5d6d9ba
743f8b9
894e40a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
/* | ||
* © 2023-2024 Snyk Limited | ||
* | ||
* 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 command | ||
|
||
|
||
import ( | ||
"context" | ||
"errors" | ||
"github.com/snyk/snyk-ls/application/config" | ||
"github.com/snyk/snyk-ls/domain/snyk" | ||
"github.com/snyk/snyk-ls/infrastructure/code" | ||
"github.com/snyk/snyk-ls/internal/notification" | ||
"github.com/snyk/snyk-ls/internal/types" | ||
) | ||
|
||
|
||
type generateAIExplanation struct { | ||
command types.CommandData | ||
notifier notification.Notifier | ||
issueProvider snyk.IssueProvider | ||
codeScanner *code.Scanner | ||
} | ||
|
||
func (cmd *generateAIExplanation) Command() types.CommandData { | ||
return cmd.command | ||
} | ||
|
||
func (cmd *generateAIExplanation) Execute (ctx context.Context) (any, error) { | ||
logger := config.CurrentConfig().Logger().With().Str("method", "generateAIExplanation.Execute").Logger() | ||
|
||
args := cmd.command.Arguments | ||
if len(args) < 4 { | ||
return nil, errors.New("missing required arguments") | ||
} | ||
|
||
derivation, ok := args[0].(string) | ||
if !ok { | ||
return nil, errors.New("failed to parse derivation") | ||
} | ||
|
||
ruleKey, ok := args[1].(string) | ||
if !ok { | ||
return nil, errors.New("failed to parse ruleKey") | ||
} | ||
|
||
ruleMessage, ok := args[2].(string) | ||
if !ok { | ||
return nil, errors.New("failed to parse ruleMessage") | ||
} | ||
|
||
diff, ok := args[3].(string) | ||
if !ok { | ||
return nil, errors.New("failed to parse diff") | ||
} | ||
|
||
explanation, err := cmd.codeScanner.GetAIExplanation(ctx, derivation, ruleKey, ruleMessage, diff) | ||
if err != nil { | ||
logger.Err(err).Msgf("received an error from API: %s", err.Error()) | ||
return explanation, err | ||
} | ||
return explanation, nil | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file would basically become |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
/* | ||
* © 2024 Snyk Limited | ||
* | ||
* 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 code | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"time" | ||
|
||
"github.com/snyk/snyk-ls/application/config" | ||
performance2 "github.com/snyk/snyk-ls/internal/observability/performance" | ||
) | ||
|
||
|
||
func (sc *Scanner) GetAIExplanation( | ||
ctx context.Context, | ||
derivation string, | ||
ruleKey string, | ||
ruleMessage string, | ||
diff string, | ||
) (explanation string, err error) { | ||
method := "GetAIExplanation" | ||
logger := config.CurrentConfig().Logger().With().Str("method", method).Logger() | ||
span := sc.BundleUploader.instrumentor.StartSpan(ctx, method) | ||
defer sc.BundleUploader.instrumentor.Finish(span) | ||
|
||
codeClient := sc.BundleUploader.SnykCode | ||
|
||
options := ExplainOptions{ | ||
derivation: derivation, | ||
ruleKey: ruleKey, | ||
ruleMessage: ruleMessage, | ||
diff: diff, | ||
} | ||
logger.Info().Str("derivation", derivation).Msg("Started retrieving vuln explanation.") | ||
|
||
ticker := time.NewTicker(1 * time.Second) | ||
defer ticker.Stop() | ||
// timeoutTimer sends a trigger after 2 minutes to its channel | ||
timeoutTimer := time.NewTimer(2 * time.Minute) | ||
defer timeoutTimer.Stop() | ||
for { | ||
select { | ||
case <-timeoutTimer.C: | ||
const msg = "Timeout waiting for an explanation." | ||
logger.Error().Msg(msg) | ||
return "", errors.New(msg) | ||
case <-ticker.C: | ||
explanation, explainStatus, explanationErr := codeClient.GetAIExplanation(span.Context(), options) | ||
if explanationErr != nil { | ||
logger.Err(explanationErr).Msg("Error getting an explanation") | ||
return "", explanationErr | ||
} else if explainStatus == completeStatus { | ||
return explanation, nil | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
||
func (s *SnykCodeHTTPClient) GetAIExplanation(ctx context.Context, options ExplainOptions) ( | ||
explanation string, | ||
status string, | ||
err error, | ||
) { | ||
method := "GetAIExplanation" | ||
span := s.instrumentor.StartSpan(ctx, method) | ||
defer s.instrumentor.Finish(span) | ||
logger := config.CurrentConfig().Logger().With().Str("method", method).Logger() | ||
logger.Info().Msg("Started obtaining AI explanation") | ||
defer logger.Info().Msg("Finished obtaining AI explanation") | ||
|
||
explainResponse, err := s.getExplainResponse(ctx, options) | ||
if err != nil { | ||
return "", status, err | ||
} | ||
return explainResponse.Explanation, completeStatus, nil | ||
} | ||
|
||
func (s *SnykCodeHTTPClient) getExplainResponse(ctx context.Context, options ExplainOptions) (explainResponse ExplainResponse, err error) { | ||
method := "getExplainResponse" | ||
|
||
span := s.instrumentor.StartSpan(ctx, method) | ||
defer s.instrumentor.Finish(span) | ||
logger := config.CurrentConfig().Logger().With().Str("method", method).Logger() | ||
|
||
requestId, err := performance2.GetTraceId(ctx) | ||
if err != nil { | ||
logger.Err(err).Msg(failedToObtainRequestIdString + err.Error()) | ||
return explainResponse, err | ||
} | ||
logger.Info().Str("requestId", requestId).Msg("Started obtaining explain Response") | ||
defer logger.Info().Str("requestId", requestId).Msg("Finished obtaining explain Response") | ||
|
||
response, err := s.RunExplain(span.Context(), options) | ||
if err != nil { | ||
return response, err | ||
} | ||
|
||
logger.Debug().Msgf("Status: %s", response.Status) | ||
|
||
if response.Status == "FAILED" { | ||
logger.Error().Str("responseStatus", response.Status).Msg("explain failed") | ||
return response, errors.New("Explain failed") | ||
} | ||
|
||
if response.Status == "" { | ||
logger.Error().Str("responseStatus", response.Status).Msg("unknown response status (empty)") | ||
return response, errors.New("Unknown response status (empty)") | ||
} | ||
|
||
if response.Status != completeStatus { | ||
return response, nil | ||
} | ||
|
||
return response, nil | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move to |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,8 @@ | |
package code | ||
|
||
import ( | ||
"io/ioutil" | ||
Check failure on line 20 in infrastructure/code/snyk_code_http_client.go GitHub Actions / lint
|
||
|
||
"bytes" | ||
"context" | ||
"encoding/json" | ||
|
@@ -524,6 +526,80 @@ | |
return response, nil | ||
} | ||
|
||
func (s *SnykCodeHTTPClient) RunExplain(ctx context.Context, options ExplainOptions) (ExplainResponse, error) { | ||
requestId, err := performance2.GetTraceId(ctx) | ||
span := s.instrumentor.StartSpan(ctx, "code.RunExplain") | ||
defer span.Finish() | ||
|
||
logger := s.c.Logger().With().Str("method", "code.RunExplain").Str("requestId", requestId).Logger() | ||
if err != nil { | ||
logger.Err(err).Msg(failedToObtainRequestIdString + err.Error()) | ||
return ExplainResponse{}, err | ||
} | ||
|
||
logger.Debug().Msg("API: Retrieving explain for bundle") | ||
defer logger.Debug().Msg("API: Retrieving explain done") | ||
|
||
// construct the requestBody depending on the values given from IDE. | ||
requestBody, err := s.explainRequestBody(&options) | ||
if err != nil { | ||
logger.Err(err).Str("requestBody", string(requestBody)).Msg("error creating request body") | ||
return ExplainResponse{}, err | ||
} | ||
|
||
url := "http://localhost:10000/explain" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess this should be injected from configuration There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. shouldn't this be the same API endpoint that we have from the config.Endpoint? |
||
logger.Debug().Str("payload body: %s\n", string(requestBody)).Msg("Marshaled payload") | ||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(requestBody)) | ||
if err != nil { | ||
logger.Err(err).Str("requestBody", string(requestBody)).Msg("error getting response") | ||
} | ||
defer resp.Body.Close() | ||
|
||
// Read the response body | ||
responseBody, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
logger.Err(err).Str("requestBody", string(requestBody)).Msg("error reading all response") | ||
} | ||
logger.Debug().Str("response body: %s\n", string(responseBody)).Msg("Got the response") | ||
|
||
if err != nil { | ||
return ExplainResponse{}, err | ||
} | ||
|
||
var response ExplainResponse | ||
response.Status = completeStatus | ||
err = json.Unmarshal(responseBody, &response) | ||
if err != nil { | ||
logger.Err(err).Str("responseBody", string(responseBody)).Msg("error unmarshalling") | ||
return ExplainResponse{}, err | ||
} | ||
return response, nil | ||
} | ||
|
||
func (s *SnykCodeHTTPClient) explainRequestBody(options *ExplainOptions) ([]byte, error) { | ||
logger := s.c.Logger().With().Str("method", "code.explainRequestBody").Logger() | ||
|
||
var request ExplainRequest | ||
if options.diff == "" { | ||
request.VulnExplanation = &ExplainVulnerabilityRequest{ | ||
RuleId: options.ruleKey, | ||
Derivation: options.derivation, | ||
RuleMessage: options.ruleMessage, | ||
ExplanationLength: SHORT, | ||
} | ||
logger.Debug().Msg("payload for VulnExplanation") | ||
} else{ | ||
request.FixExplanation = &ExplainFixRequest{ | ||
RuleId: options.ruleKey, | ||
Diff: options.diff, | ||
ExplanationLength: SHORT, | ||
} | ||
logger.Debug().Msg("payload for FixExplanation") | ||
} | ||
requestBody, err := json.Marshal(request) | ||
return requestBody, err | ||
} | ||
|
||
func (s *SnykCodeHTTPClient) autofixRequestBody(options *AutofixOptions) ([]byte, error) { | ||
_, ruleID, ok := getIssueLangAndRuleId(options.issue) | ||
if !ok { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this will go somewhere else - most likely in a new subcomponent (
SnykLLMBindings
)