-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add LLM suggestions to CLI (#94)
- Loading branch information
1 parent
e1f5918
commit d350da8
Showing
7 changed files
with
494 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
"github.com/manifoldco/promptui" | ||
"github.com/speakeasy-api/speakeasy/internal/validation" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
var suggestCmd = &cobra.Command{ | ||
Use: "suggest", | ||
Short: "Validate an OpenAPI document and get fixes suggested by ChatGPT", | ||
Long: `The "suggest" command validates an OpenAPI spec and uses OpenAI's ChatGPT to suggest fixes to your spec. | ||
You will need to set your OpenAI API key in a OPENAI_API_KEY environment variable. You will also need to authenticate with the Speakeasy API, | ||
you must first create an API key via https://app.speakeasyapi.dev and then set the SPEAKEASY_API_KEY environment variable to the value of the API key.`, | ||
RunE: suggestFixesOpenAPI, | ||
} | ||
|
||
func suggestInit() { | ||
suggestCmd.Flags().StringP("schema", "s", "", "path to the OpenAPI document") | ||
_ = suggestCmd.MarkFlagRequired("schema") | ||
rootCmd.AddCommand(suggestCmd) | ||
} | ||
|
||
func suggestFixesOpenAPI(cmd *cobra.Command, args []string) error { | ||
// no authentication required for validating specs | ||
|
||
schemaPath, err := cmd.Flags().GetString("schema") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := validation.ValidateOpenAPI(cmd.Context(), schemaPath, true); err != nil { | ||
rootCmd.SilenceUsage = true | ||
|
||
return err | ||
} | ||
|
||
uploadCommand := promptui.Styler(promptui.FGCyan, promptui.FGBold)("speakeasy api register-schema --schema=" + schemaPath) | ||
fmt.Printf("\nYou can upload your schema to Speakeasy using the following command:\n%s\n", uploadCommand) | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
package suggestions | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"github.com/speakeasy-api/speakeasy/internal/config" | ||
"io/ioutil" | ||
"mime/multipart" | ||
"net/http" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
) | ||
|
||
const uploadTimeout = time.Minute * 2 | ||
const suggestionTimeout = time.Minute * 1 | ||
|
||
const ApiURL = "https://api.prod.speakeasyapi.dev" | ||
|
||
var baseURL = ApiURL | ||
|
||
type suggestionResponse struct { | ||
Suggestion string `json:"suggestion"` | ||
} | ||
|
||
type suggestionRequest struct { | ||
Error string `json:"error"` | ||
LineNumber int `json:"line_number"` | ||
} | ||
|
||
func Upload(filePath string) (string, string, error) { | ||
openAIKey, err := GetOpenAIKey() | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
apiKey, err := getSpeakeasyAPIKey() | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
fileData, err := os.ReadFile(filePath) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
body := new(bytes.Buffer) | ||
writer := multipart.NewWriter(body) | ||
part, err := writer.CreatePart(map[string][]string{ | ||
"Content-Disposition": {"form-data; name=\"file\"; filename=\"" + filepath.Base(filePath) + "\""}, | ||
"Content-Type": {detectFileType(filePath)}, // Set the MIME type here | ||
}) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
_, err = part.Write(fileData) | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
err = writer.Close() | ||
if err != nil { | ||
return "", "", err | ||
} | ||
|
||
req, err := http.NewRequest("POST", baseURL+"/v1/llm/openapi", body) | ||
if err != nil { | ||
return "", "", fmt.Errorf("error creating request for upload: %v", err) | ||
} | ||
|
||
req.Header.Set("Content-Type", writer.FormDataContentType()) | ||
req.Header.Set("x-openai-key", openAIKey) | ||
req.Header.Set("x-api-key", apiKey) | ||
|
||
client := &http.Client{ | ||
Timeout: uploadTimeout, | ||
} | ||
resp, err := client.Do(req) | ||
if err != nil { | ||
return "", "", fmt.Errorf("error making request for upload: %v", err) | ||
} | ||
|
||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode == http.StatusUnprocessableEntity { | ||
return "", "", fmt.Errorf("OpenAPI document is larger than 50,000 line limit") | ||
} | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return "", "", fmt.Errorf("%v error occured: %s", resp.StatusCode, resp.Status) | ||
} | ||
|
||
token := resp.Header.Get("x-session-token") | ||
if token == "" { | ||
return "", "", fmt.Errorf("session token is empty") | ||
} | ||
|
||
return token, strings.ToLower(filepath.Ext(filePath))[1:], nil | ||
} | ||
|
||
func Suggestion(token string, error string, lineNumber int, fileType string) (string, error) { | ||
openAIKey, err := GetOpenAIKey() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
apiKey, err := getSpeakeasyAPIKey() | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
reqBody := suggestionRequest{ | ||
Error: error, | ||
LineNumber: lineNumber, | ||
} | ||
|
||
jsonPayload, err := json.Marshal(reqBody) | ||
if err != nil { | ||
return "", fmt.Errorf("error marshaling request payload: %v", err) | ||
} | ||
|
||
req, err := http.NewRequest("POST", baseURL+"/v1/llm/openapi/suggestion", bytes.NewBuffer(jsonPayload)) | ||
if err != nil { | ||
return "", fmt.Errorf("error creating request for suggest: %v", err) | ||
} | ||
|
||
req.Header.Set("Content-Type", "application/json") | ||
req.Header.Set("x-session-token", token) | ||
req.Header.Set("x-openai-key", openAIKey) | ||
req.Header.Set("x-api-key", apiKey) | ||
req.Header.Set("x-file-type", fileType) | ||
|
||
client := &http.Client{ | ||
Timeout: suggestionTimeout, | ||
} | ||
resp, err := client.Do(req) | ||
if err != nil { | ||
return "", fmt.Errorf("error making request for suggest: %v", err) | ||
} | ||
|
||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return "", fmt.Errorf("%v error occured: %s", resp.StatusCode, resp.Status) | ||
} | ||
|
||
body, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
var response suggestionResponse | ||
if err := json.Unmarshal(body, &response); err != nil { | ||
return "", fmt.Errorf("error unmarshaling response body: %v", err) | ||
} | ||
|
||
return response.Suggestion, nil | ||
} | ||
|
||
func Clear(token string) error { | ||
apiKey, err := getSpeakeasyAPIKey() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
req, err := http.NewRequest("DELETE", baseURL+"/v1/llm/openapi", nil) | ||
if err != nil { | ||
return fmt.Errorf("error creating request for suggest: %v", err) | ||
} | ||
|
||
req.Header.Set("Content-Type", "application/json") | ||
req.Header.Set("x-session-token", token) | ||
req.Header.Set("x-api-key", apiKey) | ||
|
||
client := &http.Client{ | ||
Timeout: suggestionTimeout, | ||
} | ||
resp, err := client.Do(req) | ||
if err != nil { | ||
return fmt.Errorf("error making request for suggest: %v", err) | ||
} | ||
|
||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return fmt.Errorf("unexpected status code: %v", resp.StatusCode) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func GetOpenAIKey() (string, error) { | ||
key := os.Getenv("OPENAI_API_KEY") | ||
if key == "" { | ||
return "", fmt.Errorf("OPENAI_API_KEY must be set to use LLM Suggestions") | ||
} | ||
|
||
return key, nil | ||
} | ||
|
||
func getSpeakeasyAPIKey() (string, error) { | ||
key, _ := config.GetSpeakeasyAPIKey() | ||
if key == "" { | ||
return "", fmt.Errorf("no speakeasy api key available, please set SPEAKEASY_API_KEY or run 'speakeasy auth' to authenticate the CLI with the Speakeasy Platform") | ||
} | ||
|
||
return key, nil | ||
} | ||
|
||
func init() { | ||
if url := os.Getenv("SPEAKEASY_SERVER_URL"); url != "" { | ||
baseURL = url | ||
} | ||
} |
Oops, something went wrong.