feat: add support for attachments in message tool
This commit is contained in:
parent
11008573d7
commit
ac551a0caa
5 changed files with 55 additions and 12 deletions
|
|
@ -95,11 +95,12 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
|
||||||
// Message tool - available to both agent and subagent
|
// Message tool - available to both agent and subagent
|
||||||
// Subagent uses it to communicate directly with user
|
// Subagent uses it to communicate directly with user
|
||||||
messageTool := tools.NewMessageTool()
|
messageTool := tools.NewMessageTool()
|
||||||
messageTool.SetSendCallback(func(channel, chatID, content string) error {
|
messageTool.SetSendCallback(func(channel, chatID, content string, attachments []string) error {
|
||||||
msgBus.PublishOutbound(bus.OutboundMessage{
|
msgBus.PublishOutbound(bus.OutboundMessage{
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
ChatID: chatID,
|
ChatID: chatID,
|
||||||
Content: content,
|
Content: content,
|
||||||
|
Media: attachments,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ type InboundMessage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutboundMessage struct {
|
type OutboundMessage struct {
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
ChatID string `json:"chat_id"`
|
ChatID string `json:"chat_id"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Media []string `json:"media,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageHandler func(InboundMessage) error
|
type MessageHandler func(InboundMessage) error
|
||||||
|
|
|
||||||
|
|
@ -101,12 +101,24 @@ func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
||||||
return fmt.Errorf("channel ID is empty")
|
return fmt.Errorf("channel ID is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, mediaPath := range msg.Media {
|
||||||
|
file, err := os.Open(mediaPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open media file %s: %w", mediaPath, err)
|
||||||
|
}
|
||||||
|
_, err = c.session.ChannelFileSend(channelID, mediaPath, file)
|
||||||
|
file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send media file %s: %w", mediaPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
runes := []rune(msg.Content)
|
runes := []rune(msg.Content)
|
||||||
if len(runes) == 0 {
|
if len(runes) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks := splitMessage(msg.Content, 1500) // Discord has a limit of 2000 characters per message, leave 500 for natural split e.g. code blocks
|
chunks := splitMessage(msg.Content, 1500)
|
||||||
|
|
||||||
for _, chunk := range chunks {
|
for _, chunk := range chunks {
|
||||||
if err := c.sendChunk(ctx, channelID, chunk); err != nil {
|
if err := c.sendChunk(ctx, channelID, chunk); err != nil {
|
||||||
|
|
@ -253,7 +265,19 @@ func (c *DiscordChannel) sendChunk(ctx context.Context, channelID, content strin
|
||||||
|
|
||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
_, err := c.session.ChannelMessageSend(channelID, content)
|
var err error
|
||||||
|
if strings.HasPrefix(content, "file://") {
|
||||||
|
filePath := strings.TrimPrefix(content, "file://")
|
||||||
|
file, fErr := os.Open(filePath)
|
||||||
|
if fErr != nil {
|
||||||
|
done <- fErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
_, err = c.session.ChannelFileSend(channelID, filePath, file)
|
||||||
|
} else {
|
||||||
|
_, err = c.session.ChannelMessageSend(channelID, content)
|
||||||
|
}
|
||||||
done <- err
|
done <- err
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SendCallback func(channel, chatID, content string) error
|
type SendCallback func(channel, chatID, content string, attachments []string) error
|
||||||
|
|
||||||
type MessageTool struct {
|
type MessageTool struct {
|
||||||
sendCallback SendCallback
|
sendCallback SendCallback
|
||||||
|
|
@ -42,6 +42,11 @@ func (t *MessageTool) Parameters() map[string]interface{} {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional: target chat/user ID",
|
"description": "Optional: target chat/user ID",
|
||||||
},
|
},
|
||||||
|
"attachments": map[string]interface{}{
|
||||||
|
"type": "array",
|
||||||
|
"items": map[string]interface{}{"type": "string"},
|
||||||
|
"description": "Optional: list of local file paths or URLs to attach",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"content"},
|
"required": []string{"content"},
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +75,13 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{})
|
||||||
|
|
||||||
channel, _ := args["channel"].(string)
|
channel, _ := args["channel"].(string)
|
||||||
chatID, _ := args["chat_id"].(string)
|
chatID, _ := args["chat_id"].(string)
|
||||||
|
attachmentsRaw, _ := args["attachments"].([]interface{})
|
||||||
|
var attachments []string
|
||||||
|
for _, a := range attachmentsRaw {
|
||||||
|
if s, ok := a.(string); ok {
|
||||||
|
attachments = append(attachments, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if channel == "" {
|
if channel == "" {
|
||||||
channel = t.defaultChannel
|
channel = t.defaultChannel
|
||||||
|
|
@ -86,7 +98,7 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{})
|
||||||
return &ToolResult{ForLLM: "Message sending not configured", IsError: true}
|
return &ToolResult{ForLLM: "Message sending not configured", IsError: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := t.sendCallback(channel, chatID, content); err != nil {
|
if err := t.sendCallback(channel, chatID, content, attachments); err != nil {
|
||||||
return &ToolResult{
|
return &ToolResult{
|
||||||
ForLLM: fmt.Sprintf("sending message: %v", err),
|
ForLLM: fmt.Sprintf("sending message: %v", err),
|
||||||
IsError: true,
|
IsError: true,
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,12 @@ func TestMessageTool_Execute_Success(t *testing.T) {
|
||||||
tool.SetContext("test-channel", "test-chat-id")
|
tool.SetContext("test-channel", "test-chat-id")
|
||||||
|
|
||||||
var sentChannel, sentChatID, sentContent string
|
var sentChannel, sentChatID, sentContent string
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
var sentAttachments []string
|
||||||
|
tool.SetSendCallback(func(channel, chatID, content string, attachments []string) error {
|
||||||
sentChannel = channel
|
sentChannel = channel
|
||||||
sentChatID = chatID
|
sentChatID = chatID
|
||||||
sentContent = content
|
sentContent = content
|
||||||
|
sentAttachments = attachments
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -35,6 +37,9 @@ func TestMessageTool_Execute_Success(t *testing.T) {
|
||||||
if sentContent != "Hello, world!" {
|
if sentContent != "Hello, world!" {
|
||||||
t.Errorf("Expected content 'Hello, world!', got '%s'", sentContent)
|
t.Errorf("Expected content 'Hello, world!', got '%s'", sentContent)
|
||||||
}
|
}
|
||||||
|
if len(sentAttachments) != 0 {
|
||||||
|
t.Errorf("Expected 0 attachments, got %d", len(sentAttachments))
|
||||||
|
}
|
||||||
|
|
||||||
// Verify ToolResult meets US-011 criteria:
|
// Verify ToolResult meets US-011 criteria:
|
||||||
// - Send success returns SilentResult (Silent=true)
|
// - Send success returns SilentResult (Silent=true)
|
||||||
|
|
@ -63,7 +68,7 @@ func TestMessageTool_Execute_WithCustomChannel(t *testing.T) {
|
||||||
tool.SetContext("default-channel", "default-chat-id")
|
tool.SetContext("default-channel", "default-chat-id")
|
||||||
|
|
||||||
var sentChannel, sentChatID string
|
var sentChannel, sentChatID string
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
tool.SetSendCallback(func(channel, chatID, content string, attachments []string) error {
|
||||||
sentChannel = channel
|
sentChannel = channel
|
||||||
sentChatID = chatID
|
sentChatID = chatID
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -99,7 +104,7 @@ func TestMessageTool_Execute_SendFailure(t *testing.T) {
|
||||||
tool.SetContext("test-channel", "test-chat-id")
|
tool.SetContext("test-channel", "test-chat-id")
|
||||||
|
|
||||||
sendErr := errors.New("network error")
|
sendErr := errors.New("network error")
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
tool.SetSendCallback(func(channel, chatID, content string, attachments []string) error {
|
||||||
return sendErr
|
return sendErr
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -153,7 +158,7 @@ func TestMessageTool_Execute_NoTargetChannel(t *testing.T) {
|
||||||
tool := NewMessageTool()
|
tool := NewMessageTool()
|
||||||
// No SetContext called, so defaultChannel and defaultChatID are empty
|
// No SetContext called, so defaultChannel and defaultChatID are empty
|
||||||
|
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
tool.SetSendCallback(func(channel, chatID, content string, attachments []string) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue