Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/slack bot #381

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,16 @@ type GPTScript struct {

type Triggers struct {
Discord Discord
Slack Slack
Cron Cron
}

type Slack struct {
Enabled bool `envconfig:"SLACK_ENABLED" default:"false"`
AppToken string `envconfig:"SLACK_APP_TOKEN"`
BotToken string `envconfig:"SLACK_BOT_TOKEN"`
}

type Discord struct {
Enabled bool `envconfig:"DISCORD_ENABLED" default:"false"`
BotToken string `envconfig:"DISCORD_BOT_TOKEN"`
Expand Down
212 changes: 212 additions & 0 deletions api/pkg/trigger/slack/trigger_slack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package slack

import (
"context"
"fmt"
stdlog "log"
"os"

"github.com/helixml/helix/api/pkg/config"
"github.com/helixml/helix/api/pkg/controller"
"github.com/helixml/helix/api/pkg/store"
"github.com/helixml/helix/api/pkg/types"

openai "github.com/lukemarsden/go-openai2"
"github.com/rs/zerolog/log"
"github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents"
"github.com/slack-go/slack/socketmode"
)

const (
defaultModel = string("meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo")
)

type Slack struct {
cfg *config.ServerConfig
store store.Store
controller *controller.Controller
}

func New(cfg *config.ServerConfig, store store.Store, controller *controller.Controller) *Slack {
return &Slack{
cfg: cfg,
store: store,
controller: controller,
}
}

func (s *Slack) Start(ctx context.Context) error {
log.Info().Msg("starting Slack trigger")
defer log.Info().Msg("stopping Slack trigger")

options := []slack.Option{
slack.OptionDebug(true),
slack.OptionLog(stdlog.New(os.Stdout, "api: ", stdlog.Lshortfile|stdlog.LstdFlags)),
}

if s.cfg.Triggers.Slack.AppToken != "" {
options = append(options, slack.OptionAppLevelToken(s.cfg.Triggers.Slack.AppToken))
}

api := slack.New(
s.cfg.Triggers.Slack.BotToken,
options...,
)

client := socketmode.New(
api,
socketmode.OptionDebug(true),
socketmode.OptionLog(stdlog.New(os.Stdout, "socketmode: ", stdlog.Lshortfile|stdlog.LstdFlags)),
)

socketmodeHandler := socketmode.NewSocketmodeHandler(client)

socketmodeHandler.Handle(socketmode.EventTypeConnecting, middlewareConnecting)
socketmodeHandler.Handle(socketmode.EventTypeConnectionError, middlewareConnectionError)
socketmodeHandler.Handle(socketmode.EventTypeConnected, middlewareConnected)

// Handle a specific event from EventsAPI
socketmodeHandler.HandleEvents(slackevents.AppMention, s.middlewareAppMentionEvent)

// TODO: this is to listen to everything
// socketmodeHandler.Handle(socketmode.EventTypeEventsAPI, s.middlewareEventsAPI)

err := socketmodeHandler.RunEventLoop()
if err != nil {
log.Error().Err(err).Msg("failed to run event loop")
}

<-ctx.Done()

return nil
}

func (s *Slack) middlewareEventsAPI(evt *socketmode.Event, client *socketmode.Client) {
fmt.Println("middlewareEventsAPI")
eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
if !ok {
fmt.Printf("Ignored %+v\n", evt)
return
}

fmt.Printf("Event received: %+v\n", eventsAPIEvent)

client.Ack(*evt.Request)

switch eventsAPIEvent.Type {
case slackevents.CallbackEvent:
innerEvent := eventsAPIEvent.InnerEvent
switch ev := innerEvent.Data.(type) {
case *slackevents.AppMentionEvent:
fmt.Printf("We have been mentionned in %v", ev.Channel)
_, _, err := client.Client.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false))
if err != nil {
fmt.Printf("failed posting message: %v", err)
}
case *slackevents.MemberJoinedChannelEvent:
fmt.Printf("user %q joined to channel %q", ev.User, ev.Channel)
}
default:
client.Debugf("unsupported Events API event received")
}
}

func (s *Slack) middlewareAppMentionEvent(evt *socketmode.Event, client *socketmode.Client) {

eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent)
if !ok {
log.Info().Msgf("Ignored: %+v", evt)
return
}

client.Ack(*evt.Request)

ev, ok := eventsAPIEvent.InnerEvent.Data.(*slackevents.AppMentionEvent)
if !ok {
log.Info().Msgf("Ignored event: %+v", ev)
return
}

log.Info().Str("channel", ev.Channel).Msg("We have been mentioned")

resp, err := s.startChat(context.Background(), &types.App{}, ev)
if err != nil {
log.Error().Err(err).Msg("failed to start chat")
_, _, _ = client.Client.PostMessage(ev.Channel, slack.MsgOptionText(err.Error(), false))
return
}

_, _, err = client.Client.PostMessage(ev.Channel, slack.MsgOptionText(resp, false))
if err != nil {
log.Error().Err(err).Msg("failed to post message")
}
}

func (s *Slack) startChat(ctx context.Context, app *types.App, event *slackevents.AppMentionEvent) (string, error) {
system := openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleSystem,
Content: `You are an AI assistant Discord bot. Be concise with the replies, keep them short but informative.`,
}

messages := []openai.ChatCompletionMessage{
system,
}

// TODO: Add history from a thread
// for _, msg := range history {
// switch {
// case msg.Author.ID == s.State.User.ID:
// messages = append(messages, openai.ChatCompletionMessage{
// Role: openai.ChatMessageRoleAssistant,
// Content: msg.Content,
// })
// default:
// messages = append(messages, openai.ChatCompletionMessage{
// Role: openai.ChatMessageRoleUser,
// Content: msg.Content,
// })
// }
// }

userMessage := openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleUser,
Content: event.Text,
}

messages = append(messages, userMessage)

resp, err := s.controller.ChatCompletion(
ctx,
&types.User{},
openai.ChatCompletionRequest{
Stream: false,
Model: defaultModel,
Messages: messages,
},
&controller.ChatCompletionOptions{
AppID: app.ID,
},
)
if err != nil {
return "", fmt.Errorf("failed to get response from inference API: %w", err)
}

if len(resp.Choices) == 0 {
return "", fmt.Errorf("no choices in response")
}

return resp.Choices[0].Message.Content, nil
}

func middlewareConnecting(evt *socketmode.Event, client *socketmode.Client) {
log.Info().Msg("Connecting to Slack with Socket Mode...")
}

func middlewareConnectionError(evt *socketmode.Event, client *socketmode.Client) {
log.Error().Msg("Connection failed. Retrying later...")
}

func middlewareConnected(evt *socketmode.Event, client *socketmode.Client) {
log.Info().Msg("Connected to Slack with Socket Mode.")
}
20 changes: 20 additions & 0 deletions api/pkg/trigger/trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/helixml/helix/api/pkg/store"
"github.com/helixml/helix/api/pkg/trigger/cron"
"github.com/helixml/helix/api/pkg/trigger/discord"
"github.com/helixml/helix/api/pkg/trigger/slack"

"github.com/rs/zerolog/log"
)
Expand Down Expand Up @@ -41,6 +42,14 @@ func (t *TriggerManager) Start(ctx context.Context) {
}()
}

if t.cfg.Triggers.Slack.Enabled && t.cfg.Triggers.Slack.BotToken != "" {
t.wg.Add(1)
go func() {
defer t.wg.Done()
t.runSlack(ctx)
}()
}

t.wg.Add(1)
go func() {
defer t.wg.Done()
Expand All @@ -50,6 +59,17 @@ func (t *TriggerManager) Start(ctx context.Context) {
t.wg.Wait()
}

func (t *TriggerManager) runSlack(ctx context.Context) {
slackTrigger := slack.New(t.cfg, t.store, t.controller)

for {
err := slackTrigger.Start(ctx)
if err != nil {
log.Err(err).Msg("failed to start slack trigger, retrying in 10 seconds")
}
}
}

func (t *TriggerManager) runDiscord(ctx context.Context) {
discordTrigger := discord.New(t.cfg, t.store, t.controller)

Expand Down
7 changes: 7 additions & 0 deletions api/pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -980,12 +980,19 @@ type DiscordTrigger struct {
ServerName string `json:"server_name" yaml:"server_name"`
}

type SlackTrigger struct {
AppToken string `json:"app_token" yaml:"app_token"`
BotToken string `json:"bot_token" yaml:"bot_token"`
Channels []string `json:"channels" yaml:"channels"`
}

type CronTrigger struct {
Schedule string `json:"schedule,omitempty"`
Input string `json:"input,omitempty"`
}

type Trigger struct {
Slack *SlackTrigger `json:"slack,omitempty"`
Discord *DiscordTrigger `json:"discord,omitempty"`
Cron *CronTrigger `json:"cron,omitempty"`
}
Expand Down
18 changes: 18 additions & 0 deletions examples/slack_bot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: slack-bot
description: |
A simple app that demonstrates how to setup Helix as a Discord bot.
Our bot can be installed following this link https://discord.com/oauth2/authorize?client_id=1251942355980779531
assistants:
- name: Helix
description: Responds to messages in a Discord channel
apis:
- name: Demo Hiring Pipeline API
description: List all job vacancies, optionally filter by job title and/or candidate name
url: https://demos.tryhelix.ai
schema: ./openapi/jobvacancies.yaml

triggers:
- slack:
app_token: $SLACK_APP_TOKEN
bot_token: $SLACK_BOT_TOKEN
channels: $SLACK_CHANNELS
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ require (
github.com/nats-io/nats-server/v2 v2.10.9
github.com/nats-io/nats.go v1.32.0
github.com/oklog/ulid/v2 v2.1.0
github.com/olekukonko/tablewriter v0.0.6-0.20230925090304-df64c4bbad77
github.com/rs/zerolog v1.31.0
github.com/slack-go/slack v0.12.2
github.com/sourcegraph/conc v0.3.0
github.com/spf13/cobra v1.8.1
github.com/stripe/stripe-go/v76 v76.8.0
Expand Down Expand Up @@ -106,7 +108,6 @@ require (
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
github.com/olekukonko/tablewriter v0.0.6-0.20230925090304-df64c4bbad77 // indirect
github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
Expand Down
Loading