From c9cf1b5e49f2b047b4f3488dc4fa1c45d7cfcd79 Mon Sep 17 00:00:00 2001 From: Moby von Briesen Date: Sat, 22 Apr 2023 17:50:57 -0400 Subject: [PATCH] implement high-level functionality integrate the llm and versioncontrol packages in the pullpal package * Functionality to select a Github issue, and generate an LLM prompt from it (PickIssue) * Functionality to parse LLM response (from string or file), update local git repository, and create pull request (ProcessResponse and ProcessResponseFromFile) --- {template => llm}/code-change-request.tmpl | 0 llm/common.go | 2 +- pullpal/common.go | 88 ++++++++++++++++++++++ vc/github.go | 12 +-- 4 files changed, 95 insertions(+), 7 deletions(-) rename {template => llm}/code-change-request.tmpl (100%) diff --git a/template/code-change-request.tmpl b/llm/code-change-request.tmpl similarity index 100% rename from template/code-change-request.tmpl rename to llm/code-change-request.tmpl diff --git a/llm/common.go b/llm/common.go index e1f8cd1..c97f5a7 100644 --- a/llm/common.go +++ b/llm/common.go @@ -37,7 +37,7 @@ func (req CodeChangeRequest) MustGetPrompt() string { // GetPrompt converts the information in the request to a prompt for an LLM. func (req CodeChangeRequest) GetPrompt() (string, error) { - tmpl, err := template.ParseFiles("../template/code-change-request.tmpl") + tmpl, err := template.ParseFiles("./code-change-request.tmpl") if err != nil { return "", err } diff --git a/pullpal/common.go b/pullpal/common.go index 13bae9e..56d852b 100644 --- a/pullpal/common.go +++ b/pullpal/common.go @@ -2,7 +2,11 @@ package pullpal import ( "context" + "errors" + "io/ioutil" + "strings" + "github.com/mobyvb/pull-pal/llm" "github.com/mobyvb/pull-pal/vc" "go.uber.org/zap" @@ -20,6 +24,7 @@ type PullPal struct { vcClient vc.VCClient } +// NewPullPal creates a new "pull pal service", including setting up local version control and LLM integrations. func NewPullPal(ctx context.Context, log *zap.Logger, self vc.Author, repo vc.Repository) (*PullPal, error) { ghClient, err := vc.NewGithubClient(ctx, log, self, repo) if err != nil { @@ -33,3 +38,86 @@ func NewPullPal(ctx context.Context, log *zap.Logger, self vc.Author, repo vc.Re vcClient: ghClient, }, nil } + +// IssueNotFound is returned when no issue can be found to generate a prompt for. +var IssueNotFound = errors.New("no issue found") + +// PickIssue selects an issue from the version control server and returns the selected issue, as well as the LLM prompt needed to fulfill the request. +func (p *PullPal) PickIssue() (issue vc.Issue, changeRequest llm.CodeChangeRequest, err error) { + // TODO I should be able to pass in settings for listing issues from here + issues, err := p.vcClient.ListOpenIssues() + if err != nil { + return issue, changeRequest, err + } + + if len(issues) == 0 { + return issue, changeRequest, IssueNotFound + } + + issue = issues[0] + + // remove file list from issue body + // TODO do this better + parts := strings.Split(issue.Body, "Files:") + issue.Body = parts[0] + + fileList := []string{} + if len(parts) > 1 { + fileList = strings.Split(parts[1], ",") + } + + // get file contents from local git repository + files := []llm.File{} + for _, path := range fileList { + path = strings.TrimSpace(path) + nextFile, err := p.vcClient.GetLocalFile(path) + if err != nil { + return issue, changeRequest, err + } + files = append(files, nextFile) + } + + changeRequest.Subject = issue.Subject + changeRequest.Body = issue.Body + changeRequest.IssueID = issue.ID + changeRequest.Files = files + + return issue, changeRequest, nil +} + +// ProcessResponseFromFile is the same as ProcessResponse, but the response is inputted into a file rather than passed directly as an argument. +func (p *PullPal) ProcessResponseFromFile(codeChangeRequest llm.CodeChangeRequest, llmResponsePath string) (url string, err error) { + data, err := ioutil.ReadFile(llmResponsePath) + if err != nil { + return "", err + } + return p.ProcessResponse(codeChangeRequest, string(data)) +} + +// ProcessResponse parses the llm response, updates files in the local git repo accordingly, and opens a new code change request (e.g. Github PR). +func (p *PullPal) ProcessResponse(codeChangeRequest llm.CodeChangeRequest, llmResponse string) (url string, err error) { + // 1. parse llm response + codeChangeResponse := llm.ParseCodeChangeResponse(llmResponse) + + // 2. create commit with file changes + err = p.vcClient.StartCommit() + if err != nil { + return "", err + } + for _, f := range codeChangeResponse.Files { + err = p.vcClient.ReplaceOrAddLocalFile(f) + if err != nil { + return "", err + } + } + + commitMessage := codeChangeRequest.Subject + "\n\n" + codeChangeResponse.Notes + "\n\nResolves: #" + codeChangeRequest.IssueID + err = p.vcClient.FinishCommit(commitMessage) + if err != nil { + return "", err + } + + // 3. open code change request + _, url, err = p.vcClient.OpenCodeChangeRequest(codeChangeRequest, codeChangeResponse) + return url, err +} diff --git a/vc/github.go b/vc/github.go index 3fb5662..b47207b 100644 --- a/vc/github.go +++ b/vc/github.go @@ -83,7 +83,6 @@ func NewGithubClient(ctx context.Context, log *zap.Logger, self Author, repo Rep // OpenCodeChangeRequest pushes to a new remote branch and opens a PR on Github. func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse) (id, url string, err error) { // TODO handle gc.ctx canceled - gc.log.Debug("Creating a new pull request...") title := req.Subject branchName := randomBranchName() @@ -94,7 +93,6 @@ func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm body += fmt.Sprintf("\n\nResolves #%s", req.IssueID) issue, err := strconv.Atoi(req.IssueID) if err != nil { - gc.log.Error("Failed to parse issue ID from code change request as integer", zap.String("provided issue ID", req.IssueID), zap.Error(err)) return "", "", err } @@ -144,13 +142,11 @@ func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm Issue: &issue, }) if err != nil { - gc.log.Error("Failed to create pull request", zap.Error(err)) return "", "", err } url = pr.GetHTMLURL() id = strconv.Itoa(int(pr.GetID())) - gc.log.Info("Successfully created pull request.", zap.String("ID", id), zap.String("URL", url)) return id, url, nil } @@ -166,12 +162,16 @@ func (gc *GithubClient) ListOpenIssues() ([]Issue, error) { // List and parse GitHub issues issues, _, err := gc.client.Issues.ListByRepo(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, nil) if err != nil { - gc.log.Error("Failed to list issues", zap.Error(err)) return nil, err } - toReturn := make([]Issue, len(issues)) + toReturn := []Issue{} for _, issue := range issues { + // TODO make this filtering configurable from outside + if issue.GetUser().GetLogin() != gc.repo.Owner.Handle { + continue + } + nextIssue := Issue{ ID: strconv.Itoa(int(issue.GetID())), Subject: issue.GetTitle(),