mirror of
https://github.com/Pull-Pal/pull-pal.git
synced 2025-01-02 15:36:51 -05:00
begin implementing comments (#2)
add a section of the main loop that checks for comments on PRs that the bot has opened reworks git logic and adds debug commands for git and llm stuff changes a bunch of other stuff too
This commit is contained in:
parent
39d0ad7d0b
commit
ab7521477a
54
cmd/debug-git.go
Normal file
54
cmd/debug-git.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var debugGitCmd = &cobra.Command{
|
||||||
|
Use: "debug-git",
|
||||||
|
Short: "debug git functionality",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cfg := getConfig()
|
||||||
|
|
||||||
|
p, err := getPullPal(cmd.Context(), cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("error creating new pull pal", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Successfully initialized pull pal")
|
||||||
|
|
||||||
|
err = p.DebugGit()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("err debugging git", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var debugGithubCmd = &cobra.Command{
|
||||||
|
Use: "debug-github",
|
||||||
|
Short: "debug github functionality",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cfg := getConfig()
|
||||||
|
|
||||||
|
p, err := getPullPal(cmd.Context(), cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("error creating new pull pal", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Successfully initialized pull pal")
|
||||||
|
|
||||||
|
err = p.DebugGithub(cfg.usersToListenTo)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("err debugging github", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(debugGitCmd)
|
||||||
|
rootCmd.AddCommand(debugGithubCmd)
|
||||||
|
}
|
@ -6,9 +6,9 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var debugGitCmd = &cobra.Command{
|
var debugLLMCmd = &cobra.Command{
|
||||||
Use: "debug-git",
|
Use: "debug-llm",
|
||||||
Short: "debug git functionality",
|
Short: "debug llm functionality",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
cfg := getConfig()
|
cfg := getConfig()
|
||||||
|
|
||||||
@ -19,14 +19,14 @@ var debugGitCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
fmt.Println("Successfully initialized pull pal")
|
fmt.Println("Successfully initialized pull pal")
|
||||||
|
|
||||||
err = p.DebugGit()
|
err = p.DebugLLM()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("err debugging git", err)
|
fmt.Println("err debugging prompts", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(debugGitCmd)
|
rootCmd.AddCommand(debugLLMCmd)
|
||||||
}
|
}
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/mobyvb/pull-pal/pullpal"
|
"github.com/mobyvb/pull-pal/pullpal"
|
||||||
"github.com/mobyvb/pull-pal/vc"
|
"github.com/mobyvb/pull-pal/vc"
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -79,7 +80,8 @@ func getPullPal(ctx context.Context, cfg config) (*pullpal.PullPal, error) {
|
|||||||
Handles: cfg.usersToListenTo,
|
Handles: cfg.usersToListenTo,
|
||||||
Labels: cfg.requiredIssueLabels,
|
Labels: cfg.requiredIssueLabels,
|
||||||
}
|
}
|
||||||
p, err := pullpal.NewPullPal(ctx, log.Named("pullpal"), listIssueOptions, author, repo, cfg.openAIToken)
|
// TODO make model configurable
|
||||||
|
p, err := pullpal.NewPullPal(ctx, log.Named("pullpal"), listIssueOptions, author, repo, openai.GPT4, cfg.openAIToken)
|
||||||
|
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package llm
|
package llm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"html/template"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -12,6 +10,13 @@ type File struct {
|
|||||||
Contents string
|
Contents string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResponseType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResponseAnswer ResponseType = iota
|
||||||
|
ResponseCodeChange
|
||||||
|
)
|
||||||
|
|
||||||
// CodeChangeRequest contains all necessary information for generating a prompt for a LLM.
|
// CodeChangeRequest contains all necessary information for generating a prompt for a LLM.
|
||||||
type CodeChangeRequest struct {
|
type CodeChangeRequest struct {
|
||||||
Files []File
|
Files []File
|
||||||
@ -20,75 +25,23 @@ type CodeChangeRequest struct {
|
|||||||
IssueID string
|
IssueID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// String is the string representation of a CodeChangeRequest. Functionally, it contains the LLM prompt.
|
|
||||||
func (req CodeChangeRequest) String() string {
|
|
||||||
prompt := req.MustGetPrompt()
|
|
||||||
return "START OF PROMPT\n" + prompt + "\nEND OF PROMPT"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustGetPrompt only returns the prompt, but panics if the data in the request cannot populate the template.
|
|
||||||
func (req CodeChangeRequest) MustGetPrompt() string {
|
|
||||||
prompt, err := req.GetPrompt()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return prompt
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPrompt converts the information in the request to a prompt for an LLM.
|
|
||||||
func (req CodeChangeRequest) GetPrompt() (string, error) {
|
|
||||||
tmpl, err := template.ParseFiles("./llm/prompts/code-change-request.tmpl")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result bytes.Buffer
|
|
||||||
err = tmpl.Execute(&result, req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CodeChangeResponse contains data derived from an LLM response to a prompt generated via a CodeChangeRequest.
|
// CodeChangeResponse contains data derived from an LLM response to a prompt generated via a CodeChangeRequest.
|
||||||
type CodeChangeResponse struct {
|
type CodeChangeResponse struct {
|
||||||
Files []File
|
Files []File
|
||||||
Notes string
|
Notes string
|
||||||
}
|
}
|
||||||
|
|
||||||
// String is a string representation of CodeChangeResponse.
|
// TODO support threads
|
||||||
func (res CodeChangeResponse) String() string {
|
type DiffCommentRequest struct {
|
||||||
out := "Notes:\n"
|
File File
|
||||||
out += res.Notes + "\n\n"
|
Contents string
|
||||||
out += "Files:\n"
|
Diff string
|
||||||
for _, f := range res.Files {
|
|
||||||
out += f.Path + ":\n```\n"
|
|
||||||
out += f.Contents + "\n```\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseCodeChangeResponse parses the LLM's response to CodeChangeRequest (string) into a CodeChangeResponse.
|
type DiffCommentResponse struct {
|
||||||
func ParseCodeChangeResponse(llmResponse string) CodeChangeResponse {
|
Type ResponseType
|
||||||
sections := strings.Split(llmResponse, "ppnotes:")
|
Answer string
|
||||||
|
File File
|
||||||
filesSection := ""
|
|
||||||
if len(sections) > 0 {
|
|
||||||
filesSection = sections[0]
|
|
||||||
}
|
|
||||||
notes := ""
|
|
||||||
if len(sections) > 1 {
|
|
||||||
notes = strings.TrimSpace(sections[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
files := parseFiles(filesSection)
|
|
||||||
|
|
||||||
return CodeChangeResponse{
|
|
||||||
Files: files,
|
|
||||||
Notes: notes,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseFiles process the "files" subsection of the LLM's response. It is a helper for GetCodeChangeResponse.
|
// parseFiles process the "files" subsection of the LLM's response. It is a helper for GetCodeChangeResponse.
|
||||||
|
90
llm/diffcomment.go
Normal file
90
llm/diffcomment.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package llm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (req DiffCommentRequest) String() string {
|
||||||
|
return req.MustGetPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGetPrompt only returns the prompt, but panics if the data in the request cannot populate the template.
|
||||||
|
func (req DiffCommentRequest) MustGetPrompt() string {
|
||||||
|
prompt, err := req.GetPrompt()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrompt converts the information in the request to a prompt for an LLM.
|
||||||
|
func (req DiffCommentRequest) GetPrompt() (string, error) {
|
||||||
|
tmpl, err := template.ParseFiles("./llm/prompts/comment-diff-request.tmpl")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result bytes.Buffer
|
||||||
|
err = tmpl.Execute(&result, req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String is a string representation of DiffCommentResponse.
|
||||||
|
func (res DiffCommentResponse) String() string {
|
||||||
|
out := ""
|
||||||
|
if res.Type == ResponseAnswer {
|
||||||
|
out += "Type: Answer\n"
|
||||||
|
out += res.Answer
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
out += "Type: Code Change\n"
|
||||||
|
|
||||||
|
out += "Response:\n"
|
||||||
|
out += res.Answer + "\n\n"
|
||||||
|
out += "Files:\n"
|
||||||
|
out += res.File.Path + ":\n```\n"
|
||||||
|
out += res.File.Contents + "\n```\n"
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseDiffCommentResponse(llmResponse string) DiffCommentResponse {
|
||||||
|
llmResponse = strings.TrimSpace(llmResponse)
|
||||||
|
if llmResponse[0] == 'A' {
|
||||||
|
answer := strings.TrimSpace(llmResponse[1:])
|
||||||
|
return DiffCommentResponse{
|
||||||
|
Type: ResponseAnswer,
|
||||||
|
Answer: answer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts := strings.Split(llmResponse, "ppresponse:")
|
||||||
|
|
||||||
|
filesSection := ""
|
||||||
|
if len(parts) > 0 {
|
||||||
|
filesSection = parts[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
answer := ""
|
||||||
|
if len(parts) > 1 {
|
||||||
|
answer = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
files := parseFiles(filesSection)
|
||||||
|
f := File{}
|
||||||
|
if len(files) > 0 {
|
||||||
|
f = files[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return DiffCommentResponse{
|
||||||
|
Type: ResponseCodeChange,
|
||||||
|
Answer: answer,
|
||||||
|
File: f,
|
||||||
|
}
|
||||||
|
}
|
71
llm/issue.go
Normal file
71
llm/issue.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package llm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// String is the string representation of a CodeChangeRequest. Functionally, it contains the LLM prompt.
|
||||||
|
func (req CodeChangeRequest) String() string {
|
||||||
|
return req.MustGetPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustGetPrompt only returns the prompt, but panics if the data in the request cannot populate the template.
|
||||||
|
func (req CodeChangeRequest) MustGetPrompt() string {
|
||||||
|
prompt, err := req.GetPrompt()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPrompt converts the information in the request to a prompt for an LLM.
|
||||||
|
func (req CodeChangeRequest) GetPrompt() (string, error) {
|
||||||
|
tmpl, err := template.ParseFiles("./llm/prompts/code-change-request.tmpl")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result bytes.Buffer
|
||||||
|
err = tmpl.Execute(&result, req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String is a string representation of CodeChangeResponse.
|
||||||
|
func (res CodeChangeResponse) String() string {
|
||||||
|
out := "Notes:\n"
|
||||||
|
out += res.Notes + "\n\n"
|
||||||
|
out += "Files:\n"
|
||||||
|
for _, f := range res.Files {
|
||||||
|
out += f.Path + ":\n```\n"
|
||||||
|
out += f.Contents + "\n```\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCodeChangeResponse parses the LLM's response to CodeChangeRequest (string) into a CodeChangeResponse.
|
||||||
|
func ParseCodeChangeResponse(llmResponse string) CodeChangeResponse {
|
||||||
|
sections := strings.Split(llmResponse, "ppnotes:")
|
||||||
|
|
||||||
|
filesSection := ""
|
||||||
|
if len(sections) > 0 {
|
||||||
|
filesSection = sections[0]
|
||||||
|
}
|
||||||
|
notes := ""
|
||||||
|
if len(sections) > 1 {
|
||||||
|
notes = strings.TrimSpace(sections[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
files := parseFiles(filesSection)
|
||||||
|
|
||||||
|
return CodeChangeResponse{
|
||||||
|
Files: files,
|
||||||
|
Notes: notes,
|
||||||
|
}
|
||||||
|
}
|
@ -8,24 +8,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type OpenAIClient struct {
|
type OpenAIClient struct {
|
||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
client *openai.Client
|
client *openai.Client
|
||||||
|
defaultModel string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOpenAIClient(log *zap.Logger, token string) *OpenAIClient {
|
func NewOpenAIClient(log *zap.Logger, defaultModel, token string) *OpenAIClient {
|
||||||
return &OpenAIClient{
|
return &OpenAIClient{
|
||||||
log: log,
|
log: log,
|
||||||
client: openai.NewClient(token),
|
client: openai.NewClient(token),
|
||||||
|
defaultModel: defaultModel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oc *OpenAIClient) EvaluateCCR(ctx context.Context, req CodeChangeRequest) (res CodeChangeResponse, err error) {
|
func (oc *OpenAIClient) EvaluateCCR(ctx context.Context, model string, req CodeChangeRequest) (res CodeChangeResponse, err error) {
|
||||||
|
if model == "" {
|
||||||
|
model = oc.defaultModel
|
||||||
|
}
|
||||||
resp, err := oc.client.CreateChatCompletion(
|
resp, err := oc.client.CreateChatCompletion(
|
||||||
ctx,
|
ctx,
|
||||||
openai.ChatCompletionRequest{
|
openai.ChatCompletionRequest{
|
||||||
// TODO make model configurable
|
Model: model,
|
||||||
Model: openai.GPT4,
|
|
||||||
//Model: openai.GPT3Dot5Turbo,
|
|
||||||
Messages: []openai.ChatCompletionMessage{
|
Messages: []openai.ChatCompletionMessage{
|
||||||
{
|
{
|
||||||
Role: openai.ChatMessageRoleUser,
|
Role: openai.ChatMessageRoleUser,
|
||||||
@ -46,3 +49,32 @@ func (oc *OpenAIClient) EvaluateCCR(ctx context.Context, req CodeChangeRequest)
|
|||||||
|
|
||||||
return ParseCodeChangeResponse(choice), nil
|
return ParseCodeChangeResponse(choice), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (oc *OpenAIClient) EvaluateDiffComment(ctx context.Context, model string, req DiffCommentRequest) (res DiffCommentResponse, err error) {
|
||||||
|
if model == "" {
|
||||||
|
model = oc.defaultModel
|
||||||
|
}
|
||||||
|
resp, err := oc.client.CreateChatCompletion(
|
||||||
|
ctx,
|
||||||
|
openai.ChatCompletionRequest{
|
||||||
|
Model: model,
|
||||||
|
Messages: []openai.ChatCompletionMessage{
|
||||||
|
{
|
||||||
|
Role: openai.ChatMessageRoleUser,
|
||||||
|
Content: req.String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
oc.log.Error("chat completion error", zap.Error(err))
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
choice := resp.Choices[0].Message.Content
|
||||||
|
|
||||||
|
// TODO make debug log when I figure out how to config that
|
||||||
|
oc.log.Info("got response from llm", zap.String("output", choice))
|
||||||
|
|
||||||
|
return ParseDiffCommentResponse(choice), nil
|
||||||
|
}
|
||||||
|
@ -15,9 +15,9 @@ Body:
|
|||||||
Respond in the exact format:
|
Respond in the exact format:
|
||||||
Files:
|
Files:
|
||||||
{{ range $index, $file := .Files }}
|
{{ range $index, $file := .Files }}
|
||||||
ppname: {{ $file.Path }}
|
ppname: {{ $file.Path }}
|
||||||
ppcontents:
|
ppcontents:
|
||||||
[new {{ $file.Path }} contents]
|
[new {{ $file.Path }} contents]
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
ppnotes:
|
ppnotes:
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
File:
|
File:
|
||||||
- name: {{ .Path }}:
|
- name: {{ .File.Path }}:
|
||||||
contents:
|
contents:
|
||||||
```
|
```
|
||||||
{{ .Contents }}
|
{{ .File.Contents }}
|
||||||
```
|
```
|
||||||
|
|
||||||
Diff:
|
Diff:
|
||||||
{{ .Diff }}
|
{{ .Diff }}
|
||||||
|
|
||||||
Comment:
|
Comment:
|
||||||
{{ .Comment }}
|
{{ .Contents }}
|
||||||
|
|
||||||
The above is information about a comment left on a file. The diff contains information about the precise location of the comment.
|
The above is information about a comment left on a file. The diff contains information about the precise location of the comment.
|
||||||
|
|
||||||
First, determine if the comment is a question or a request for changes.
|
First, determine if the comment is a question or a request for changes.
|
||||||
If the comment is a question, come up with an answer, and respond exactly as outlined in "Response Template A"
|
If the comment is a question, come up with an answer, and respond exactly as outlined directly below "Response Template A"
|
||||||
If the comment is a request, modify the file provided at the beginning of the message, and respond exactly as outlined in "Response Template B".
|
If the comment is a request, modify the file provided at the beginning of the message, and respond exactly as outlined directly below "Response Template B".
|
||||||
|
|
||||||
Response Template A:
|
Response Template A:
|
||||||
Q
|
Q
|
||||||
@ -24,9 +24,9 @@ Q
|
|||||||
Response Template B:
|
Response Template B:
|
||||||
R
|
R
|
||||||
Files:
|
Files:
|
||||||
ppname: {{ .Path }}
|
ppname: {{ .File.Path }}
|
||||||
ppcontents:
|
ppcontents:
|
||||||
[new {{ .Path }} contents]
|
[new {{ .File.Path }} contents]
|
||||||
|
|
||||||
ppresponse:
|
ppresponse:
|
||||||
[additional context about your changes]
|
[additional context about your changes]
|
||||||
|
@ -33,7 +33,7 @@ type PullPal struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewPullPal creates a new "pull pal service", including setting up local version control and LLM integrations.
|
// NewPullPal creates a new "pull pal service", including setting up local version control and LLM integrations.
|
||||||
func NewPullPal(ctx context.Context, log *zap.Logger, listIssueOptions vc.ListIssueOptions, self vc.Author, repo vc.Repository, openAIToken string) (*PullPal, error) {
|
func NewPullPal(ctx context.Context, log *zap.Logger, listIssueOptions vc.ListIssueOptions, self vc.Author, repo vc.Repository, model string, openAIToken string) (*PullPal, error) {
|
||||||
ghClient, err := vc.NewGithubClient(ctx, log, self, repo)
|
ghClient, err := vc.NewGithubClient(ctx, log, self, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -50,7 +50,7 @@ func NewPullPal(ctx context.Context, log *zap.Logger, listIssueOptions vc.ListIs
|
|||||||
|
|
||||||
ghClient: ghClient,
|
ghClient: ghClient,
|
||||||
localGitClient: localGitClient,
|
localGitClient: localGitClient,
|
||||||
openAIClient: llm.NewOpenAIClient(log.Named("openaiClient"), openAIToken),
|
openAIClient: llm.NewOpenAIClient(log.Named("openaiClient"), model, openAIToken),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +59,7 @@ func (p *PullPal) Run() error {
|
|||||||
p.log.Info("Starting Pull Pal")
|
p.log.Info("Starting Pull Pal")
|
||||||
// TODO gracefully handle context cancelation
|
// TODO gracefully handle context cancelation
|
||||||
for {
|
for {
|
||||||
|
p.log.Info("checking github issues...")
|
||||||
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
|
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error listing issues", zap.Error(err))
|
p.log.Error("error listing issues", zap.Error(err))
|
||||||
@ -66,178 +67,66 @@ func (p *PullPal) Run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(issues) == 0 {
|
if len(issues) == 0 {
|
||||||
// todo don't sleep
|
p.log.Info("no issues found")
|
||||||
p.log.Info("no issues found. sleeping for 30 seconds")
|
} else {
|
||||||
time.Sleep(30 * time.Second)
|
p.log.Info("picked issue to process")
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
issue := issues[0]
|
issue := issues[0]
|
||||||
issueNumber, err := strconv.Atoi(issue.ID)
|
err = p.handleIssue(issue)
|
||||||
if err != nil {
|
|
||||||
p.log.Error("error converting issue ID to int", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.ghClient.CommentOnIssue(issueNumber, "on it")
|
|
||||||
if err != nil {
|
|
||||||
p.log.Error("error commenting on issue", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, label := range p.listIssueOptions.Labels {
|
|
||||||
err = p.ghClient.RemoveLabelFromIssue(issueNumber, label)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error removing labels from issue", zap.Error(err))
|
p.log.Error("error handling issue", zap.Error(err))
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove file list from issue body
|
p.log.Info("checking pr comments...")
|
||||||
// TODO do this better and probably somewhere else
|
comments, err := p.ghClient.ListOpenComments(vc.ListCommentOptions{
|
||||||
parts := strings.Split(issue.Body, "Files:")
|
Handles: p.listIssueOptions.Handles,
|
||||||
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.localGitClient.GetLocalFile(path)
|
|
||||||
if err != nil {
|
|
||||||
p.log.Error("error getting file from vcclient", zap.Error(err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
files = append(files, nextFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeRequest := llm.CodeChangeRequest{
|
|
||||||
Subject: issue.Subject,
|
|
||||||
Body: issue.Body,
|
|
||||||
IssueID: issue.ID,
|
|
||||||
Files: files,
|
|
||||||
}
|
|
||||||
|
|
||||||
changeResponse, err := p.openAIClient.EvaluateCCR(p.ctx, changeRequest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.log.Error("error getting response from openai", zap.Error(err))
|
p.log.Error("error listing comments", zap.Error(err))
|
||||||
continue
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse llm response
|
|
||||||
//codeChangeResponse := llm.ParseCodeChangeResponse(llmResponse)
|
|
||||||
|
|
||||||
// create commit with file changes
|
|
||||||
err = p.localGitClient.StartCommit()
|
|
||||||
//err = p.ghClient.StartCommit()
|
|
||||||
if err != nil {
|
|
||||||
p.log.Error("error starting commit", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
newBranchName := fmt.Sprintf("fix-%s", changeRequest.IssueID)
|
|
||||||
/*
|
|
||||||
err = p.localGitClient.SwitchBranch(newBranchName)
|
|
||||||
if err != nil {
|
|
||||||
p.log.Error("error switching branch", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
for _, f := range changeResponse.Files {
|
|
||||||
p.log.Info("replacing or adding file", zap.String("path", f.Path), zap.String("contents", f.Contents))
|
|
||||||
err = p.localGitClient.ReplaceOrAddLocalFile(f)
|
|
||||||
// err = p.ghClient.ReplaceOrAddLocalFile(f)
|
|
||||||
if err != nil {
|
|
||||||
p.log.Error("error replacing or adding file", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commitMessage := changeRequest.Subject + "\n\n" + changeResponse.Notes + "\n\nResolves: #" + changeRequest.IssueID
|
|
||||||
p.log.Info("about to create commit", zap.String("message", commitMessage))
|
|
||||||
err = p.localGitClient.FinishCommit(commitMessage)
|
|
||||||
if err != nil {
|
|
||||||
p.log.Error("error finishing commit", zap.Error(err))
|
|
||||||
// TODO figure out why sometimes finish commit returns "already up-to-date error"
|
|
||||||
// return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.localGitClient.PushBranch(newBranchName)
|
|
||||||
if err != nil {
|
|
||||||
p.log.Error("error pushing branch", zap.Error(err))
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// open code change request
|
if len(comments) == 0 {
|
||||||
// TODO don't hardcode main branch, make configurable
|
p.log.Info("no comments found")
|
||||||
_, url, err := p.ghClient.OpenCodeChangeRequest(changeRequest, changeResponse, newBranchName, "main")
|
} else {
|
||||||
if err != nil {
|
p.log.Info("picked comment to process")
|
||||||
p.log.Error("error opening PR", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.log.Info("successfully created PR", zap.String("URL", url))
|
|
||||||
|
|
||||||
p.log.Info("going to sleep for thirty seconds")
|
comment := comments[0]
|
||||||
|
err = p.handleComment(comment)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("error handling comment", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove sleep
|
||||||
|
p.log.Info("sleeping 30s")
|
||||||
time.Sleep(30 * time.Second)
|
time.Sleep(30 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
func (p *PullPal) handleIssue(issue vc.Issue) error {
|
||||||
|
issueNumber, err := strconv.Atoi(issue.ID)
|
||||||
// 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 {
|
if err != nil {
|
||||||
return issue, changeRequest, err
|
p.log.Error("error converting issue ID to int", zap.Error(err))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt, err := changeRequest.GetPrompt()
|
err = p.ghClient.CommentOnIssue(issueNumber, "on it")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return issue, changeRequest, err
|
p.log.Error("error commenting on issue", zap.Error(err))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
for _, label := range p.listIssueOptions.Labels {
|
||||||
err = ioutil.WriteFile(promptPath, []byte(prompt), 0644)
|
err = p.ghClient.RemoveLabelFromIssue(issueNumber, label)
|
||||||
return issue, changeRequest, err
|
if err != nil {
|
||||||
}
|
p.log.Error("error removing labels from issue", zap.Error(err))
|
||||||
|
return err
|
||||||
// PickIssueToClipboard is the same as PickIssue, but the changeRequest is converted to a string and copied to the clipboard.
|
}
|
||||||
func (p *PullPal) PickIssueToClipboard() (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 = clipboard.WriteAll(prompt)
|
|
||||||
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.ghClient.ListOpenIssues(p.listIssueOptions)
|
|
||||||
if err != nil {
|
|
||||||
return issue, changeRequest, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(issues) == 0 {
|
|
||||||
return issue, changeRequest, IssueNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
issue = issues[0]
|
|
||||||
|
|
||||||
// remove file list from issue body
|
// remove file list from issue body
|
||||||
// TODO do this better
|
// TODO do this better and probably somewhere else
|
||||||
parts := strings.Split(issue.Body, "Files:")
|
parts := strings.Split(issue.Body, "Files:")
|
||||||
issue.Body = parts[0]
|
issue.Body = parts[0]
|
||||||
|
|
||||||
@ -250,103 +139,7 @@ func (p *PullPal) PickIssue() (issue vc.Issue, changeRequest llm.CodeChangeReque
|
|||||||
files := []llm.File{}
|
files := []llm.File{}
|
||||||
for _, path := range fileList {
|
for _, path := range fileList {
|
||||||
path = strings.TrimSpace(path)
|
path = strings.TrimSpace(path)
|
||||||
nextFile, err := p.ghClient.GetLocalFile(path)
|
nextFile, err := p.localGitClient.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.ghClient.StartCommit()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
for _, f := range codeChangeResponse.Files {
|
|
||||||
err = p.ghClient.ReplaceOrAddLocalFile(f)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commitMessage := codeChangeRequest.Subject + "\n\n" + codeChangeResponse.Notes + "\n\nResolves: #" + codeChangeRequest.IssueID
|
|
||||||
err = p.ghClient.FinishCommit(commitMessage)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. open code change request
|
|
||||||
_, url, err = p.ghClient.OpenCodeChangeRequest(codeChangeRequest, codeChangeResponse)
|
|
||||||
return url, err
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
// ListIssues gets a list of all issues meeting the provided criteria.
|
|
||||||
func (p *PullPal) ListIssues(handles, labels []string) ([]vc.Issue, error) {
|
|
||||||
issues, err := p.ghClient.ListOpenIssues(vc.ListIssueOptions{
|
|
||||||
Handles: handles,
|
|
||||||
Labels: labels,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.ghClient.ListOpenComments(vc.ListCommentOptions{
|
|
||||||
ChangeID: changeID,
|
|
||||||
Handles: handles,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return comments, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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.ghClient.GetLocalFile(path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -360,14 +153,118 @@ func (p *PullPal) MakeLocalChange(issue vc.Issue) error {
|
|||||||
Files: files,
|
Files: files,
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := p.openAIClient.EvaluateCCR(p.ctx, changeRequest)
|
changeResponse, err := p.openAIClient.EvaluateCCR(p.ctx, "", changeRequest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// create commit with file changes
|
||||||
|
err = p.localGitClient.StartCommit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// todo remove hardcoded main
|
||||||
|
p.log.Info("checking out main branch")
|
||||||
|
err = p.localGitClient.CheckoutRemoteBranch("main")
|
||||||
|
if err != nil {
|
||||||
|
p.log.Info("error checking out main branch", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newBranchName := fmt.Sprintf("fix-%s", changeRequest.IssueID)
|
||||||
|
for _, f := range changeResponse.Files {
|
||||||
|
p.log.Info("replacing or adding file", zap.String("path", f.Path), zap.String("contents", f.Contents))
|
||||||
|
err = p.localGitClient.ReplaceOrAddLocalFile(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commitMessage := changeRequest.Subject + "\n\n" + changeResponse.Notes + "\n\nResolves: #" + changeRequest.IssueID
|
||||||
|
p.log.Info("about to create commit", zap.String("message", commitMessage))
|
||||||
|
err = p.localGitClient.FinishCommit(commitMessage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("response from openai")
|
p.log.Info("pushing to branch", zap.String("branchname", newBranchName))
|
||||||
fmt.Println(res)
|
err = p.localGitClient.PushBranch(newBranchName)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Info("error pushing to branch", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// open code change request
|
||||||
|
// TODO don't hardcode main branch, make configurable
|
||||||
|
_, url, err := p.ghClient.OpenCodeChangeRequest(changeRequest, changeResponse, newBranchName, "main")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.log.Info("successfully created PR", zap.String("URL", url))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PullPal) handleComment(comment vc.Comment) error {
|
||||||
|
if comment.Branch == "" {
|
||||||
|
return errors.New("no branch provided in comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := p.localGitClient.GetLocalFile(comment.FilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
diffCommentRequest := llm.DiffCommentRequest{
|
||||||
|
File: file,
|
||||||
|
Contents: comment.Body,
|
||||||
|
Diff: comment.DiffHunk,
|
||||||
|
}
|
||||||
|
p.log.Info("diff comment request", zap.String("req", diffCommentRequest.String()))
|
||||||
|
|
||||||
|
diffCommentResponse, err := p.openAIClient.EvaluateDiffComment(p.ctx, "", diffCommentRequest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if diffCommentResponse.Type == llm.ResponseCodeChange {
|
||||||
|
p.log.Info("about to start commit")
|
||||||
|
err = p.localGitClient.StartCommit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.log.Info("checking out branch", zap.String("name", comment.Branch))
|
||||||
|
err = p.localGitClient.CheckoutRemoteBranch(comment.Branch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.log.Info("replacing or adding file", zap.String("path", diffCommentResponse.File.Path), zap.String("contents", diffCommentResponse.File.Contents))
|
||||||
|
err = p.localGitClient.ReplaceOrAddLocalFile(diffCommentResponse.File)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
commitMessage := "update based on comment"
|
||||||
|
p.log.Info("about to create commit", zap.String("message", commitMessage))
|
||||||
|
err = p.localGitClient.FinishCommit(commitMessage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.localGitClient.PushBranch(comment.Branch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.ghClient.RespondToComment(comment.PRNumber, comment.ID, diffCommentResponse.Answer)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("error commenting on issue", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.log.Info("responded addressed comment")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package pullpal
|
package pullpal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/mobyvb/pull-pal/llm"
|
"github.com/mobyvb/pull-pal/llm"
|
||||||
|
"github.com/mobyvb/pull-pal/vc"
|
||||||
|
"github.com/sashabaranov/go-openai"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ func (p *PullPal) DebugGit() error {
|
|||||||
p.log.Error("error starting commit", zap.Error(err))
|
p.log.Error("error starting commit", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
newBranchName := fmt.Sprintf("debug-branch")
|
newBranchName := "debug-branch"
|
||||||
|
|
||||||
for _, f := range []string{"a", "b"} {
|
for _, f := range []string{"a", "b"} {
|
||||||
err = p.localGitClient.ReplaceOrAddLocalFile(llm.File{
|
err = p.localGitClient.ReplaceOrAddLocalFile(llm.File{
|
||||||
@ -45,3 +45,94 @@ func (p *PullPal) DebugGit() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo dont require args for listing comments
|
||||||
|
func (p *PullPal) DebugGithub(handles []string) error {
|
||||||
|
p.log.Info("Starting Pull Pal Github debug")
|
||||||
|
|
||||||
|
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("error listing issues", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, i := range issues {
|
||||||
|
p.log.Info("got issue", zap.String("issue", i.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
comments, err := p.ghClient.ListOpenComments(vc.ListCommentOptions{
|
||||||
|
Handles: handles,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("error listing comments", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, c := range comments {
|
||||||
|
p.log.Info("got comment", zap.String("comment", c.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PullPal) DebugLLM() error {
|
||||||
|
p.log.Info("Starting Pull Pal llm debug")
|
||||||
|
|
||||||
|
file := llm.File{
|
||||||
|
Path: "main.go",
|
||||||
|
Contents: `package main\n\nimport (\n "net/http"\n)\n\nfunc main() {\n fs := http.FileServer(http.Dir("static"))\n http.Handle("/", fs)\n\n http.ListenAndServe(":7777", nil)\n}\n\n\n \n\n \n `,
|
||||||
|
}
|
||||||
|
|
||||||
|
codeChangeRequest := llm.CodeChangeRequest{
|
||||||
|
Files: []llm.File{file},
|
||||||
|
Subject: "update port and add endpoint",
|
||||||
|
Body: "use port 8080 for the server in main.go. Also add an endpoint at GET /api/numbers that returns a random integer between 2 and 10",
|
||||||
|
IssueID: "1234",
|
||||||
|
}
|
||||||
|
|
||||||
|
p.log.Info("CODE CHANGE REQUEST", zap.String("request", codeChangeRequest.String()))
|
||||||
|
|
||||||
|
diffCommentRequestChange := llm.DiffCommentRequest{
|
||||||
|
File: file,
|
||||||
|
Contents: "remove this unnecessary whitespace at the end",
|
||||||
|
Diff: "@@ -0,0 +1,15 @@\n+package main\n+ \n+ import (\n+ \"net/http\"\n+ )\n+ \n+ func main() {\n+ fs := http.FileServer(http.Dir(\"static\"))\n+ http.Handle(\"/\", fs)\n+ \n+ http.ListenAndServe(\":7777\", nil)\n+ }\n+",
|
||||||
|
}
|
||||||
|
p.log.Info("DIFF COMMENT REQUEST CODECHANGE", zap.String("request", diffCommentRequestChange.String()))
|
||||||
|
|
||||||
|
diffCommentRequestQuestion := llm.DiffCommentRequest{
|
||||||
|
File: file,
|
||||||
|
Contents: "what does this Handle line do?",
|
||||||
|
Diff: "@@ -0,0 +1,15 @@\n+package main\n+ \n+ import (\n+ \"net/http\"\n+ )\n+ \n+ func main() {\n+ fs := http.FileServer(http.Dir(\"static\"))\n+ http.Handle(\"/\", fs)\n",
|
||||||
|
}
|
||||||
|
p.log.Info("DIFF COMMENT REQUEST QUESTION", zap.String("request", diffCommentRequestQuestion.String()))
|
||||||
|
|
||||||
|
for _, m := range []string{openai.GPT3Dot5Turbo, openai.GPT4} {
|
||||||
|
p.log.Info("testing with openai api", zap.String("MODEL", m))
|
||||||
|
|
||||||
|
p.log.Info("testing code change request")
|
||||||
|
res, err := p.openAIClient.EvaluateCCR(p.ctx, m, codeChangeRequest)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("error evaluating code change request for model", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.log.Info("openai api response", zap.String("model", m), zap.String("response", res.String()))
|
||||||
|
|
||||||
|
p.log.Info("testing diff comment code change request")
|
||||||
|
diffRes, err := p.openAIClient.EvaluateDiffComment(p.ctx, m, diffCommentRequestChange)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("error evaluating diff comment request for model", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.log.Info("openai api response", zap.String("model", m), zap.String("response", diffRes.String()))
|
||||||
|
|
||||||
|
p.log.Info("testing diff comment question request")
|
||||||
|
diffRes, err = p.openAIClient.EvaluateDiffComment(p.ctx, m, diffCommentRequestQuestion)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("error evaluating diff comment request for model", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.log.Info("openai api response", zap.String("model", m), zap.String("response", diffRes.String()))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO group errors and return
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -32,24 +32,25 @@ type ListIssueOptions struct {
|
|||||||
// Comment represents a comment on a code change request.
|
// Comment represents a comment on a code change request.
|
||||||
// TODO comments on issue?
|
// TODO comments on issue?
|
||||||
type Comment struct {
|
type Comment struct {
|
||||||
ID string
|
ID int64
|
||||||
// ChangeID is the local identifier for the code change request this comment was left on (e.g. Github PR number)
|
// ChangeID is the local identifier for the code change request this comment was left on (e.g. Github PR number)
|
||||||
ChangeID string
|
ChangeID string
|
||||||
Author Author
|
Author Author
|
||||||
Body string
|
Body string
|
||||||
Position int
|
Position int
|
||||||
|
FilePath string
|
||||||
DiffHunk string
|
DiffHunk string
|
||||||
URL string
|
URL string
|
||||||
|
Branch string
|
||||||
|
PRNumber int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Comment) String() string {
|
func (c Comment) String() string {
|
||||||
return fmt.Sprintf("Comment ID: %s\nAuthor: %s\nBody: %s\nPosition: %d\n\nDiffHunk:\n%s\n\nURL: %s\n", c.ID, c.Author.Handle, c.Body, c.Position, c.DiffHunk, c.URL)
|
return fmt.Sprintf("Comment ID: %d\nAuthor: %s\nBody: %s\nPosition: %d\n\nDiffHunk:\n%s\n\nURL: %s\nBranch:\n%s\n\nFilePath:\n%s\n\n", c.ID, c.Author.Handle, c.Body, c.Position, c.DiffHunk, c.URL, c.Branch, c.FilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListCommentOptions defines options for listing comments.
|
// ListCommentOptions defines options for listing comments.
|
||||||
type ListCommentOptions struct {
|
type ListCommentOptions struct {
|
||||||
// ChangeID is the local identifier for the code change request to list comments from (e.g. Github PR number)
|
|
||||||
ChangeID string
|
|
||||||
// Handles defines the list of usernames to list comments from
|
// Handles defines the list of usernames to list comments from
|
||||||
// The comment can be created by *any* user provided.
|
// The comment can be created by *any* user provided.
|
||||||
Handles []string
|
Handles []string
|
||||||
|
58
vc/git.go
58
vc/git.go
@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/config"
|
"github.com/go-git/go-git/v5/config"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||||
)
|
)
|
||||||
@ -60,46 +61,53 @@ func NewLocalGitClient( /*ctx context.Context, */ log *zap.Logger, self Author,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
func (gc *LocalGitClient) CheckoutRemoteBranch(branchName string) (err error) {
|
||||||
func (gc *LocalGitClient) SwitchBranch(branchName string) (err error) {
|
|
||||||
if gc.worktree == nil {
|
if gc.worktree == nil {
|
||||||
return errors.New("worktree is nil - cannot check out a branch")
|
return errors.New("worktree is nil - cannot check out a branch")
|
||||||
}
|
}
|
||||||
|
|
||||||
branchRefName := plumbing.NewBranchReferenceName(branchName)
|
// TODO configurable remote
|
||||||
// remoteName := "origin"
|
branchRefName := plumbing.NewRemoteReferenceName("origin", branchName)
|
||||||
|
branchCoOpts := git.CheckoutOptions{
|
||||||
err = gc.repo.localRepo.Fetch(&git.FetchOptions{
|
Branch: plumbing.ReferenceName(branchRefName),
|
||||||
RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = gc.worktree.Checkout(&git.CheckoutOptions{
|
|
||||||
Branch: branchRefName,
|
|
||||||
Force: true,
|
Force: true,
|
||||||
})
|
}
|
||||||
|
err = gc.worktree.Checkout(&branchCoOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = gc.repo.localRepo.CreateBranch(&config.Branch{
|
|
||||||
Name: branchName,
|
// Pull the latest changes from the remote branch
|
||||||
Remote: remoteName,
|
err = gc.worktree.Pull(&git.PullOptions{
|
||||||
Merge: branchRefName,
|
RemoteName: "origin",
|
||||||
})
|
Auth: &http.BasicAuth{
|
||||||
if err != nil {
|
Username: gc.self.Handle,
|
||||||
return err
|
Password: gc.self.Token,
|
||||||
}
|
},
|
||||||
|
})
|
||||||
|
if err != nil && err != git.NoErrAlreadyUpToDate {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
func (gc *LocalGitClient) PushBranch(branchName string) (err error) {
|
func (gc *LocalGitClient) PushBranch(branchName string) (err error) {
|
||||||
//branchRefName := plumbing.NewBranchReferenceName(branchName)
|
//branchRefName := plumbing.NewBranchReferenceName(branchName)
|
||||||
remoteName := "origin"
|
remoteName := "origin"
|
||||||
|
|
||||||
|
headRef, err := gc.repo.localRepo.Head()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new branch at current HEAD
|
||||||
|
branchRef := plumbing.NewHashReference(plumbing.NewBranchReferenceName(branchName), headRef.Hash())
|
||||||
|
err = gc.repo.localRepo.Storer.SetReference(branchRef)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Push the new branch to the remote repository
|
// Push the new branch to the remote repository
|
||||||
remote, err := gc.repo.localRepo.Remote(remoteName)
|
remote, err := gc.repo.localRepo.Remote(remoteName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -109,7 +117,7 @@ func (gc *LocalGitClient) PushBranch(branchName string) (err error) {
|
|||||||
err = remote.Push(&git.PushOptions{
|
err = remote.Push(&git.PushOptions{
|
||||||
RemoteName: remoteName,
|
RemoteName: remoteName,
|
||||||
// TODO remove hardcoded "main"
|
// TODO remove hardcoded "main"
|
||||||
RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/heads/%s", "main", branchName))},
|
RefSpecs: []config.RefSpec{config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/heads/%s", branchName, branchName))},
|
||||||
Auth: &http.BasicAuth{
|
Auth: &http.BasicAuth{
|
||||||
Username: gc.self.Handle,
|
Username: gc.self.Handle,
|
||||||
Password: gc.self.Token,
|
Password: gc.self.Token,
|
||||||
|
100
vc/github.go
100
vc/github.go
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mobyvb/pull-pal/llm"
|
"github.com/mobyvb/pull-pal/llm"
|
||||||
|
|
||||||
@ -151,45 +152,84 @@ func (gc *GithubClient) RemoveLabelFromIssue(issueNumber int, label string) erro
|
|||||||
|
|
||||||
// ListOpenComments lists unresolved comments in the Github repository.
|
// ListOpenComments lists unresolved comments in the Github repository.
|
||||||
func (gc *GithubClient) ListOpenComments(options ListCommentOptions) ([]Comment, error) {
|
func (gc *GithubClient) ListOpenComments(options ListCommentOptions) ([]Comment, error) {
|
||||||
toReturn := []Comment{}
|
prs, _, err := gc.client.PullRequests.List(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, nil)
|
||||||
prNumber, err := strconv.Atoi(options.ChangeID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
comments, _, err := gc.client.PullRequests.ListComments(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, prNumber, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: filter out comments that "self" has already replied to
|
allComments := []Comment{}
|
||||||
// TODO: ignore resolved comments
|
repliedTo := make(map[int64]bool)
|
||||||
for _, c := range comments {
|
|
||||||
commentUser := c.GetUser().GetLogin()
|
for _, pr := range prs {
|
||||||
allowedUser := false
|
if pr.GetUser().GetLogin() != gc.self.Handle {
|
||||||
for _, u := range options.Handles {
|
|
||||||
if commentUser == u {
|
|
||||||
allowedUser = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !allowedUser {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
nextComment := Comment{
|
branch := ""
|
||||||
ID: strconv.FormatInt(c.GetID(), 10),
|
if pr.Head != nil {
|
||||||
ChangeID: options.ChangeID,
|
branch = pr.Head.GetLabel()
|
||||||
URL: c.GetHTMLURL(),
|
if strings.Contains(branch, ":") {
|
||||||
Author: Author{
|
branch = strings.Split(branch, ":")[1]
|
||||||
Email: c.GetUser().GetEmail(),
|
}
|
||||||
Handle: c.GetUser().GetLogin(),
|
}
|
||||||
},
|
|
||||||
Body: c.GetBody(),
|
comments, _, err := gc.client.PullRequests.ListComments(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, pr.GetNumber(), nil)
|
||||||
Position: c.GetPosition(),
|
if err != nil {
|
||||||
DiffHunk: c.GetDiffHunk(),
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range comments {
|
||||||
|
commentUser := c.GetUser().GetLogin()
|
||||||
|
if commentUser == gc.self.Handle {
|
||||||
|
repliedTo[c.GetInReplyTo()] = true
|
||||||
|
}
|
||||||
|
allowedUser := false
|
||||||
|
for _, u := range options.Handles {
|
||||||
|
if commentUser == u {
|
||||||
|
allowedUser = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !allowedUser {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nextComment := Comment{
|
||||||
|
ID: c.GetID(),
|
||||||
|
ChangeID: strconv.Itoa(pr.GetNumber()),
|
||||||
|
URL: c.GetHTMLURL(),
|
||||||
|
Author: Author{
|
||||||
|
Email: c.GetUser().GetEmail(),
|
||||||
|
Handle: c.GetUser().GetLogin(),
|
||||||
|
},
|
||||||
|
Body: c.GetBody(),
|
||||||
|
FilePath: c.GetPath(),
|
||||||
|
Position: c.GetPosition(),
|
||||||
|
DiffHunk: c.GetDiffHunk(),
|
||||||
|
Branch: branch,
|
||||||
|
PRNumber: pr.GetNumber(),
|
||||||
|
}
|
||||||
|
allComments = append(allComments, nextComment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove any comments that bot has replied to already from the list
|
||||||
|
toReturn := []Comment{}
|
||||||
|
for _, c := range allComments {
|
||||||
|
if !repliedTo[c.ID] {
|
||||||
|
toReturn = append(toReturn, c)
|
||||||
}
|
}
|
||||||
toReturn = append(toReturn, nextComment)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return toReturn, nil
|
return toReturn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RespondToComment adds a comment to the provided thread.
|
||||||
|
func (gc *GithubClient) RespondToComment(prNumber int, commentID int64, comment string) error {
|
||||||
|
_, _, err := gc.client.PullRequests.CreateCommentInReplyTo(gc.ctx, gc.repo.Owner.Handle, gc.repo.Name, prNumber, comment, commentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user