pull-pal/pullpal/common.go

140 lines
4.0 KiB
Go

package pullpal
import (
"context"
"errors"
"io/ioutil"
"strings"
"github.com/mobyvb/pull-pal/llm"
"github.com/mobyvb/pull-pal/vc"
"go.uber.org/zap"
)
// PullPal is the service responsible for:
// * Interacting with git server (e.g. reading issues and making PRs on Github)
// * Generating LLM prompts
// * Parsing LLM responses
// * Interacting with LLM (e.g. with GPT via OpenAI API)
type PullPal struct {
ctx context.Context
log *zap.Logger
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 {
return nil, err
}
return &PullPal{
ctx: ctx,
log: log,
vcClient: ghClient,
}, nil
}
// IssueNotFound is returned when no issue can be found to generate a prompt for.
var IssueNotFound = errors.New("no issue found")
// PickIssueToFile is the same as PickIssue, but the changeRequest is converted to a string and written to a file.
func (p *PullPal) PickIssueToFile(promptPath string) (issue vc.Issue, changeRequest llm.CodeChangeRequest, err error) {
issue, changeRequest, err = p.PickIssue()
if err != nil {
return issue, changeRequest, err
}
prompt, err := changeRequest.GetPrompt()
if err != nil {
return issue, changeRequest, err
}
err = ioutil.WriteFile(promptPath, []byte(prompt), 0644)
return issue, changeRequest, err
}
// 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
}