Skip to content

Commit de2a47e

Browse files
committed
openai: add #include-attachments option to load all text based attachments
1 parent aa80f8e commit de2a47e

6 files changed

Lines changed: 128 additions & 21 deletions

File tree

client/slack.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package client
44

55
import (
66
"fmt"
7+
"io"
78
"strings"
89
"sync"
910

@@ -125,6 +126,9 @@ type SlackClient interface {
125126

126127
// PinMessage will pin a message to the channel
127128
PinMessage(channel string, timestamp string) error
129+
130+
// GetFile downloads a file from Slack by its private URL
131+
GetFile(downloadURL string, writer io.Writer) error
128132
}
129133

130134
// Slack is wrapper to the slack.Client which also holds the the socketmode.Client and all needed config
@@ -336,6 +340,11 @@ func (s *Slack) UploadFile(params slack.UploadFileV2Parameters) (*slack.FileSumm
336340
return s.UploadFileV2(params)
337341
}
338342

343+
// GetFile downloads a file from Slack by its private URL
344+
func (s *Slack) GetFile(downloadURL string, writer io.Writer) error {
345+
return s.Client.GetFile(downloadURL, writer)
346+
}
347+
339348
// GetUserIDAndName returns the user-id and user-name based on a identifier. If can get a user-id or name
340349
func GetUserIDAndName(identifier string) (id string, name string) {
341350
identifier = strings.TrimPrefix(identifier, "@")

command/openai/command.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package openai
22

33
import (
4+
"bytes"
45
"fmt"
56
"regexp"
67
"strings"
@@ -87,7 +88,7 @@ func (c *openaiCommand) startConversation(message msg.Ref, text string) bool {
8788

8889
// Handle #message-history hashtag
8990
if hashtagOptions.MessageHistory > 0 {
90-
channelMessages, err := c.getChannelHistory(message.GetChannel(), hashtagOptions.MessageHistory)
91+
channelMessages, err := c.getChannelHistory(message.GetChannel(), hashtagOptions.MessageHistory, hashtagOptions.IncludeAttachments)
9192
if err != nil {
9293
c.ReplyError(message, fmt.Errorf("can't load channel history: %w", err))
9394
return true
@@ -410,8 +411,32 @@ func (c *openaiCommand) GetTemplateFunction() template.FuncMap {
410411
}
411412
}
412413

414+
// loadTextAttachments downloads text file attachments and returns them formatted
415+
func (c *openaiCommand) loadTextAttachments(files []slack.File) string {
416+
log.Warn("loadTextAttachments") // todo temporary
417+
418+
var result strings.Builder
419+
for _, file := range files {
420+
if !strings.HasPrefix(file.Mimetype, "text/") {
421+
log.Infof("Skipping attachment %s: mimetype is %s", file.Name, file.Mimetype)
422+
continue
423+
}
424+
425+
var buf bytes.Buffer
426+
log.Infof("Downloading attachment %s", file.Name)
427+
428+
if err := c.GetFile(file.URLPrivate, &buf); err != nil {
429+
log.Warnf("Failed to download attachment %s: %v", file.Name, err)
430+
continue
431+
}
432+
433+
result.WriteString(fmt.Sprintf(" <Attachment filename=\"%s\">%s</Attachment>", file.Name, buf.String()))
434+
}
435+
return result.String()
436+
}
437+
413438
// getChannelHistory fetches the last N messages from a channel (including thread messages)
414-
func (c *openaiCommand) getChannelHistory(channel string, count int) ([]slack.Message, error) {
439+
func (c *openaiCommand) getChannelHistory(channel string, count int, includeAttachments bool) ([]slack.Message, error) {
415440
params := &slack.GetConversationHistoryParameters{
416441
ChannelID: channel,
417442
Limit: count,
@@ -425,6 +450,10 @@ func (c *openaiCommand) getChannelHistory(channel string, count int) ([]slack.Me
425450
// Collect main channel messages and their thread replies
426451
allMessages := make([]slack.Message, 0)
427452
for _, message := range response.Messages {
453+
// Load text attachments if requested
454+
if includeAttachments && len(message.Files) > 0 {
455+
message.Text += c.loadTextAttachments(message.Files)
456+
}
428457
allMessages = append(allMessages, message)
429458

430459
// If this message has replies, fetch the thread messages
@@ -441,7 +470,13 @@ func (c *openaiCommand) getChannelHistory(channel string, count int) ([]slack.Me
441470
}
442471

443472
// Skip the first message as it's the parent (already added)
473+
// Also load attachments for thread messages if requested
444474
if len(threadMessages) > 1 {
475+
for i := 1; i < len(threadMessages); i++ {
476+
if includeAttachments && len(threadMessages[i].Files) > 0 {
477+
threadMessages[i].Text += c.loadTextAttachments(threadMessages[i].Files)
478+
}
479+
}
445480
allMessages = append(allMessages, threadMessages[1:]...)
446481
}
447482
}
@@ -460,7 +495,7 @@ func (c *openaiCommand) GetHelp() []bot.Help {
460495
return []bot.Help{
461496
{
462497
Command: "openai <question>",
463-
Description: "Starts a chatgpt/openai conversation in a new thread. Supports hashtags for advanced options: \n- #model-<name> (e.g., #model-gpt-4o), \n- #high-thinking/#medium-thinking/#low-thinking/#no-thinking for reasoning control\n- #message-history or #message-history-<N> to include recent channel messages as context\n- #no-streaming to disable streaming and get the full response at once\n- #no-thread to reply directly without creating a thread\n- #debug to show debug information about the request",
498+
Description: "Starts a chatgpt/openai conversation in a new thread. Supports hashtags for advanced options: \n- #model-<name> (e.g., #model-gpt-4o), \n- #high-thinking/#medium-thinking/#low-thinking/#no-thinking for reasoning control\n- #message-history or #message-history-<N> to include recent channel messages as context\n- #include-attachments to include text file attachments when using #message-history\n- #no-streaming to disable streaming and get the full response at once\n- #no-thread to reply directly without creating a thread\n- #debug to show debug information about the request",
464499
Category: category,
465500
Examples: []string{
466501
"openai why is the sky blue?",
@@ -470,6 +505,7 @@ func (c *openaiCommand) GetHelp() []bot.Help {
470505
"openai #no-streaming give me a detailed explanation",
471506
"openai #no-thread quick question without a thread",
472507
"openai #debug analyze this code for performance issues",
508+
"openai #message-history-10 #include-attachments summarize the shared files",
473509
},
474510
},
475511
{

command/openai/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func (c *Config) IsEnabled() bool {
4646

4747
var defaultConfig = Config{
4848
APIHost: apiHost,
49-
Model: "gpt-5", // aka model behind ChatGPT
49+
Model: "gpt-5.2", // aka model behind ChatGPT
5050
UpdateInterval: time.Second * 1,
5151
APITimeout: time.Second * 120,
5252
HistorySize: 25,

command/openai/hashtag_parser.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import (
44
"regexp"
55
"strconv"
66
"strings"
7+
8+
log "github.com/sirupsen/logrus"
9+
)
10+
11+
// Pre-compiled regexes for hashtag parsing
12+
var (
13+
messageHistoryWithCountRe = regexp.MustCompile(`#message-history-(\d+)`)
14+
modelRe = regexp.MustCompile(`#model-([\w.-]+)`)
715
)
816

917
// removeHashtag removes a hashtag from the text if present and returns whether it was found
@@ -17,12 +25,13 @@ func removeHashtag(text *string, hashtag string) bool {
1725

1826
// HashtagOptions contains parsed hashtag options from user input
1927
type HashtagOptions struct {
20-
ReasoningEffort string // "minimal", "medium", "high", or ""
21-
Model string // override model, empty means use config default
22-
MessageHistory int // number of channel messages to include, 0 means disabled
23-
NoStreaming bool // disable streaming responses, get full response at once
24-
NoThread bool // disable thread replies, reply directly to the message instead
25-
Debug bool // show debug information at the end of the response
28+
ReasoningEffort string // "minimal", "medium", "high", or ""
29+
Model string // override model, empty means use config default
30+
MessageHistory int // number of channel messages to include, 0 means disabled
31+
NoStreaming bool // disable streaming responses, get full response at once
32+
NoThread bool // disable thread replies, reply directly to the message instead
33+
Debug bool // show debug information at the end of the response
34+
IncludeAttachments bool // include text file attachments in message history
2635
}
2736

2837
// ParseHashtags extracts hashtag options from the input text and returns
@@ -36,7 +45,6 @@ func ParseHashtags(text string) (cleanText string, options HashtagOptions) {
3645
// 4. #high-thinking, #medium-thinking, #minimal-thinking, #no-thinking
3746

3847
// Parse message-history with number: #message-history-20
39-
messageHistoryWithCountRe := regexp.MustCompile(`#message-history-(\d+)`)
4048
if matches := messageHistoryWithCountRe.FindStringSubmatch(text); len(matches) > 1 {
4149
if count, err := strconv.Atoi(matches[1]); err == nil {
4250
options.MessageHistory = count
@@ -48,7 +56,6 @@ func ParseHashtags(text string) (cleanText string, options HashtagOptions) {
4856
}
4957

5058
// Parse model: #model-gpt-4o
51-
modelRe := regexp.MustCompile(`#model-([\w.-]+)`)
5259
if matches := modelRe.FindStringSubmatch(text); len(matches) > 1 {
5360
options.Model = matches[1]
5461
text = modelRe.ReplaceAllString(text, "")
@@ -78,8 +85,14 @@ func ParseHashtags(text string) (cleanText string, options HashtagOptions) {
7885
// Parse debug option
7986
options.Debug = removeHashtag(&text, "#debug")
8087

88+
// Parse include-attachments option
89+
options.IncludeAttachments = removeHashtag(&text, "#include-attachments")
90+
8191
// Clean up extra whitespace
8292
cleanText = strings.Join(strings.Fields(text), " ")
8393

94+
// just temporary
95+
log.Warn("Parsed Hashtag Options: ", options)
96+
8497
return cleanText, options
8598
}

command/openai/hashtag_parser_test.go

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ import (
66

77
func TestParseHashtags(t *testing.T) {
88
tests := []struct {
9-
name string
10-
input string
11-
expectedText string
12-
expectedModel string
13-
expectedReason string
14-
expectedHistory int
15-
expectedStreaming bool
16-
expectedNoThread bool
17-
expectedDebug bool
9+
name string
10+
input string
11+
expectedText string
12+
expectedModel string
13+
expectedReason string
14+
expectedHistory int
15+
expectedStreaming bool
16+
expectedNoThread bool
17+
expectedDebug bool
18+
expectedAttachments bool
1819
}{
1920
{
2021
name: "No hashtags",
@@ -132,6 +133,31 @@ func TestParseHashtags(t *testing.T) {
132133
expectedNoThread: true,
133134
expectedDebug: true,
134135
},
136+
{
137+
name: "Include attachments only",
138+
input: "#include-attachments Show me the files",
139+
expectedText: "Show me the files",
140+
expectedAttachments: true,
141+
},
142+
{
143+
name: "Include attachments with message history",
144+
input: "#message-history-10 #include-attachments Summarize the shared files",
145+
expectedText: "Summarize the shared files",
146+
expectedHistory: 10,
147+
expectedAttachments: true,
148+
},
149+
{
150+
name: "All hashtags including attachments",
151+
input: "#model-o1 #high-thinking #message-history-20 #include-attachments #no-streaming #no-thread #debug Complete request with files",
152+
expectedText: "Complete request with files",
153+
expectedModel: "o1",
154+
expectedReason: "high",
155+
expectedHistory: 20,
156+
expectedStreaming: true,
157+
expectedNoThread: true,
158+
expectedDebug: true,
159+
expectedAttachments: true,
160+
},
135161
}
136162

137163
for _, tt := range tests {
@@ -159,6 +185,9 @@ func TestParseHashtags(t *testing.T) {
159185
if options.Debug != tt.expectedDebug {
160186
t.Errorf("Expected Debug %v, got %v", tt.expectedDebug, options.Debug)
161187
}
188+
if options.IncludeAttachments != tt.expectedAttachments {
189+
t.Errorf("Expected IncludeAttachments %v, got %v", tt.expectedAttachments, options.IncludeAttachments)
190+
}
162191
})
163192
}
164193
}

mocks/SlackClient.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)