mirror of
https://github.com/Pull-Pal/pull-pal.git
synced 2024-06-12 11:20:44 +00:00
Support multiple repositories
Allow pull pal to be configured with multiple code repositories Also make wait duration configurable
This commit is contained in:
parent
5b143cd135
commit
94e9034dc2
45
cmd/root.go
45
cmd/root.go
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mobyvb/pull-pal/pullpal"
|
||||
"github.com/mobyvb/pull-pal/vc"
|
||||
|
@ -23,17 +24,15 @@ type config struct {
|
|||
openAIToken string
|
||||
|
||||
// remote repo info
|
||||
repoDomain string
|
||||
repoHandle string
|
||||
repoName string
|
||||
repos []string
|
||||
|
||||
// local paths
|
||||
localRepoPath string
|
||||
|
||||
// program settings
|
||||
promptToClipboard bool
|
||||
usersToListenTo []string
|
||||
requiredIssueLabels []string
|
||||
waitDuration time.Duration
|
||||
}
|
||||
|
||||
func getConfig() config {
|
||||
|
@ -43,15 +42,13 @@ func getConfig() config {
|
|||
githubToken: viper.GetString("github-token"),
|
||||
openAIToken: viper.GetString("open-ai-token"),
|
||||
|
||||
repoDomain: viper.GetString("repo-domain"),
|
||||
repoHandle: viper.GetString("repo-handle"),
|
||||
repoName: viper.GetString("repo-name"),
|
||||
repos: viper.GetStringSlice("repos"),
|
||||
|
||||
localRepoPath: viper.GetString("local-repo-path"),
|
||||
|
||||
promptToClipboard: viper.GetBool("prompt-to-clipboard"),
|
||||
usersToListenTo: viper.GetStringSlice("users-to-listen-to"),
|
||||
requiredIssueLabels: viper.GetStringSlice("required-issue-labels"),
|
||||
waitDuration: viper.GetDuration("wait-duration"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,20 +65,22 @@ func getPullPal(ctx context.Context, cfg config) (*pullpal.PullPal, error) {
|
|||
Handle: cfg.selfHandle,
|
||||
Token: cfg.githubToken,
|
||||
}
|
||||
repo := vc.Repository{
|
||||
LocalPath: cfg.localRepoPath,
|
||||
HostDomain: cfg.repoDomain,
|
||||
Name: cfg.repoName,
|
||||
Owner: vc.Author{
|
||||
Handle: cfg.repoHandle,
|
||||
},
|
||||
}
|
||||
listIssueOptions := vc.ListIssueOptions{
|
||||
Handles: cfg.usersToListenTo,
|
||||
Labels: cfg.requiredIssueLabels,
|
||||
}
|
||||
// TODO make model configurable
|
||||
p, err := pullpal.NewPullPal(ctx, log.Named("pullpal"), listIssueOptions, author, repo, openai.GPT4, cfg.openAIToken)
|
||||
ppCfg := pullpal.Config{
|
||||
WaitDuration: cfg.waitDuration,
|
||||
LocalRepoPath: cfg.localRepoPath,
|
||||
Repos: cfg.repos,
|
||||
Self: author,
|
||||
ListIssueOptions: listIssueOptions,
|
||||
// TODO configurable model
|
||||
Model: openai.GPT4,
|
||||
OpenAIToken: cfg.openAIToken,
|
||||
}
|
||||
p, err := pullpal.NewPullPal(ctx, log.Named("pullpal"), ppCfg)
|
||||
|
||||
return p, err
|
||||
}
|
||||
|
@ -129,28 +128,26 @@ func init() {
|
|||
rootCmd.PersistentFlags().StringP("github-token", "t", "GITHUB TOKEN", "token for authenticating Github actions")
|
||||
rootCmd.PersistentFlags().StringP("open-ai-token", "k", "OPENAI TOKEN", "token for authenticating OpenAI")
|
||||
|
||||
rootCmd.PersistentFlags().StringP("repo-domain", "d", "github.com", "domain for version control server")
|
||||
rootCmd.PersistentFlags().StringP("repo-handle", "o", "REPO-HANDLE", "handle of repository's owner on version control server")
|
||||
rootCmd.PersistentFlags().StringP("repo-name", "n", "REPO-NAME", "name of repository on version control server")
|
||||
rootCmd.PersistentFlags().StringSliceP("repos", "r", []string{}, "a list of git repositories that Pull Pal will monitor")
|
||||
|
||||
rootCmd.PersistentFlags().StringP("local-repo-path", "l", "/tmp/pullpallrepo", "local path to check out ephemeral repository in")
|
||||
rootCmd.PersistentFlags().StringP("local-repo-path", "l", "/tmp/pullpalrepo", "local path to check out ephemeral repository in")
|
||||
|
||||
rootCmd.PersistentFlags().StringSliceP("users-to-listen-to", "a", []string{}, "a list of Github users that Pull Pal will respond to")
|
||||
rootCmd.PersistentFlags().StringSliceP("required-issue-labels", "i", []string{}, "a list of labels that are required for Pull Pal to select an issue")
|
||||
rootCmd.PersistentFlags().Duration("wait-time", 30*time.Second, "the amount of time Pull Pal should wait when no issues or comments are found to address")
|
||||
|
||||
viper.BindPFlag("handle", rootCmd.PersistentFlags().Lookup("handle"))
|
||||
viper.BindPFlag("email", rootCmd.PersistentFlags().Lookup("email"))
|
||||
viper.BindPFlag("github-token", rootCmd.PersistentFlags().Lookup("github-token"))
|
||||
viper.BindPFlag("open-ai-token", rootCmd.PersistentFlags().Lookup("open-ai-token"))
|
||||
|
||||
viper.BindPFlag("repo-domain", rootCmd.PersistentFlags().Lookup("repo-domain"))
|
||||
viper.BindPFlag("repo-handle", rootCmd.PersistentFlags().Lookup("repo-handle"))
|
||||
viper.BindPFlag("repo-name", rootCmd.PersistentFlags().Lookup("repo-name"))
|
||||
viper.BindPFlag("repos", rootCmd.PersistentFlags().Lookup("repos"))
|
||||
|
||||
viper.BindPFlag("local-repo-path", rootCmd.PersistentFlags().Lookup("local-repo-path"))
|
||||
|
||||
viper.BindPFlag("users-to-listen-to", rootCmd.PersistentFlags().Lookup("users-to-listen-to"))
|
||||
viper.BindPFlag("required-issue-labels", rootCmd.PersistentFlags().Lookup("required-issue-labels"))
|
||||
viper.BindPFlag("wait-time", rootCmd.PersistentFlags().Lookup("wait-time"))
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mobyvb/pull-pal/llm"
|
||||
|
@ -15,40 +17,96 @@ import (
|
|||
// IssueNotFound is returned when no issue can be found to generate a prompt for.
|
||||
var IssueNotFound = errors.New("no issue found")
|
||||
|
||||
type Config struct {
|
||||
WaitDuration time.Duration
|
||||
LocalRepoPath string
|
||||
Repos []string
|
||||
Self vc.Author
|
||||
ListIssueOptions vc.ListIssueOptions
|
||||
Model string
|
||||
OpenAIToken string
|
||||
}
|
||||
|
||||
// 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
|
||||
listIssueOptions vc.ListIssueOptions
|
||||
ctx context.Context
|
||||
log *zap.Logger
|
||||
cfg Config
|
||||
|
||||
ghClient *vc.GithubClient
|
||||
localGitClient *vc.LocalGitClient
|
||||
openAIClient *llm.OpenAIClient
|
||||
repos []pullPalRepo
|
||||
openAIClient *llm.OpenAIClient
|
||||
}
|
||||
|
||||
type pullPalRepo struct {
|
||||
ctx context.Context
|
||||
log *zap.Logger
|
||||
|
||||
listIssueOptions vc.ListIssueOptions
|
||||
ghClient *vc.GithubClient
|
||||
localGitClient *vc.LocalGitClient
|
||||
openAIClient *llm.OpenAIClient
|
||||
}
|
||||
|
||||
// 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, model string, openAIToken string) (*PullPal, error) {
|
||||
ghClient, err := vc.NewGithubClient(ctx, log, self, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func NewPullPal(ctx context.Context, log *zap.Logger, cfg Config) (*PullPal, error) {
|
||||
openAIClient := llm.NewOpenAIClient(log.Named("openaiClient"), cfg.Model, cfg.OpenAIToken)
|
||||
|
||||
ppRepos := []pullPalRepo{}
|
||||
fmt.Println("asdfasfdasfasdfasdf")
|
||||
for _, r := range cfg.Repos {
|
||||
fmt.Println(r)
|
||||
parts := strings.Split(r, "/")
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
host := parts[0]
|
||||
owner := parts[1]
|
||||
name := parts[2]
|
||||
fmt.Println(host)
|
||||
fmt.Println(owner)
|
||||
fmt.Println(name)
|
||||
newRepo := vc.Repository{
|
||||
LocalPath: filepath.Join(cfg.LocalRepoPath, owner, name),
|
||||
HostDomain: host,
|
||||
Name: name,
|
||||
Owner: vc.Author{
|
||||
Handle: owner,
|
||||
},
|
||||
}
|
||||
ghClient, err := vc.NewGithubClient(ctx, log.Named("ghclient-"+r), cfg.Self, newRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
localGitClient, err := vc.NewLocalGitClient(log.Named("gitclient-"+r), cfg.Self, newRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ppRepos = append(ppRepos, pullPalRepo{
|
||||
ctx: ctx,
|
||||
log: log,
|
||||
|
||||
ghClient: ghClient,
|
||||
localGitClient: localGitClient,
|
||||
openAIClient: openAIClient,
|
||||
|
||||
listIssueOptions: cfg.ListIssueOptions,
|
||||
})
|
||||
}
|
||||
localGitClient, err := vc.NewLocalGitClient(log, self, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if len(ppRepos) == 0 {
|
||||
return nil, errors.New("no repos set up")
|
||||
}
|
||||
|
||||
return &PullPal{
|
||||
ctx: ctx,
|
||||
log: log,
|
||||
listIssueOptions: listIssueOptions,
|
||||
ctx: ctx,
|
||||
log: log,
|
||||
|
||||
ghClient: ghClient,
|
||||
localGitClient: localGitClient,
|
||||
openAIClient: llm.NewOpenAIClient(log.Named("openaiClient"), model, openAIToken),
|
||||
repos: ppRepos,
|
||||
openAIClient: openAIClient,
|
||||
cfg: cfg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -57,67 +115,78 @@ func (p *PullPal) Run() error {
|
|||
p.log.Info("Starting Pull Pal")
|
||||
// TODO gracefully handle context cancelation
|
||||
for {
|
||||
p.log.Info("checking github issues...")
|
||||
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
|
||||
if err != nil {
|
||||
p.log.Error("error listing issues", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(issues) == 0 {
|
||||
p.log.Info("no issues found")
|
||||
} else {
|
||||
p.log.Info("picked issue to process")
|
||||
|
||||
issue := issues[0]
|
||||
err = p.handleIssue(issue)
|
||||
for _, r := range p.repos {
|
||||
err := r.checkIssuesAndComments()
|
||||
if err != nil {
|
||||
// TODO leave comment if error (make configurable)
|
||||
p.log.Error("error handling issue", zap.Error(err))
|
||||
commentText := fmt.Sprintf("I ran into a problem working on this:\n```\n%w\n```", err)
|
||||
err = p.ghClient.CommentOnIssue(issue.Number, commentText)
|
||||
if err != nil {
|
||||
p.log.Error("error commenting on issue with error", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.log.Info("checking pr comments...")
|
||||
comments, err := p.ghClient.ListOpenComments(vc.ListCommentOptions{
|
||||
Handles: p.listIssueOptions.Handles,
|
||||
})
|
||||
if err != nil {
|
||||
p.log.Error("error listing comments", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(comments) == 0 {
|
||||
p.log.Info("no comments found")
|
||||
} else {
|
||||
p.log.Info("picked comment to process")
|
||||
|
||||
comment := comments[0]
|
||||
err = p.handleComment(comment)
|
||||
if err != nil {
|
||||
// TODO leave comment if error (make configurable)
|
||||
p.log.Error("error handling comment", zap.Error(err))
|
||||
commentText := fmt.Sprintf("I ran into a problem working on this:\n```\n%w\n```", err)
|
||||
err = p.ghClient.RespondToComment(comment.PRNumber, comment.ID, commentText)
|
||||
if err != nil {
|
||||
p.log.Error("error commenting on thread with error", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
p.log.Error("issue checking repo for issues and comments", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO remove sleep
|
||||
p.log.Info("sleeping 30s")
|
||||
time.Sleep(30 * time.Second)
|
||||
p.log.Info("sleeping", zap.Duration("wait duration", p.cfg.WaitDuration))
|
||||
time.Sleep(p.cfg.WaitDuration)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PullPal) handleIssue(issue vc.Issue) error {
|
||||
// checkIssuesAndComments will attempt to find and solve one issue and one comment, and then return.
|
||||
func (p pullPalRepo) checkIssuesAndComments() error {
|
||||
p.log.Info("checking github issues...")
|
||||
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
|
||||
if err != nil {
|
||||
p.log.Error("error listing issues", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(issues) == 0 {
|
||||
p.log.Info("no issues found")
|
||||
} else {
|
||||
p.log.Info("picked issue to process")
|
||||
|
||||
issue := issues[0]
|
||||
err = p.handleIssue(issue)
|
||||
if err != nil {
|
||||
// TODO leave comment if error (make configurable)
|
||||
p.log.Error("error handling issue", zap.Error(err))
|
||||
commentText := fmt.Sprintf("I ran into a problem working on this:\n```\n%w\n```", err)
|
||||
err = p.ghClient.CommentOnIssue(issue.Number, commentText)
|
||||
if err != nil {
|
||||
p.log.Error("error commenting on issue with error", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.log.Info("checking pr comments...")
|
||||
comments, err := p.ghClient.ListOpenComments(vc.ListCommentOptions{
|
||||
Handles: p.listIssueOptions.Handles,
|
||||
})
|
||||
if err != nil {
|
||||
p.log.Error("error listing comments", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(comments) == 0 {
|
||||
p.log.Info("no comments found")
|
||||
} else {
|
||||
p.log.Info("picked comment to process")
|
||||
|
||||
comment := comments[0]
|
||||
err = p.handleComment(comment)
|
||||
if err != nil {
|
||||
// TODO leave comment if error (make configurable)
|
||||
p.log.Error("error handling comment", zap.Error(err))
|
||||
commentText := fmt.Sprintf("I ran into a problem working on this:\n```\n%w\n```", err)
|
||||
err = p.ghClient.RespondToComment(comment.PRNumber, comment.ID, commentText)
|
||||
if err != nil {
|
||||
p.log.Error("error commenting on thread with error", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *pullPalRepo) handleIssue(issue vc.Issue) error {
|
||||
err := p.ghClient.CommentOnIssue(issue.Number, "working on it")
|
||||
if err != nil {
|
||||
p.log.Error("error commenting on issue", zap.Error(err))
|
||||
|
@ -175,7 +244,7 @@ func (p *PullPal) handleIssue(issue vc.Issue) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *PullPal) handleComment(comment vc.Comment) error {
|
||||
func (p *pullPalRepo) handleComment(comment vc.Comment) error {
|
||||
if comment.Branch == "" {
|
||||
return errors.New("no branch provided in comment")
|
||||
}
|
||||
|
|
|
@ -9,37 +9,38 @@ import (
|
|||
|
||||
func (p *PullPal) DebugGit() error {
|
||||
p.log.Info("Starting Pull Pal git debug")
|
||||
r := p.repos[0]
|
||||
|
||||
// create commit with file changes
|
||||
err := p.localGitClient.StartCommit()
|
||||
err := r.localGitClient.StartCommit()
|
||||
//err = p.ghClient.StartCommit()
|
||||
if err != nil {
|
||||
p.log.Error("error starting commit", zap.Error(err))
|
||||
r.log.Error("error starting commit", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
newBranchName := "debug-branch"
|
||||
|
||||
for _, f := range []string{"a", "b"} {
|
||||
err = p.localGitClient.ReplaceOrAddLocalFile(llm.File{
|
||||
err = r.localGitClient.ReplaceOrAddLocalFile(llm.File{
|
||||
Path: f,
|
||||
Contents: "hello",
|
||||
})
|
||||
if err != nil {
|
||||
p.log.Error("error replacing or adding file", zap.Error(err))
|
||||
r.log.Error("error replacing or adding file", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
commitMessage := "debug commit message"
|
||||
err = p.localGitClient.FinishCommit(commitMessage)
|
||||
err = r.localGitClient.FinishCommit(commitMessage)
|
||||
if err != nil {
|
||||
p.log.Error("error finishing commit", zap.Error(err))
|
||||
r.log.Error("error finishing commit", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
err = p.localGitClient.PushBranch(newBranchName)
|
||||
err = r.localGitClient.PushBranch(newBranchName)
|
||||
if err != nil {
|
||||
p.log.Error("error pushing branch", zap.Error(err))
|
||||
r.log.Error("error pushing branch", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -49,25 +50,26 @@ func (p *PullPal) DebugGit() error {
|
|||
// todo dont require args for listing comments
|
||||
func (p *PullPal) DebugGithub(handles []string) error {
|
||||
p.log.Info("Starting Pull Pal Github debug")
|
||||
r := p.repos[0]
|
||||
|
||||
issues, err := p.ghClient.ListOpenIssues(p.listIssueOptions)
|
||||
issues, err := r.ghClient.ListOpenIssues(r.listIssueOptions)
|
||||
if err != nil {
|
||||
p.log.Error("error listing issues", zap.Error(err))
|
||||
r.log.Error("error listing issues", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
for _, i := range issues {
|
||||
p.log.Info("got issue", zap.String("issue", i.String()))
|
||||
r.log.Info("got issue", zap.String("issue", i.String()))
|
||||
}
|
||||
|
||||
comments, err := p.ghClient.ListOpenComments(vc.ListCommentOptions{
|
||||
comments, err := r.ghClient.ListOpenComments(vc.ListCommentOptions{
|
||||
Handles: handles,
|
||||
})
|
||||
if err != nil {
|
||||
p.log.Error("error listing comments", zap.Error(err))
|
||||
r.log.Error("error listing comments", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
for _, c := range comments {
|
||||
p.log.Info("got comment", zap.String("comment", c.String()))
|
||||
r.log.Info("got comment", zap.String("comment", c.String()))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
Loading…
Reference in New Issue
Block a user