Skip to content
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

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,20 @@ Right now the language server supports the following actions:
// WHEN
comp.changePassword();
```
- `Generate AI Explanation` allows to retrieve explainations for various use-cases.
- command: `snyk.generateAIExplanation`
- args:
- derivation string
- ruleKey string
- ruleMessage string
- diff string
- returns an explanations as string:
```json5
[{
"explainId": "123",
"explanations": "bla bla explained"
}]
```
- `Feature Flag Status Command` triggers the api call to check if a feature flag is enabled
- command: `snyk.getFeatureFlagStatus`
- args:
Expand All @@ -393,8 +407,8 @@ Right now the language server supports the following actions:
```
- `Clear Cache` Clears either persisted or inMemory Cache or both.
- command: `snyk.clearCache`
- args:
- `folderUri` string,
- args:
- `folderUri` string,
- `cacheType` `persisted` or `inMemory`
- `Generate Issue Description` Generates issue description in HTML.
- command: `snyk.generateIssueDescription`
Expand Down
1 change: 1 addition & 0 deletions application/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ func initializeHandler(srv *jrpc2.Server) handler.Func {
types.ClearCacheCommand,
types.GenerateIssueDescriptionCommand,
types.ReportAnalyticsCommand,
types.GenerateAIExplanationCommand,
},
},
},
Expand Down
2 changes: 2 additions & 0 deletions application/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ func Test_initialize_shouldSupportAllCommands(t *testing.T) {
assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.CodeFixCommand)
assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.CodeSubmitFixFeedback)
assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.CodeFixDiffsCommand)
assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.GenerateAIExplanationCommand)
assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.GenerateIssueDescriptionCommand)
assert.Contains(t, result.Capabilities.ExecuteCommandProvider.Commands, types.ExecuteCLICommand)
}

Expand Down
7 changes: 7 additions & 0 deletions domain/ide/command/command_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ func CreateFromCommandData(
issueProvider: issueProvider,
notifier: notifier,
}, nil
case types.GenerateAIExplanationCommand:
return &generateAIExplanation{
command: commandData,
codeScanner: codeScanner,
issueProvider: issueProvider,
notifier: notifier,
}, nil
case types.ExecuteCLICommand:
return &executeCLICommand{command: commandData, authService: authService, notifier: notifier, logger: c.Logger(), cli: cli}, nil
case types.ClearCacheCommand:
Expand Down
76 changes: 76 additions & 0 deletions domain/ide/command/generate_ai_explanation.go
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)
Copy link
Collaborator

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)

if err != nil {
logger.Err(err).Msgf("received an error from API: %s", err.Error())
return explanation, err
}
return explanation, nil
}
131 changes: 131 additions & 0 deletions infrastructure/code/ai_explain.go
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file would basically become DeepCodeLLMBindings

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
}
5 changes: 5 additions & 0 deletions infrastructure/code/fake_snyk_code_api_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,18 @@ type FakeSnykCodeClient struct {
Options map[string]AnalysisOptions
C *config.Config
FeedbackSent string
Explanation string
}

func (f *FakeSnykCodeClient) GetAutofixDiffs(_ context.Context, _ string, _ AutofixOptions) (unifiedDiffSuggestions []AutofixUnifiedDiffSuggestion, status AutofixStatus, err error) {
f.AutofixStatus = AutofixStatus{message: completeStatus}
return f.UnifiedDiffSuggestions, f.AutofixStatus, nil
}

func (f *FakeSnykCodeClient) GetAIExplanation(_ context.Context, _ ExplainOptions) (explanation string, status string, err error) {
return f.Explanation, completeStatus, nil
}

func (f *FakeSnykCodeClient) getAutofixResponse(_ context.Context, _ AutofixOptions) (autofixResponse AutofixResponse, status AutofixStatus, err error) {
f.AutofixStatus = AutofixStatus{message: completeStatus}
return autofixResponse, f.AutofixStatus, nil
Expand Down
76 changes: 76 additions & 0 deletions infrastructure/code/snyk_code_http_client.go
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to DeepCodeLLMBinding

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package code

import (
"io/ioutil"

Check failure on line 20 in infrastructure/code/snyk_code_http_client.go

View workflow job for this annotation

GitHub Actions / lint

SA1019: "io/ioutil" has been deprecated since Go 1.19: As of Go 1.16, the same functionality is now provided by package [io] or package [os], and those implementations should be preferred in new code. See the specific function documentation for details. (staticcheck)

"bytes"
"context"
"encoding/json"
Expand Down Expand Up @@ -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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this should be injected from configuration

Copy link
Contributor

Choose a reason for hiding this comment

The 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 {
Expand Down
Loading
Loading