diff --git a/example/webui/connector/githubpr.go b/example/webui/connector/githubpr.go new file mode 100644 index 0000000..6765c04 --- /dev/null +++ b/example/webui/connector/githubpr.go @@ -0,0 +1,196 @@ +package connector + +import ( + "fmt" + "strings" + "time" + + "github.com/google/go-github/v61/github" + "github.com/mudler/local-agent-framework/agent" + "github.com/mudler/local-agent-framework/xlog" + + "github.com/sashabaranov/go-openai" +) + +type GithubPRs struct { + token string + repository string + owner string + replyIfNoReplies bool + agent *agent.Agent + pollInterval time.Duration + client *github.Client +} + +// NewGithubIssueWatcher creates a new GithubPRs connector +// with the given configuration +// - token: Github token +// - repository: Github repository name +// - owner: Github repository owner +// - replyIfNoReplies: If true, the bot will reply to issues with no comments +func NewGithubPRWatcher(config map[string]string) *GithubPRs { + client := github.NewClient(nil).WithAuthToken(config["token"]) + replyIfNoReplies := false + if config["replyIfNoReplies"] == "true" { + replyIfNoReplies = true + } + + interval, err := time.ParseDuration(config["pollInterval"]) + if err != nil { + interval = 10 * time.Minute + } + + return &GithubPRs{ + client: client, + token: config["token"], + repository: config["repository"], + owner: config["owner"], + replyIfNoReplies: replyIfNoReplies, + pollInterval: interval, + } +} + +func (g *GithubPRs) AgentResultCallback() func(state agent.ActionState) { + return func(state agent.ActionState) { + // Send the result to the bot + } +} + +func (g *GithubPRs) AgentReasoningCallback() func(state agent.ActionCurrentState) bool { + return func(state agent.ActionCurrentState) bool { + // Send the reasoning to the bot + return true + } +} + +func (g *GithubPRs) Start(a *agent.Agent) { + // Start the connector + g.agent = a + + go func() { + ticker := time.NewTicker(g.pollInterval) + for { + select { + case <-ticker.C: + xlog.Info("Looking into github Prs...") + g.prService() + case <-a.Context().Done(): + xlog.Info("GithubPRs connector is now stopping") + return + } + } + }() +} + +func (g *GithubPRs) prService() { + user, _, err := g.client.Users.Get(g.agent.Context(), "") + if err != nil { + fmt.Printf("\nerror: %v\n", err) + return + } + + issues, _, err := g.client.Issues.ListByRepo( + g.agent.Context(), + g.owner, + g.repository, + &github.IssueListByRepoOptions{}) + if err != nil { + xlog.Info("Error listing issues", err) + } + for _, issue := range issues { + // Do something if not an PR + if !issue.IsPullRequest() { + continue + } + labels := []string{} + for _, l := range issue.Labels { + labels = append(labels, l.GetName()) + } + + // Get user that opened the issue + userNameLogin := issue.GetUser().Login + userName := "" + if userNameLogin != nil { + userName = *userNameLogin + } + + if userName == user.GetLogin() { + xlog.Info("Ignoring issue opened by the bot") + continue + } + messages := []openai.ChatCompletionMessage{ + { + Role: "system", + Content: fmt.Sprintf( + `This is a conversation with an user ("%s") that opened a Github issue with title "%s" in the repository "%s" owned by "%s". The issue is the issue number %d. Current labels: %+v`, userName, issue.GetTitle(), g.repository, g.owner, issue.GetNumber(), labels), + }, + { + Role: "user", + Content: issue.GetBody(), + }, + } + + comments, _, _ := g.client.Issues.ListComments(g.agent.Context(), g.owner, g.repository, issue.GetNumber(), + &github.IssueListCommentsOptions{}) + + mustAnswer := false + botAnsweredAlready := false + for i, comment := range comments { + role := "user" + if comment.GetUser().GetLogin() == user.GetLogin() { + botAnsweredAlready = true + role = "assistant" + } + messages = append(messages, openai.ChatCompletionMessage{ + Role: role, + Content: comment.GetBody(), + }) + + // if last comment is from the user and mentions the bot username, we must answer + if comment.User.GetName() != user.GetLogin() && len(comments)-1 == i { + if strings.Contains(comment.GetBody(), fmt.Sprintf("@%s", user.GetLogin())) { + xlog.Info("Bot was mentioned in the last comment") + mustAnswer = true + } + } + } + + if len(comments) == 0 || !botAnsweredAlready { + // if no comments, or bot didn't answer yet, we must answer + xlog.Info("No comments, or bot didn't answer yet", + "comments", len(comments), + "botAnsweredAlready", botAnsweredAlready, + "agent", g.agent.Character.Name, + ) + mustAnswer = true + } + + if len(comments) != 0 && g.replyIfNoReplies { + xlog.Info("Ignoring issue with comments", "issue", issue.GetNumber(), "agent", g.agent.Character.Name) + mustAnswer = false + } + + if !mustAnswer { + continue + } + + res := g.agent.Ask( + agent.WithConversationHistory(messages), + ) + if res.Error != nil { + xlog.Error("Error asking", "error", res.Error, "agent", g.agent.Character.Name) + return + } + + _, _, err := g.client.Issues.CreateComment( + g.agent.Context(), + g.owner, g.repository, + issue.GetNumber(), &github.IssueComment{ + Body: github.String(res.Response), + }, + ) + if err != nil { + xlog.Error("Error creating comment", "error", err, "agent", g.agent.Character.Name) + } + } +} diff --git a/example/webui/connectors.go b/example/webui/connectors.go index 1243246..ec1d80b 100644 --- a/example/webui/connectors.go +++ b/example/webui/connectors.go @@ -16,6 +16,7 @@ const ( ConnectorSlack = "slack" ConnectorDiscord = "discord" ConnectorGithubIssues = "github-issues" + ConnectorGithubPRs = "github-prs" ) type Connector interface { @@ -29,6 +30,7 @@ var AvailableConnectors = []string{ ConnectorSlack, ConnectorDiscord, ConnectorGithubIssues, + ConnectorGithubPRs, } func (a *AgentConfig) availableConnectors() []Connector { @@ -55,6 +57,8 @@ func (a *AgentConfig) availableConnectors() []Connector { connectors = append(connectors, connector.NewDiscord(config)) case ConnectorGithubIssues: connectors = append(connectors, connector.NewGithubIssueWatcher(config)) + case ConnectorGithubPRs: + connectors = append(connectors, connector.NewGithubPRWatcher(config)) } } return connectors