├── .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 |
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 |
--------------------------------------------------------------------------------