2023-04-22 14:39:54 -04:00
|
|
|
package pullpal
|
|
|
|
|
|
|
|
import (
|
2023-04-22 16:23:56 -04:00
|
|
|
"context"
|
2023-04-22 17:50:57 -04:00
|
|
|
"errors"
|
2023-04-25 20:32:08 -04:00
|
|
|
"fmt"
|
2023-04-22 17:50:57 -04:00
|
|
|
"io/ioutil"
|
|
|
|
"strings"
|
2023-04-22 16:23:56 -04:00
|
|
|
|
2023-04-22 17:50:57 -04:00
|
|
|
"github.com/mobyvb/pull-pal/llm"
|
2023-04-22 16:23:56 -04:00
|
|
|
"github.com/mobyvb/pull-pal/vc"
|
|
|
|
|
2023-04-22 21:56:52 -04:00
|
|
|
"github.com/atotto/clipboard"
|
2023-04-22 16:23:56 -04:00
|
|
|
"go.uber.org/zap"
|
2023-04-22 14:39:54 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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 {
|
2023-04-22 16:23:56 -04:00
|
|
|
ctx context.Context
|
|
|
|
log *zap.Logger
|
2023-04-22 14:39:54 -04:00
|
|
|
|
2023-04-25 20:32:08 -04:00
|
|
|
vcClient vc.VCClient
|
|
|
|
localGitClient *vc.LocalGitClient
|
|
|
|
openAIClient *llm.OpenAIClient
|
2023-04-22 14:39:54 -04:00
|
|
|
}
|
|
|
|
|
2023-04-22 17:50:57 -04:00
|
|
|
// NewPullPal creates a new "pull pal service", including setting up local version control and LLM integrations.
|
2023-04-25 20:32:08 -04:00
|
|
|
func NewPullPal(ctx context.Context, log *zap.Logger, self vc.Author, repo vc.Repository, openAIToken string) (*PullPal, error) {
|
2023-04-22 16:23:56 -04:00
|
|
|
ghClient, err := vc.NewGithubClient(ctx, log, self, repo)
|
2023-04-22 14:39:54 -04:00
|
|
|
if err != nil {
|
2023-04-22 21:41:28 -04:00
|
|
|
return nil, err
|
2023-04-22 14:39:54 -04:00
|
|
|
}
|
2023-04-25 20:32:08 -04:00
|
|
|
localGitClient, err := vc.NewLocalGitClient(self, repo)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-04-22 14:39:54 -04:00
|
|
|
|
2023-04-22 16:23:56 -04:00
|
|
|
return &PullPal{
|
|
|
|
ctx: ctx,
|
|
|
|
log: log,
|
2023-04-22 14:39:54 -04:00
|
|
|
|
2023-04-25 20:32:08 -04:00
|
|
|
vcClient: ghClient,
|
|
|
|
localGitClient: localGitClient,
|
|
|
|
openAIClient: llm.NewOpenAIClient(openAIToken),
|
2023-04-22 16:23:56 -04:00
|
|
|
}, nil
|
2023-04-22 14:39:54 -04:00
|
|
|
}
|
2023-04-22 17:50:57 -04:00
|
|
|
|
|
|
|
// IssueNotFound is returned when no issue can be found to generate a prompt for.
|
|
|
|
var IssueNotFound = errors.New("no issue found")
|
|
|
|
|
2023-04-22 17:58:31 -04:00
|
|
|
// PickIssueToFile is the same as PickIssue, but the changeRequest is converted to a string and written to a file.
|
2023-04-24 19:49:10 -04:00
|
|
|
func (p *PullPal) PickIssueToFile(listIssueOptions vc.ListIssueOptions, promptPath string) (issue vc.Issue, changeRequest llm.CodeChangeRequest, err error) {
|
|
|
|
issue, changeRequest, err = p.PickIssue(listIssueOptions)
|
2023-04-22 17:58:31 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-04-22 21:56:52 -04:00
|
|
|
// PickIssueToClipboard is the same as PickIssue, but the changeRequest is converted to a string and copied to the clipboard.
|
2023-04-24 19:49:10 -04:00
|
|
|
func (p *PullPal) PickIssueToClipboard(listIssueOptions vc.ListIssueOptions) (issue vc.Issue, changeRequest llm.CodeChangeRequest, err error) {
|
|
|
|
issue, changeRequest, err = p.PickIssue(listIssueOptions)
|
2023-04-22 21:56:52 -04:00
|
|
|
if err != nil {
|
|
|
|
return issue, changeRequest, err
|
|
|
|
}
|
|
|
|
|
|
|
|
prompt, err := changeRequest.GetPrompt()
|
|
|
|
if err != nil {
|
|
|
|
return issue, changeRequest, err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = clipboard.WriteAll(prompt)
|
|
|
|
return issue, changeRequest, err
|
|
|
|
}
|
|
|
|
|
2023-04-22 17:50:57 -04:00
|
|
|
// 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.
|
2023-04-24 19:49:10 -04:00
|
|
|
func (p *PullPal) PickIssue(listIssueOptions vc.ListIssueOptions) (issue vc.Issue, changeRequest llm.CodeChangeRequest, err error) {
|
2023-04-22 17:50:57 -04:00
|
|
|
// TODO I should be able to pass in settings for listing issues from here
|
2023-04-24 19:49:10 -04:00
|
|
|
issues, err := p.vcClient.ListOpenIssues(listIssueOptions)
|
2023-04-22 17:50:57 -04:00
|
|
|
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
|
|
|
|
}
|
2023-04-24 19:49:10 -04:00
|
|
|
|
|
|
|
// ListIssues gets a list of all issues meeting the provided criteria.
|
|
|
|
func (p *PullPal) ListIssues(handles, labels []string) ([]vc.Issue, error) {
|
|
|
|
issues, err := p.vcClient.ListOpenIssues(vc.ListIssueOptions{
|
|
|
|
Handles: handles,
|
|
|
|
Labels: labels,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return issues, nil
|
|
|
|
}
|
2023-04-24 20:28:27 -04:00
|
|
|
|
|
|
|
// ListComments gets a list of all comments meeting the provided criteria on a PR.
|
|
|
|
func (p *PullPal) ListComments(changeID string, handles []string) ([]vc.Comment, error) {
|
|
|
|
comments, err := p.vcClient.ListOpenComments(vc.ListCommentOptions{
|
|
|
|
ChangeID: changeID,
|
|
|
|
Handles: handles,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return comments, nil
|
|
|
|
}
|
2023-04-25 20:32:08 -04:00
|
|
|
|
|
|
|
func (p *PullPal) MakeLocalChange(issue vc.Issue) error {
|
|
|
|
// 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 err
|
|
|
|
}
|
|
|
|
files = append(files, nextFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
changeRequest := llm.CodeChangeRequest{
|
|
|
|
Subject: issue.Subject,
|
|
|
|
Body: issue.Body,
|
|
|
|
IssueID: issue.ID,
|
|
|
|
Files: files,
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := p.openAIClient.EvaluateCCR(p.ctx, changeRequest)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Println("response from openai")
|
|
|
|
fmt.Println(res)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|