mirror of
https://github.com/Pull-Pal/pull-pal.git
synced 2024-09-07 20:45:16 -04:00
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)
This commit is contained in:
parent
3aa79401d0
commit
c9cf1b5e49
@ -37,7 +37,7 @@ func (req CodeChangeRequest) MustGetPrompt() string {
|
|||||||
|
|
||||||
// GetPrompt converts the information in the request to a prompt for an LLM.
|
// GetPrompt converts the information in the request to a prompt for an LLM.
|
||||||
func (req CodeChangeRequest) GetPrompt() (string, error) {
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,11 @@ package pullpal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mobyvb/pull-pal/llm"
|
||||||
"github.com/mobyvb/pull-pal/vc"
|
"github.com/mobyvb/pull-pal/vc"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -20,6 +24,7 @@ type PullPal struct {
|
|||||||
vcClient vc.VCClient
|
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) {
|
func NewPullPal(ctx context.Context, log *zap.Logger, self vc.Author, repo vc.Repository) (*PullPal, error) {
|
||||||
ghClient, err := vc.NewGithubClient(ctx, log, self, repo)
|
ghClient, err := vc.NewGithubClient(ctx, log, self, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -33,3 +38,86 @@ func NewPullPal(ctx context.Context, log *zap.Logger, self vc.Author, repo vc.Re
|
|||||||
vcClient: ghClient,
|
vcClient: ghClient,
|
||||||
}, nil
|
}, 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
|
||||||
|
}
|
||||||
|
12
vc/github.go
12
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.
|
// 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) {
|
func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm.CodeChangeResponse) (id, url string, err error) {
|
||||||
// TODO handle gc.ctx canceled
|
// TODO handle gc.ctx canceled
|
||||||
gc.log.Debug("Creating a new pull request...")
|
|
||||||
|
|
||||||
title := req.Subject
|
title := req.Subject
|
||||||
branchName := randomBranchName()
|
branchName := randomBranchName()
|
||||||
@ -94,7 +93,6 @@ func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm
|
|||||||
body += fmt.Sprintf("\n\nResolves #%s", req.IssueID)
|
body += fmt.Sprintf("\n\nResolves #%s", req.IssueID)
|
||||||
issue, err := strconv.Atoi(req.IssueID)
|
issue, err := strconv.Atoi(req.IssueID)
|
||||||
if err != nil {
|
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
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,13 +142,11 @@ func (gc *GithubClient) OpenCodeChangeRequest(req llm.CodeChangeRequest, res llm
|
|||||||
Issue: &issue,
|
Issue: &issue,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gc.log.Error("Failed to create pull request", zap.Error(err))
|
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
url = pr.GetHTMLURL()
|
url = pr.GetHTMLURL()
|
||||||
id = strconv.Itoa(int(pr.GetID()))
|
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
|
return id, url, nil
|
||||||
}
|
}
|
||||||
@ -166,12 +162,16 @@ func (gc *GithubClient) ListOpenIssues() ([]Issue, error) {
|
|||||||
// List and parse GitHub issues
|
// List and parse GitHub issues
|
||||||
issues, _, err := gc.client.Issues.ListByRepo(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, nil)
|
issues, _, err := gc.client.Issues.ListByRepo(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gc.log.Error("Failed to list issues", zap.Error(err))
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
toReturn := make([]Issue, len(issues))
|
toReturn := []Issue{}
|
||||||
for _, issue := range issues {
|
for _, issue := range issues {
|
||||||
|
// TODO make this filtering configurable from outside
|
||||||
|
if issue.GetUser().GetLogin() != gc.repo.Owner.Handle {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
nextIssue := Issue{
|
nextIssue := Issue{
|
||||||
ID: strconv.Itoa(int(issue.GetID())),
|
ID: strconv.Itoa(int(issue.GetID())),
|
||||||
Subject: issue.GetTitle(),
|
Subject: issue.GetTitle(),
|
||||||
|
Loading…
Reference in New Issue
Block a user