├── .gitignore ├── assistant ├── go.mod ├── main.go ├── types │ └── formdata.go ├── assistant │ ├── githubhelpers.go │ ├── parseresponse.go │ ├── githelpers.go │ └── assistant.go ├── web │ ├── templates │ │ ├── result.html │ │ └── index.html │ └── web.go └── chatgpt │ └── chatgpt.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ./assistant/openai-key.txt 2 | *.swo 3 | *.swp 4 | 5 | -------------------------------------------------------------------------------- /assistant/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thomasdullien/coding-assistant/assistant 2 | 3 | go 1.23.3 4 | -------------------------------------------------------------------------------- /assistant/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/thomasdullien/coding-assistant/assistant/web" 6 | ) 7 | 8 | func main() { 9 | fmt.Println("Starting ASSISTANT on localhost:8080") 10 | web.ServeWebInterface() 11 | } -------------------------------------------------------------------------------- /assistant/types/formdata.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // FormData holds the form data submitted by the user 4 | type FormData struct { 5 | GithubUser string 6 | RepoURL string 7 | Branch string 8 | Files []string 9 | Prompt string 10 | RepoType string 11 | } -------------------------------------------------------------------------------- /assistant/assistant/githubhelpers.go: -------------------------------------------------------------------------------- 1 | package assistant 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "bytes" 7 | "log" 8 | "github.com/thomasdullien/coding-assistant/assistant/types" 9 | ) 10 | 11 | // createPullRequest creates a pull request using the GitHub CLI (`gh`) command. 12 | // Logs detailed output in case of errors. 13 | func createPullRequest(data *types.FormData) (string, error) { 14 | // Prepare the `gh` command to create a pull request 15 | cmd := exec.Command("gh", "pr", "create", "--title", fmt.Sprintf("Automated Changes based on: %s", data.Prompt), "--body", fmt.Sprintf("Automated changes based on: %s", data.Prompt)) 16 | cmd.Dir = "repo" // Set the working directory to the local repo 17 | 18 | // Capture stdout and stderr 19 | var outBuf, errBuf bytes.Buffer 20 | cmd.Stdout = &outBuf 21 | cmd.Stderr = &errBuf 22 | 23 | // Execute the command 24 | err := cmd.Run() 25 | if err != nil { 26 | // Log the output and error if the command fails 27 | log.Printf("Failed to create pull request. Stdout: %s, Stderr: %s", outBuf.String(), errBuf.String()) 28 | return "", fmt.Errorf("failed to create pull request: %v", err) 29 | } 30 | 31 | // Log the successful output 32 | log.Printf("Pull request created successfully. Stdout: %s", outBuf.String()) 33 | return outBuf.String(), nil 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /assistant/web/templates/result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Result 7 | 57 | 58 | 59 |
60 |

{{.Message}}

61 | {{if .Link}} 62 |

View the pull request: {{.Link}}

63 | {{end}} 64 | Back to Form 65 |
66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /assistant/web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "text/template" 5 | "net/http" 6 | "log" 7 | 8 | "github.com/thomasdullien/coding-assistant/assistant/assistant" 9 | "github.com/thomasdullien/coding-assistant/assistant/types" 10 | ) 11 | 12 | var tmpl = template.Must(template.ParseFiles("web/templates/index.html")) 13 | var resultTmpl = template.Must(template.ParseFiles("web/templates/result.html")) 14 | 15 | // Serve the web interface 16 | func ServeWebInterface() { 17 | http.HandleFunc("/", homeHandler) 18 | http.HandleFunc("/submit", submitHandler) 19 | http.ListenAndServe(":8080", nil) 20 | } 21 | 22 | func homeHandler(w http.ResponseWriter, r *http.Request) { 23 | tmpl.Execute(w, nil) 24 | } 25 | 26 | // Handle form submission 27 | func submitHandler(w http.ResponseWriter, r *http.Request) { 28 | r.ParseForm() 29 | data := types.FormData{ 30 | GithubUser: r.FormValue("githubUser"), 31 | RepoURL: r.FormValue("repoURL"), 32 | Branch: "assistant-branch", 33 | Files: r.Form["files"], 34 | Prompt: r.FormValue("prompt"), 35 | RepoType: r.FormValue("repoType"), // Capture the repository type 36 | } 37 | 38 | // Run ProcessAssistant and capture the pull request link or error 39 | prLink, err := assistant.ProcessAssistant(data) 40 | if err != nil { 41 | log.Printf("Error in ProcessAssistant: %v", err) 42 | resultTmpl.Execute(w, map[string]string{ 43 | "Message": "An error occurred: " + err.Error(), 44 | "Link": "", 45 | }) 46 | return 47 | } 48 | 49 | // Show the result page with the pull request link 50 | resultTmpl.Execute(w, map[string]string{ 51 | "Message": "Pull request created successfully!", 52 | "Link": prLink, 53 | }) 54 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A coding assistant to assist with making changes to C++ and Golang code bases 2 | by providing PRs via the GitHub and OpenAI API. 3 | 4 | I do not like IDE integration for AI very much, and wanted to have something 5 | that can provide PRs instead. I also have very limited amounts of time, so 6 | I decided to write that thing largely by using ChatGPT. The result is this 7 | repository. 8 | 9 | This repository is the current state. You get a web interface on localhost 10 | where you can provide your GitHub username, the (SSH) URL for the repository 11 | you want to edit, the files that should be edited, and the prompt. 12 | 13 | The code will check out the repository, calculate the `#include` dependencies, 14 | send the file(s)-to-edit and their include dependencies to the OpenAI API, 15 | extract the results, apply them, run `make tests`, commit them to a branch if 16 | tests pass, and push the branch. 17 | 18 | I still need to ask ChatGPT to add code to create the PR, and there are tons 19 | of other things to still fix. That said, it has correctly submitted PRs for 20 | a hobby project of mine. 21 | 22 | For those curious about the process that created this code, the transcript of 23 | my conversation with ChatGPT is here: 24 | 25 | https://chatgpt.com/share/67352519-0dac-800f-8718-38efb309a3dd 26 | 27 | Below a transcript of the first few messages of the conversation, giving ChatGPT 28 | an overview of the goals of the project: 29 | 30 | ---- snip ---- 31 | Hey there. I would like to write a tool that helps me turn ChatGPT into a coding assistant, but not one that is integrated with my IDE, but rather one that is integrated into my GitHub workflow. 32 | 33 | So I wish to write code for the following tool, which we call ASSISTANT. 34 | 35 | 1. I have a locally-running web interface with a prompt window as well as a few fields where I can provide access credentials for a GitHub user, and the name of a GitHub repository. 36 | 2. I fill in the credentials and the repository URL. 37 | 3. I write a prompt in which I describe a particular change that I wish to be done. I provide a list of C++ header or source files in which these changes need to be performed. 38 | 4. ASSISTANT checks out a local copy of the specified repository using the credentials provided in the web interface. It then creates a local branch of the code with a 39 | 5. ASSISTANT calculates the header file dependencies for the files in which edits need to be transformed, possibly using some LLVM / Clang tooling. 40 | 6. ASSISTANT assembles all these files, and prepares a prompt to be submitted to ChatGPT. This prompt should include the prompt I provided in (3), but also the content of all the source files obtained in (5). The request to ChatGPT should ask ChatGPT to perform the changes described in (3). It then uses the ChatGPT API to request ChatGPT to perform the changes. 41 | 7. ASSISTANT waits for the results and applies the changes to the local repository. 42 | 8. ASSISTANT runs a local testsuite by running ‘make tests’. 43 | 9. If all tests pass, ASSISTANT commits the changes to the local branch, pushes it, uses the GitHub API (or CLI) to submit a PR to be reviewed. 44 | 10. If the local tests fails, ASSISTANT adds the test failures to the prompt and re-sends the request, up to 5 times. If after 5 such iterations no test passing can be achieved, it gives up and requests a human to take over. 45 | 46 | I also want the tool to support Golang, in which case we will submit the entire 47 | repo and skip the dependency calculation step. 48 | 49 | -------------------------------------------------------------------------------- /assistant/web/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ASSISTANT Web Interface 7 | 85 | 86 | 87 |
88 |

ASSISTANT

89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 |
111 |
112 | 113 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /assistant/chatgpt/chatgpt.go: -------------------------------------------------------------------------------- 1 | package chatgpt 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "log" 11 | ) 12 | 13 | const openAIEndpoint = "https://api.openai.com/v1/chat/completions" 14 | 15 | type ChatGPTRequest struct { 16 | Model string `json:"model"` 17 | Messages []Message `json:"messages"` 18 | } 19 | 20 | type Message struct { 21 | Role string `json:"role"` 22 | Content string `json:"content"` 23 | } 24 | 25 | type ChatGPTResponse struct { 26 | Choices []struct { 27 | Message Message `json:"message"` 28 | } `json:"choices"` 29 | } 30 | 31 | const systemprompt = `You are an expert C++ and Golang developer assistant. 32 | Please execute the task described below with the following guidelines: 33 | 34 | 1. When replying, please reply with entire .cpp or .hpp files, not just the 35 | changes. 36 | 2. Delimit the files with the following markers: 37 | - Start each file with '/* START OF FILE: $filename */' 38 | - End each file with '/* END OF FILE: $filename */' 39 | 3. If parts of the file are unchanged, do not omit or summarize them. Instead, 40 | include the entire file. *This is extremely important*. 41 | 5. Additionally, include the following: 42 | - A three-word summary of the PR changes in the format "Summary: $summary". 43 | The summary should be a maximum of three words separated by dashes, and 44 | not include any other punctuation or special characters. 45 | - A one-line commit message in the format "Commit-Message: $message" 46 | 6. Absolutely do not remove comments. It is OK to suggest improvements to 47 | comments. 48 | 7. Ensure that you never return two copies of the same file, each file should 49 | only be present once. 50 | 51 | Please ensure your replies strictly adhere to these rules to avoid ambiguity 52 | and issues in creating PRs out of your changes. This is very important. 53 | ` 54 | 55 | // CreateRequest prepares the prompt request for ChatGPT 56 | func CreateRequest(prompt string) ChatGPTRequest { 57 | return ChatGPTRequest{ 58 | Model: "gpt-4o-mini", 59 | Messages: []Message{ 60 | {Role: "system", Content: systemprompt}, 61 | {Role: "user", Content: prompt}, 62 | }, 63 | } 64 | } 65 | 66 | // SendRequest sends the prompt to ChatGPT and retrieves the response 67 | func SendRequest(request ChatGPTRequest) (string, error) { 68 | apiKey := os.Getenv("OPENAI_API_KEY") 69 | if apiKey == "" { 70 | log.Printf("OPENAI_API_KEY environment variable is not set") 71 | return "", fmt.Errorf("OPENAI_API_KEY environment variable is not set") 72 | } 73 | 74 | requestBody, err := json.Marshal(request) 75 | if err != nil { 76 | return "", err 77 | } 78 | 79 | req, err := http.NewRequest("POST", openAIEndpoint, bytes.NewBuffer(requestBody)) 80 | if err != nil { 81 | return "", err 82 | } 83 | log.Printf("Request: %v", req) 84 | req.Header.Set("Authorization", "Bearer "+apiKey) 85 | req.Header.Set("Content-Type", "application/json") 86 | 87 | // Log the request for debugging. 88 | fmt.Println("ChatGPT request:", string(requestBody)) 89 | client := &http.Client{} 90 | resp, err := client.Do(req) 91 | if err != nil { 92 | return "", err 93 | } 94 | defer resp.Body.Close() 95 | 96 | if resp.StatusCode != http.StatusOK { 97 | body, _ := ioutil.ReadAll(resp.Body) 98 | return "", fmt.Errorf("ChatGPT API error: %s", string(body)) 99 | } 100 | 101 | var chatResponse ChatGPTResponse 102 | err = json.NewDecoder(resp.Body).Decode(&chatResponse) 103 | if err != nil { 104 | log.Printf("Failed to decode response: %v", err) 105 | return "", err 106 | } 107 | log.Printf("ChatGPT response: %v", chatResponse) 108 | 109 | if len(chatResponse.Choices) > 0 { 110 | return chatResponse.Choices[0].Message.Content, nil 111 | } 112 | 113 | return "", fmt.Errorf("no response from ChatGPT") 114 | } 115 | 116 | -------------------------------------------------------------------------------- /assistant/assistant/parseresponse.go: -------------------------------------------------------------------------------- 1 | package assistant 2 | 3 | import ( 4 | "regexp" 5 | "io/ioutil" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | // parseResponseForFiles extracts the content for each file and a summary string from the response. 11 | // It returns a map of file paths and their contents, the extracted summary string, and a boolean indicating success. 12 | func parseResponseForFiles(response string) (map[string]string, string, bool) { 13 | filesContent := make(map[string]string) 14 | 15 | // Regex to match the START and END delimiters with file paths, ensuring they 16 | //are surrounded by newlines 17 | startRegex := regexp.MustCompile(`(?m)^\s*/\* START OF FILE: (.*?) \*/\s*$`) 18 | endRegex := regexp.MustCompile(`(?m)^\s*/\* END OF FILE: .*? \*/\s*$`) 19 | 20 | // Regex to match "Summary: $summary", where $summary contains only alphanumeric characters and dashes 21 | summaryRegex := regexp.MustCompile(`Summary: ([a-zA-Z0-9-]+)`) 22 | summaryMatch := summaryRegex.FindStringSubmatch(response) 23 | var summary string 24 | if len(summaryMatch) > 1 { 25 | summary = summaryMatch[1] 26 | } else { 27 | return nil, "", false // No summary found 28 | } 29 | 30 | // Find all start matches and iterate over them 31 | startMatches := startRegex.FindAllStringSubmatchIndex(response, -1) 32 | if len(startMatches) == 0 { 33 | return nil, "", false // No files found 34 | } 35 | 36 | for _, startMatch := range startMatches { 37 | end := startMatch[1] 38 | filename := response[startMatch[2]:startMatch[3]] // Extract filename from capture group 39 | 40 | // Find the corresponding end delimiter starting from the end of the start delimiter 41 | endMatch := endRegex.FindStringIndex(response[end:]) 42 | if endMatch == nil { 43 | continue // If there's no matching END delimiter, skip this file 44 | } 45 | 46 | // Calculate actual end position in the original string 47 | contentStart := end 48 | contentEnd := end + endMatch[0] 49 | content := strings.TrimSpace(response[contentStart:contentEnd]) 50 | 51 | // Store the filename and its content in the map 52 | filesContent[filename] = content 53 | } 54 | 55 | return filesContent, summary, true 56 | } 57 | 58 | 59 | func spliceFileWithOriginal(filePath, newContent string) (string, error) { 60 | // Read the original file from the repository 61 | originalContentBytes, err := ioutil.ReadFile(filePath) 62 | if err != nil { 63 | return "", fmt.Errorf("failed to read original file %s: %v", filePath, err) 64 | } 65 | originalContent := string(originalContentBytes) 66 | 67 | // Split the response into sections before and after the placeholder 68 | parts := strings.Split(newContent, "// ... (other functions remain unchanged)") 69 | if len(parts) != 2 { 70 | return "", fmt.Errorf("unexpected content format, placeholder not properly split in %s", filePath) 71 | } 72 | 73 | beforePlaceholder := parts[0] 74 | afterPlaceholder := parts[1] 75 | 76 | // Extract the last few lines of "beforePlaceholder" 77 | beforeLines := strings.Split(beforePlaceholder, "\n") 78 | linesToMatch := 5 79 | if len(beforeLines) < linesToMatch { 80 | linesToMatch = len(beforeLines) 81 | } 82 | lastFewLines := strings.Join(beforeLines[len(beforeLines)-linesToMatch:], "\n") 83 | 84 | // Search for the last few lines in the original file 85 | beforeIndex := strings.LastIndex(originalContent, lastFewLines) 86 | if beforeIndex == -1 { 87 | return "", fmt.Errorf("could not find matching section for 'before' in original file %s", filePath) 88 | } 89 | 90 | // Extract the content from the original file after the placeholder 91 | afterIndex := strings.Index(originalContent[beforeIndex:], afterPlaceholder) 92 | if afterIndex == -1 { 93 | return "", fmt.Errorf("could not find matching section for 'after' in original file %s", filePath) 94 | } 95 | 96 | // Splice the sections together 97 | splicedContent := originalContent[:beforeIndex] + beforePlaceholder + originalContent[beforeIndex+afterIndex:] + afterPlaceholder 98 | 99 | return splicedContent, nil 100 | } 101 | 102 | 103 | -------------------------------------------------------------------------------- /assistant/assistant/githelpers.go: -------------------------------------------------------------------------------- 1 | package assistant 2 | 3 | import ( 4 | "time" 5 | "fmt" 6 | "os/exec" 7 | "log" 8 | "bytes" 9 | "os" 10 | 11 | "github.com/thomasdullien/coding-assistant/assistant/types" 12 | ) 13 | 14 | 15 | 16 | // It takes the summary as an argument and renames the local branch. 17 | func renameBranch(summary string) error { 18 | // Append timestamp to the branch name to avoid collision 19 | timestamp := time.Now().Format("20060102150405") // Format: YYYYMMDDHHMMSS 20 | newBranchName := fmt.Sprintf("assistant-%s-%s", summary, timestamp) 21 | 22 | // Run git command to rename the branch 23 | cmd := exec.Command("git", "branch", 24 | "-m", "assistant-branch", newBranchName) 25 | cmd.Dir = "repo" 26 | 27 | // Capture stdout and stderr 28 | var outBuf, errBuf bytes.Buffer 29 | cmd.Stdout = &outBuf 30 | cmd.Stderr = &errBuf 31 | 32 | // Execute the command 33 | err := cmd.Run() 34 | if err != nil { 35 | // Log the output and error if the command fails 36 | log.Printf("Failed to rename branch. Stdout: %s, Stderr: %s", outBuf.String(), errBuf.String()) 37 | return fmt.Errorf("failed to rename branch: %v", err) 38 | } 39 | 40 | // Log the successful output 41 | log.Printf("Branch renamed successfully. Stdout: %s", outBuf.String()) 42 | return nil 43 | } 44 | 45 | func cloneAndCheckoutRepo(data *types.FormData) error { 46 | // Remove the existing "repo" directory if it exists 47 | if _, err := os.Stat("repo"); err == nil { 48 | err = os.RemoveAll("repo") 49 | if err != nil { 50 | return fmt.Errorf("failed to remove existing repo directory: %v", err) 51 | } 52 | } 53 | 54 | // Prepare the git clone command 55 | cmd := exec.Command("git", "clone", data.RepoURL, "repo") 56 | 57 | // Capture stdout and stderr 58 | var outBuf, errBuf bytes.Buffer 59 | cmd.Stdout = &outBuf 60 | cmd.Stderr = &errBuf 61 | 62 | // Run the command 63 | err := cmd.Run() 64 | if err != nil { 65 | return fmt.Errorf("git clone failed: %v\nstdout: %s\nstderr: %s", err, outBuf.String(), errBuf.String()) 66 | } 67 | 68 | // Checkout the new branch 69 | cmd = exec.Command("git", "checkout", "-b", data.Branch) 70 | cmd.Dir = "repo" 71 | cmd.Stdout = &outBuf 72 | cmd.Stderr = &errBuf 73 | err = cmd.Run() 74 | if err != nil { 75 | return fmt.Errorf("git checkout failed: %v\nstdout: %s\nstderr: %s", err, outBuf.String(), errBuf.String()) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | // commitAndPush stages changes, commits them, and pushes to the remote repository. 82 | // Logs detailed output in case of errors for each command. 83 | func commitAndPush(data *types.FormData) error { 84 | // Run `git add .` to stage all changes 85 | addCmd := exec.Command("git", "add", ".") 86 | var addOutBuf, addErrBuf bytes.Buffer 87 | addCmd.Stdout = &addOutBuf 88 | addCmd.Stderr = &addErrBuf 89 | addCmd.Dir = "repo" 90 | 91 | if err := addCmd.Run(); err != nil { 92 | log.Printf("Failed to add changes. Stdout: %s, Stderr: %s", addOutBuf.String(), addErrBuf.String()) 93 | return fmt.Errorf("failed to add changes: %v", err) 94 | } 95 | 96 | // Run `git commit -m "Applying user prompt changes"` to create a commit 97 | commitCmd := exec.Command("git", "commit", "-m", fmt.Sprintf("Applying changes from user prompt: %s", data.Prompt)) 98 | var commitOutBuf, commitErrBuf bytes.Buffer 99 | commitCmd.Stdout = &commitOutBuf 100 | commitCmd.Stderr = &commitErrBuf 101 | commitCmd.Dir = "repo" 102 | 103 | if err := commitCmd.Run(); err != nil { 104 | log.Printf("Failed to commit changes. Stdout: %s, Stderr: %s", commitOutBuf.String(), commitErrBuf.String()) 105 | return fmt.Errorf("failed to commit changes: %v", err) 106 | } 107 | 108 | // Run `git push -u origin ` to push the changes to the remote branch 109 | log.Println("data.Branch is", data.Branch) 110 | pushCmd := exec.Command("git", "push", "-u", "origin", data.Branch) 111 | var pushOutBuf, pushErrBuf bytes.Buffer 112 | pushCmd.Stdout = &pushOutBuf 113 | pushCmd.Stderr = &pushErrBuf 114 | pushCmd.Dir = "repo" 115 | 116 | if err := pushCmd.Run(); err != nil { 117 | log.Printf("Failed to push changes. Stdout: %s, Stderr: %s", pushOutBuf.String(), pushErrBuf.String()) 118 | return fmt.Errorf("failed to push changes: %v", err) 119 | } 120 | 121 | log.Println("Changes committed and pushed successfully.") 122 | return nil 123 | } 124 | 125 | 126 | -------------------------------------------------------------------------------- /assistant/assistant/assistant.go: -------------------------------------------------------------------------------- 1 | package assistant 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | "bytes" 11 | "bufio" 12 | "path/filepath" 13 | "time" // Import time package 14 | 15 | "github.com/thomasdullien/coding-assistant/assistant/chatgpt" 16 | "github.com/thomasdullien/coding-assistant/assistant/types" 17 | ) 18 | 19 | // ProcessAssistant handles the main workflow 20 | func ProcessAssistant(data types.FormData) (string, error) { 21 | // Clone repository and create branch 22 | log.Println("Cloning repository and creating branch...") 23 | err := cloneAndCheckoutRepo(&data) 24 | if err != nil { 25 | return "", fmt.Errorf("failed to clone repository: %v", err) 26 | } 27 | 28 | var deps []string 29 | // Calculate dependencies for C++ code. 30 | if (data.RepoType == "C++") { 31 | log.Println("Calculating C++ dependencies...") 32 | deps, err = calculateDependencies(data.Files) 33 | for i, dep := range deps { 34 | log.Printf("Dependency %d: %s", i, dep) 35 | } 36 | if err != nil { 37 | return "", fmt.Errorf("failed to calculate dependencies: %v", err) 38 | } 39 | } else if data.RepoType == "Golang" { 40 | // For Golang repositories, include the entire repository 41 | log.Println("Including entire repository for Golang.") 42 | deps, err = includeEntireRepo("repo") 43 | if err != nil { 44 | return "", fmt.Errorf("failed to include entire repository: %v", err) 45 | } 46 | } 47 | 48 | // Prepare prompt 49 | log.Println("Preparing prompt...") 50 | prompt := buildPrompt(data.Prompt, deps) 51 | // Log the prompt for debugging 52 | log.Println("Prompt:", prompt) 53 | 54 | // Query ChatGPT and apply changes iteratively 55 | for attempts := 0; attempts < 2; attempts++ { 56 | log.Printf("Applying changes, attempt %d...", attempts+1) 57 | err := applyChangesWithChatGPT(&data, prompt) 58 | if err != nil { 59 | return "", fmt.Errorf("failed to apply changes: %v", err) 60 | } 61 | 62 | log.Println("Running build...") 63 | builderr, buildout := runTestsOrBuild(data.RepoType, true) 64 | if !builderr { 65 | log.Println("Build successful.") 66 | } else { 67 | prompt += "\nBuild failed, please address the following issues:\n" + buildout 68 | continue 69 | } 70 | 71 | // Run tests and create pull request if successful 72 | log.Println("Running tests...") 73 | // For the moment, assume that Golang tests always pass. This 74 | // needs to change in the future. 75 | testerr, output := runTestsOrBuild(data.RepoType, false) 76 | 77 | if !testerr { 78 | log.Println("Tests passed, creating pull request...") 79 | err1 := commitAndPush(&data) 80 | if err1 != nil { 81 | return "", fmt.Errorf("failed to commit and push changes: %v", err1) 82 | } 83 | log.Println("Changes pushed to branch.") 84 | prlink, err := createPullRequest(&data) 85 | if err != nil { 86 | return "", fmt.Errorf("failed to create pull request: %v", err) 87 | } 88 | log.Printf("Pull request created: %s", prlink) 89 | return prlink, nil 90 | } else { 91 | prompt += "\nTest failed, please address the following issues:\n" + output 92 | } 93 | } 94 | log.Println("Exceeded maximum attempts, please review manually.") 95 | return "", fmt.Errorf("Exceeded maximum attempts to fix the test, please review.") 96 | } 97 | 98 | // applyChangesWithChatGPT sends a prompt to ChatGPT, retrieves the response, and applies any changes 99 | // specified in the response to the relevant files in the local repository. 100 | func applyChangesWithChatGPT(data *types.FormData, prompt string) error { 101 | // Create a ChatGPT request with the initial prompt 102 | request := chatgpt.CreateRequest(prompt) 103 | 104 | // Send the request to ChatGPT and get a response 105 | response, err := chatgpt.SendRequest(request) 106 | if err != nil { 107 | return fmt.Errorf("failed to get response from ChatGPT: %v", err) 108 | } 109 | 110 | // Parse the response to extract file contents based on delimiters 111 | filesContent, summary, success := parseResponseForFiles(response) 112 | if !success { 113 | return fmt.Errorf("failed to parse files from ChatGPT response") 114 | } 115 | 116 | if success { 117 | err := renameBranch(summary) 118 | data.Branch = fmt.Sprintf("assistant-%s-%s", summary, time.Now().Format("20060102150405")) // Update branch name with timestamp 119 | if err != nil { 120 | log.Fatalf("Error renaming branch: %v", err) 121 | } 122 | } 123 | 124 | // Loop through each file path and content pair 125 | for filePath, newContent := range filesContent { 126 | if strings.Contains(newContent, "\n// ... remaining functions unchanged") { 127 | // Handle splicing 128 | log.Printf("Detected placeholder in %s, splicing content...", filePath) 129 | updatedContent, spliceErr := spliceFileWithOriginal(filePath, newContent) 130 | if spliceErr != nil { 131 | return fmt.Errorf("failed to splice file %s: %v", filePath, spliceErr) 132 | } 133 | newContent = updatedContent 134 | } 135 | 136 | // Write the updated content to the file 137 | err := ioutil.WriteFile(filePath, []byte(newContent), 0644) 138 | if err != nil { 139 | log.Printf("failed to write changes to file %s: %v", filePath, err) 140 | continue 141 | } 142 | log.Printf("Successfully applied changes to %s", filePath) 143 | } 144 | return nil 145 | } 146 | 147 | // calculateDependencies runs `gcc -M` on the input files and parses the output to extract dependencies. 148 | func calculateDependencies(files []string) ([]string, error) { 149 | // Prepend "repo/" to each file in the files slice 150 | for i, file := range files { 151 | files[i] = "repo/" + file 152 | } 153 | 154 | // Prepare the gcc command with the -M flag and the input files 155 | args := append([]string{"-M"}, files...) 156 | cmd := exec.Command("gcc", args...) 157 | 158 | // Capture stdout and stderr 159 | var outBuf, errBuf bytes.Buffer 160 | cmd.Stdout = &outBuf 161 | cmd.Stderr = &errBuf 162 | 163 | // Run the command 164 | err := cmd.Run() 165 | if err != nil { 166 | return nil, fmt.Errorf("gcc -M failed: %v\nstderr: %s", err, errBuf.String()) 167 | } 168 | 169 | // Parse the output, filtering out lines that end with ':' or don't contain "repo" 170 | scanner := bufio.NewScanner(&outBuf) 171 | var dependencies []string 172 | 173 | for scanner.Scan() { 174 | line := scanner.Text() 175 | // Split the line by spaces to handle the output format 176 | parts := strings.Fields(line) 177 | 178 | for _, part := range parts { 179 | // Remove any trailing commas or backslashes that `gcc -M` might include 180 | part = strings.TrimSuffix(part, ",") 181 | part = strings.TrimSuffix(part, "\\") 182 | 183 | // Filter out any strings that end with ':' or don't contain "repo" 184 | if !strings.HasSuffix(part, ":") && strings.Contains(part, "repo") { 185 | dependencies = append(dependencies, part) 186 | } 187 | } 188 | } 189 | 190 | if err := scanner.Err(); err != nil { 191 | return nil, fmt.Errorf("error reading gcc -M output: %v", err) 192 | } 193 | 194 | return dependencies, nil 195 | } 196 | 197 | func runTestsOrBuild(repoType string, isBuild bool) (bool, string) { 198 | var cmd *exec.Cmd 199 | var action string 200 | if isBuild { 201 | action = "Build" 202 | } else { 203 | action = "Test" 204 | } 205 | 206 | if repoType == "C++" && isBuild { 207 | cmd = exec.Command("make", "build") 208 | } else if repoType == "C++" && !isBuild { 209 | cmd = exec.Command("make", "tests") 210 | } else if repoType == "Golang" && isBuild { 211 | cmd = exec.Command("go", "build", "-o", "build-out-executable", ".") 212 | } else if repoType == "Golang" && !isBuild { 213 | cmd = exec.Command("go", "test", "./...") 214 | } else { 215 | return false, "Unknown repository type" 216 | } 217 | // Set the working directory to "repo" 218 | cmd.Dir = "repo" 219 | 220 | // Capture stdout and stderr 221 | var outBuf, errBuf bytes.Buffer 222 | cmd.Stdout = &outBuf 223 | cmd.Stderr = &errBuf 224 | 225 | // Run the command 226 | err := cmd.Run() 227 | 228 | // Combine stdout and stderr for logging or further prompting 229 | output := outBuf.String() + "\n" + errBuf.String() 230 | 231 | if err != nil { 232 | // Log the failure and output 233 | log.Printf("%s failed. Output:\n%s", action, output) 234 | return false, output 235 | } 236 | 237 | // Log success and return 238 | log.Printf("%s passed successfully.", action) 239 | os.Remove("build-out-executable") 240 | return true, output 241 | } 242 | 243 | func includeEntireRepo(repoPath string) ([]string, error) { 244 | var files []string 245 | err := filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error { 246 | if err != nil { 247 | return err 248 | } 249 | if !info.IsDir() && strings.HasSuffix(path, ".go") { 250 | files = append(files, path) 251 | } 252 | return nil 253 | }) 254 | if err != nil { 255 | return nil, fmt.Errorf("failed to walk repository: %v", err) 256 | } 257 | return files, nil 258 | } 259 | 260 | // buildPrompt generates a prompt that includes the user's request and the contents of each dependency file. 261 | func buildPrompt(userPrompt string, deps []string) string { 262 | var builder strings.Builder 263 | 264 | // Start with the user prompt 265 | builder.WriteString(userPrompt) 266 | builder.WriteString("\n\nDependencies:\n") 267 | 268 | // Loop through each dependency file 269 | for _, dep := range deps { 270 | // Add start delimiter 271 | builder.WriteString(fmt.Sprintf("\n/* START OF FILE: %s */\n", dep)) 272 | 273 | // Read the content of the dependency file 274 | content, err := ioutil.ReadFile(dep) 275 | if err != nil { 276 | builder.WriteString(fmt.Sprintf("Error reading file: %s\n", err)) 277 | } else { 278 | builder.Write(content) 279 | } 280 | 281 | // Add end delimiter 282 | builder.WriteString(fmt.Sprintf("\n/* END OF FILE: %s */\n\n", dep)) 283 | } 284 | 285 | return builder.String() 286 | } 287 | --------------------------------------------------------------------------------