Add slack and github connectors

This commit is contained in:
mudler
2024-04-09 18:24:47 +02:00
parent 414a973282
commit 4453f43bec
11 changed files with 423 additions and 34 deletions

View File

@@ -0,0 +1,105 @@
package connector
import (
"fmt"
"strings"
"github.com/bwmarrin/discordgo"
"github.com/mudler/local-agent-framework/agent"
)
type Discord struct {
token string
defaultChannel string
}
func NewDiscord(config map[string]string) *Discord {
return &Discord{
token: config["token"],
defaultChannel: config["defaultChannel"],
}
}
func (d *Discord) AgentResultCallback() func(state agent.ActionState) {
return func(state agent.ActionState) {
// Send the result to the bot
}
}
func (d *Discord) AgentReasoningCallback() func(state agent.ActionCurrentState) bool {
return func(state agent.ActionCurrentState) bool {
// Send the reasoning to the bot
return true
}
}
func (d *Discord) Start(a *agent.Agent) {
Token := d.token
// Create a new Discord session using the provided bot token.
dg, err := discordgo.New(Token)
if err != nil {
fmt.Println("error creating Discord session,", err)
return
}
// Register the messageCreate func as a callback for MessageCreate events.
dg.AddHandler(d.messageCreate(a))
// In this example, we only care about receiving message events.
dg.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentsDirectMessages | discordgo.IntentMessageContent
// Open a websocket connection to Discord and begin listening.
err = dg.Open()
if err != nil {
fmt.Println("error opening connection,", err)
return
}
go func() {
<-a.Context().Done()
dg.Close()
}()
}
// This function will be called (due to AddHandler above) every time a new
// message is created on any channel that the authenticated bot has access to.
func (d *Discord) messageCreate(a *agent.Agent) func(s *discordgo.Session, m *discordgo.MessageCreate) {
return func(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore all messages created by the bot itself
// This isn't required in this specific example but it's a good practice.
if m.Author.ID == s.State.User.ID {
return
}
interact := func() {
//m := m.ContentWithMentionsReplaced()
content := m.Content
content = strings.ReplaceAll(content, "<@"+s.State.User.ID+"> ", "")
job := a.Ask(
agent.WithText(
content,
),
)
_, err := s.ChannelMessageSend(m.ChannelID, job.Response)
if err != nil {
fmt.Println("error sending message,", err)
}
}
// Interact if we are mentioned
for _, mention := range m.Mentions {
if mention.ID == s.State.User.ID {
interact()
return
}
}
// Or we are in the default channel (if one is set!)
if d.defaultChannel != "" && m.ChannelID == d.defaultChannel {
interact()
return
}
}
}

View File

@@ -0,0 +1,153 @@
package connector
import (
"fmt"
"strings"
"time"
"github.com/google/go-github/v61/github"
"github.com/mudler/local-agent-framework/agent"
"github.com/sashabaranov/go-openai"
)
type GithubIssues struct {
token string
repository string
owner string
agent *agent.Agent
pollInterval time.Duration
client *github.Client
}
func NewGithub(config map[string]string) *GithubIssues {
client := github.NewClient(nil).WithAuthToken(config["token"])
interval, err := time.ParseDuration(config["pollInterval"])
if err != nil {
interval = 1 * time.Minute
}
return &GithubIssues{
client: client,
token: config["token"],
repository: config["repository"],
owner: config["owner"],
pollInterval: interval,
}
}
func (g *GithubIssues) AgentResultCallback() func(state agent.ActionState) {
return func(state agent.ActionState) {
// Send the result to the bot
}
}
func (g *GithubIssues) AgentReasoningCallback() func(state agent.ActionCurrentState) bool {
return func(state agent.ActionCurrentState) bool {
// Send the reasoning to the bot
return true
}
}
func (g *GithubIssues) Start(a *agent.Agent) {
// Start the connector
g.agent = a
go func() {
ticker := time.NewTicker(g.pollInterval)
for {
select {
case <-ticker.C:
fmt.Println("Fire in da hole!")
g.issuesService()
case <-a.Context().Done():
return
}
}
}()
}
func (g *GithubIssues) issuesService() {
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 {
fmt.Println("Error listing issues", err)
}
for _, issue := range issues {
// Do something with the issue
if issue.IsPullRequest() {
continue
}
messages := []openai.ChatCompletionMessage{
{
Role: "system",
Content: fmt.Sprintf("This is a conversation with an user that opened a Github issue with title '%s'.", issue.GetTitle()),
},
{
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())) {
fmt.Println("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
fmt.Println("No comments, or bot didn't answer yet")
fmt.Println("Comments:", len(comments))
fmt.Println("Bot answered already", botAnsweredAlready)
mustAnswer = true
}
if !mustAnswer {
continue
}
res := g.agent.Ask(
agent.WithConversationHistory(messages),
)
_, _, err := g.client.Issues.CreateComment(
g.agent.Context(),
g.owner, g.repository,
issue.GetNumber(), &github.IssueComment{
Body: github.String(res.Response),
},
)
if err != nil {
fmt.Println("Error creating comment", err)
}
}
}

View File

@@ -41,15 +41,15 @@ func (t *Slack) AgentReasoningCallback() func(state agent.ActionCurrentState) bo
func (t *Slack) Start(a *agent.Agent) {
api := slack.New(
t.botToken,
slack.OptionDebug(true),
// slack.OptionDebug(true),
slack.OptionLog(log.New(os.Stdout, "api: ", log.Lshortfile|log.LstdFlags)),
slack.OptionAppLevelToken(t.appToken),
)
client := socketmode.New(
api,
socketmode.OptionDebug(true),
socketmode.OptionLog(log.New(os.Stdout, "socketmode: ", log.Lshortfile|log.LstdFlags)),
// socketmode.OptionDebug(true),
//socketmode.OptionLog(log.New(os.Stdout, "socketmode: ", log.Lshortfile|log.LstdFlags)),
)
go func() {
for evt := range client.Events {

View File

@@ -14,22 +14,29 @@ import (
type Telegram struct {
Token string
lastChatID int64
bot *bot.Bot
agent *agent.Agent
}
// Send any text message to the bot after the bot has been started
func (t *Telegram) AgentResultCallback() func(state agent.ActionState) {
return func(state agent.ActionState) {
// Send the result to the bot
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{
Description: state.Reasoning,
})
}
}
func (t *Telegram) AgentReasoningCallback() func(state agent.ActionCurrentState) bool {
return func(state agent.ActionCurrentState) bool {
// Send the reasoning to the bot
t.bot.SetMyDescription(t.agent.Context(), &bot.SetMyDescriptionParams{
Description: state.Reasoning,
})
return true
}
}
func (t *Telegram) Start(a *agent.Agent) {
ctx, cancel := signal.NotifyContext(a.Context(), os.Interrupt)
defer cancel()
@@ -54,6 +61,9 @@ func (t *Telegram) Start(a *agent.Agent) {
panic(err)
}
t.bot = b
t.agent = a
go func() {
for m := range a.ConversationChannel() {
if t.lastChatID == 0 {