feat(tools): add reaction tool and reply-aware message sends (#2156)
- Add `reaction` tool that reacts to a message (defaults to current inbound message via context) - Extend `message` tool with optional `reply_to_message_id` parameter - Introduce `WithToolInboundContext` to inject inbound message IDs into tool execution context - Surface `MessageID` and `ReplyToMessageID` in `processOptions` for tool-surface consumption Refs #2137
This commit is contained in:
parent
5e7545a22a
commit
e88df4ff9c
8 changed files with 338 additions and 13 deletions
|
|
@ -75,6 +75,8 @@ type processOptions struct {
|
||||||
SessionKey string // Session identifier for history/context
|
SessionKey string // Session identifier for history/context
|
||||||
Channel string // Target channel for tool execution
|
Channel string // Target channel for tool execution
|
||||||
ChatID string // Target chat ID for tool execution
|
ChatID string // Target chat ID for tool execution
|
||||||
|
MessageID string // Current inbound platform message ID
|
||||||
|
ReplyToMessageID string // Current inbound reply target message ID
|
||||||
SenderID string // Current sender ID for dynamic context
|
SenderID string // Current sender ID for dynamic context
|
||||||
SenderDisplayName string // Current sender display name for dynamic context
|
SenderDisplayName string // Current sender display name for dynamic context
|
||||||
UserMessage string // User message content (may include prefix)
|
UserMessage string // User message content (may include prefix)
|
||||||
|
|
@ -104,6 +106,7 @@ const (
|
||||||
metadataKeyAccountID = "account_id"
|
metadataKeyAccountID = "account_id"
|
||||||
metadataKeyGuildID = "guild_id"
|
metadataKeyGuildID = "guild_id"
|
||||||
metadataKeyTeamID = "team_id"
|
metadataKeyTeamID = "team_id"
|
||||||
|
metadataKeyReplyToMessage = "reply_to_message_id"
|
||||||
metadataKeyParentPeerKind = "parent_peer_kind"
|
metadataKeyParentPeerKind = "parent_peer_kind"
|
||||||
metadataKeyParentPeerID = "parent_peer_id"
|
metadataKeyParentPeerID = "parent_peer_id"
|
||||||
)
|
)
|
||||||
|
|
@ -222,17 +225,37 @@ func registerSharedTools(
|
||||||
// Message tool
|
// Message tool
|
||||||
if cfg.Tools.IsToolEnabled("message") {
|
if cfg.Tools.IsToolEnabled("message") {
|
||||||
messageTool := tools.NewMessageTool()
|
messageTool := tools.NewMessageTool()
|
||||||
messageTool.SetSendCallback(func(channel, chatID, content string) error {
|
messageTool.SetSendCallback(func(channel, chatID, content, replyToMessageID string) error {
|
||||||
pubCtx, pubCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
pubCtx, pubCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer pubCancel()
|
defer pubCancel()
|
||||||
return msgBus.PublishOutbound(pubCtx, bus.OutboundMessage{
|
return msgBus.PublishOutbound(pubCtx, bus.OutboundMessage{
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
ChatID: chatID,
|
ChatID: chatID,
|
||||||
Content: content,
|
Content: content,
|
||||||
|
ReplyToMessageID: replyToMessageID,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
agent.Tools.Register(messageTool)
|
agent.Tools.Register(messageTool)
|
||||||
}
|
}
|
||||||
|
if cfg.Tools.IsToolEnabled("reaction") {
|
||||||
|
reactionTool := tools.NewReactionTool()
|
||||||
|
reactionTool.SetReactionCallback(func(ctx context.Context, channel, chatID, messageID string) error {
|
||||||
|
if al.channelManager == nil {
|
||||||
|
return fmt.Errorf("channel manager not configured")
|
||||||
|
}
|
||||||
|
ch, ok := al.channelManager.GetChannel(channel)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("channel %s not found", channel)
|
||||||
|
}
|
||||||
|
rc, ok := ch.(channels.ReactionCapable)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("channel %s does not support reactions", channel)
|
||||||
|
}
|
||||||
|
_, err := rc.ReactToMessage(ctx, chatID, messageID)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
agent.Tools.Register(reactionTool)
|
||||||
|
}
|
||||||
|
|
||||||
// Send file tool (outbound media via MediaStore — store injected later by SetMediaStore)
|
// Send file tool (outbound media via MediaStore — store injected later by SetMediaStore)
|
||||||
if cfg.Tools.IsToolEnabled("send_file") {
|
if cfg.Tools.IsToolEnabled("send_file") {
|
||||||
|
|
@ -1315,6 +1338,8 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||||
SessionKey: sessionKey,
|
SessionKey: sessionKey,
|
||||||
Channel: msg.Channel,
|
Channel: msg.Channel,
|
||||||
ChatID: msg.ChatID,
|
ChatID: msg.ChatID,
|
||||||
|
MessageID: msg.MessageID,
|
||||||
|
ReplyToMessageID: inboundMetadata(msg, metadataKeyReplyToMessage),
|
||||||
SenderID: msg.SenderID,
|
SenderID: msg.SenderID,
|
||||||
SenderDisplayName: msg.Sender.DisplayName,
|
SenderDisplayName: msg.Sender.DisplayName,
|
||||||
UserMessage: msg.Content,
|
UserMessage: msg.Content,
|
||||||
|
|
@ -2384,8 +2409,15 @@ turnLoop:
|
||||||
}
|
}
|
||||||
|
|
||||||
toolStart := time.Now()
|
toolStart := time.Now()
|
||||||
toolResult := ts.agent.Tools.ExecuteWithContext(
|
execCtx := tools.WithToolInboundContext(
|
||||||
turnCtx,
|
turnCtx,
|
||||||
|
ts.channel,
|
||||||
|
ts.chatID,
|
||||||
|
ts.opts.MessageID,
|
||||||
|
ts.opts.ReplyToMessageID,
|
||||||
|
)
|
||||||
|
toolResult := ts.agent.Tools.ExecuteWithContext(
|
||||||
|
execCtx,
|
||||||
toolName,
|
toolName,
|
||||||
toolArgs,
|
toolArgs,
|
||||||
ts.channel,
|
ts.channel,
|
||||||
|
|
|
||||||
|
|
@ -531,6 +531,20 @@ func TestToolContext_Updates(t *testing.T) {
|
||||||
if got := tools.ToolChannel(context.Background()); got != "" {
|
if got := tools.ToolChannel(context.Background()); got != "" {
|
||||||
t.Errorf("expected empty channel from bare context, got %q", got)
|
t.Errorf("expected empty channel from bare context, got %q", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inboundCtx := tools.WithToolInboundContext(
|
||||||
|
context.Background(),
|
||||||
|
"telegram",
|
||||||
|
"chat-42",
|
||||||
|
"msg-123",
|
||||||
|
"msg-100",
|
||||||
|
)
|
||||||
|
if got := tools.ToolMessageID(inboundCtx); got != "msg-123" {
|
||||||
|
t.Errorf("expected messageID 'msg-123', got %q", got)
|
||||||
|
}
|
||||||
|
if got := tools.ToolReplyToMessageID(inboundCtx); got != "msg-100" {
|
||||||
|
t.Errorf("expected replyToMessageID 'msg-100', got %q", got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestToolRegistry_GetDefinitions verifies tool definitions can be retrieved
|
// TestToolRegistry_GetDefinitions verifies tool definitions can be retrieved
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ type Tool interface {
|
||||||
type toolCtxKey struct{ name string }
|
type toolCtxKey struct{ name string }
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctxKeyChannel = &toolCtxKey{"channel"}
|
ctxKeyChannel = &toolCtxKey{"channel"}
|
||||||
ctxKeyChatID = &toolCtxKey{"chatID"}
|
ctxKeyChatID = &toolCtxKey{"chatID"}
|
||||||
|
ctxKeyMessageID = &toolCtxKey{"messageID"}
|
||||||
|
ctxKeyReplyToMessageID = &toolCtxKey{"replyToMessageID"}
|
||||||
)
|
)
|
||||||
|
|
||||||
// WithToolContext returns a child context carrying channel and chatID.
|
// WithToolContext returns a child context carrying channel and chatID.
|
||||||
|
|
@ -32,6 +34,23 @@ func WithToolContext(ctx context.Context, channel, chatID string) context.Contex
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithToolMessageContext returns a child context carrying inbound message IDs.
|
||||||
|
func WithToolMessageContext(ctx context.Context, messageID, replyToMessageID string) context.Context {
|
||||||
|
ctx = context.WithValue(ctx, ctxKeyMessageID, messageID)
|
||||||
|
ctx = context.WithValue(ctx, ctxKeyReplyToMessageID, replyToMessageID)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithToolInboundContext returns a child context carrying channel/chat and inbound IDs.
|
||||||
|
func WithToolInboundContext(
|
||||||
|
ctx context.Context,
|
||||||
|
channel, chatID, messageID, replyToMessageID string,
|
||||||
|
) context.Context {
|
||||||
|
ctx = WithToolContext(ctx, channel, chatID)
|
||||||
|
ctx = WithToolMessageContext(ctx, messageID, replyToMessageID)
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
// ToolChannel extracts the channel from ctx, or "" if unset.
|
// ToolChannel extracts the channel from ctx, or "" if unset.
|
||||||
func ToolChannel(ctx context.Context) string {
|
func ToolChannel(ctx context.Context) string {
|
||||||
v, _ := ctx.Value(ctxKeyChannel).(string)
|
v, _ := ctx.Value(ctxKeyChannel).(string)
|
||||||
|
|
@ -44,6 +63,18 @@ func ToolChatID(ctx context.Context) string {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToolMessageID extracts the current inbound message ID from ctx, or "" if unset.
|
||||||
|
func ToolMessageID(ctx context.Context) string {
|
||||||
|
v, _ := ctx.Value(ctxKeyMessageID).(string)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolReplyToMessageID extracts the current inbound reply target from ctx, or "" if unset.
|
||||||
|
func ToolReplyToMessageID(ctx context.Context) string {
|
||||||
|
v, _ := ctx.Value(ctxKeyReplyToMessageID).(string)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
// AsyncCallback is a function type that async tools use to notify completion.
|
// AsyncCallback is a function type that async tools use to notify completion.
|
||||||
// When an async tool finishes its work, it calls this callback with the result.
|
// When an async tool finishes its work, it calls this callback with the result.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SendCallback func(channel, chatID, content string) error
|
type SendCallback func(channel, chatID, content, replyToMessageID string) error
|
||||||
|
|
||||||
type MessageTool struct {
|
type MessageTool struct {
|
||||||
sendCallback SendCallback
|
sendCallback SendCallback
|
||||||
|
|
@ -41,6 +41,10 @@ func (t *MessageTool) Parameters() map[string]any {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional: target chat/user ID",
|
"description": "Optional: target chat/user ID",
|
||||||
},
|
},
|
||||||
|
"reply_to_message_id": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional: reply target message ID for channels that support threaded replies",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"content"},
|
"required": []string{"content"},
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +73,7 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolRes
|
||||||
|
|
||||||
channel, _ := args["channel"].(string)
|
channel, _ := args["channel"].(string)
|
||||||
chatID, _ := args["chat_id"].(string)
|
chatID, _ := args["chat_id"].(string)
|
||||||
|
replyToMessageID, _ := args["reply_to_message_id"].(string)
|
||||||
|
|
||||||
if channel == "" {
|
if channel == "" {
|
||||||
channel = ToolChannel(ctx)
|
channel = ToolChannel(ctx)
|
||||||
|
|
@ -85,7 +90,7 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolRes
|
||||||
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, replyToMessageID); err != nil {
|
||||||
return &ToolResult{
|
return &ToolResult{
|
||||||
ForLLM: fmt.Sprintf("sending message: %v", err),
|
ForLLM: fmt.Sprintf("sending message: %v", err),
|
||||||
IsError: true,
|
IsError: true,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ func TestMessageTool_Execute_Success(t *testing.T) {
|
||||||
tool := NewMessageTool()
|
tool := NewMessageTool()
|
||||||
|
|
||||||
var sentChannel, sentChatID, sentContent string
|
var sentChannel, sentChatID, sentContent string
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
tool.SetSendCallback(func(channel, chatID, content, replyToMessageID string) error {
|
||||||
sentChannel = channel
|
sentChannel = channel
|
||||||
sentChatID = chatID
|
sentChatID = chatID
|
||||||
sentContent = content
|
sentContent = content
|
||||||
|
|
@ -61,7 +61,7 @@ func TestMessageTool_Execute_WithCustomChannel(t *testing.T) {
|
||||||
tool := NewMessageTool()
|
tool := NewMessageTool()
|
||||||
|
|
||||||
var sentChannel, sentChatID string
|
var sentChannel, sentChatID string
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
tool.SetSendCallback(func(channel, chatID, content, replyToMessageID string) error {
|
||||||
sentChannel = channel
|
sentChannel = channel
|
||||||
sentChatID = chatID
|
sentChatID = chatID
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -96,7 +96,7 @@ func TestMessageTool_Execute_SendFailure(t *testing.T) {
|
||||||
tool := NewMessageTool()
|
tool := NewMessageTool()
|
||||||
|
|
||||||
sendErr := errors.New("network error")
|
sendErr := errors.New("network error")
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
tool.SetSendCallback(func(channel, chatID, content, replyToMessageID string) error {
|
||||||
return sendErr
|
return sendErr
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -149,7 +149,7 @@ func TestMessageTool_Execute_NoTargetChannel(t *testing.T) {
|
||||||
tool := NewMessageTool()
|
tool := NewMessageTool()
|
||||||
// No WithToolContext — channel/chatID are empty
|
// No WithToolContext — channel/chatID are empty
|
||||||
|
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
tool.SetSendCallback(func(channel, chatID, content, replyToMessageID string) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -251,4 +251,37 @@ func TestMessageTool_Parameters(t *testing.T) {
|
||||||
if chatIDProp["type"] != "string" {
|
if chatIDProp["type"] != "string" {
|
||||||
t.Error("Expected chat_id type to be 'string'")
|
t.Error("Expected chat_id type to be 'string'")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check reply_to_message_id property (optional)
|
||||||
|
replyToProp, ok := props["reply_to_message_id"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Error("Expected 'reply_to_message_id' property")
|
||||||
|
}
|
||||||
|
if replyToProp["type"] != "string" {
|
||||||
|
t.Error("Expected reply_to_message_id type to be 'string'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessageTool_Execute_WithReplyToMessageID(t *testing.T) {
|
||||||
|
tool := NewMessageTool()
|
||||||
|
|
||||||
|
var sentReplyTo string
|
||||||
|
tool.SetSendCallback(func(channel, chatID, content, replyToMessageID string) error {
|
||||||
|
sentReplyTo = replyToMessageID
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := WithToolContext(context.Background(), "test-channel", "test-chat-id")
|
||||||
|
args := map[string]any{
|
||||||
|
"content": "Reply test",
|
||||||
|
"reply_to_message_id": "msg-123",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := tool.Execute(ctx, args)
|
||||||
|
if result.IsError {
|
||||||
|
t.Fatalf("expected success, got error: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
if sentReplyTo != "msg-123" {
|
||||||
|
t.Fatalf("expected reply_to_message_id msg-123, got %q", sentReplyTo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
87
pkg/tools/reaction.go
Normal file
87
pkg/tools/reaction.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReactionCallback func(ctx context.Context, channel, chatID, messageID string) error
|
||||||
|
|
||||||
|
type ReactionTool struct {
|
||||||
|
reactionCallback ReactionCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReactionTool() *ReactionTool {
|
||||||
|
return &ReactionTool{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ReactionTool) Name() string {
|
||||||
|
return "reaction"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ReactionTool) Description() string {
|
||||||
|
return "Add a reaction to a message. Defaults to the current inbound message when message_id is omitted."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ReactionTool) Parameters() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]any{
|
||||||
|
"message_id": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional: target message ID; defaults to the current inbound message",
|
||||||
|
},
|
||||||
|
"channel": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional: target channel (telegram, whatsapp, etc.)",
|
||||||
|
},
|
||||||
|
"chat_id": map[string]any{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional: target chat/user ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ReactionTool) SetReactionCallback(callback ReactionCallback) {
|
||||||
|
t.reactionCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ReactionTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||||
|
channel, _ := args["channel"].(string)
|
||||||
|
chatID, _ := args["chat_id"].(string)
|
||||||
|
messageID, _ := args["message_id"].(string)
|
||||||
|
|
||||||
|
if channel == "" {
|
||||||
|
channel = ToolChannel(ctx)
|
||||||
|
}
|
||||||
|
if chatID == "" {
|
||||||
|
chatID = ToolChatID(ctx)
|
||||||
|
}
|
||||||
|
if messageID == "" {
|
||||||
|
messageID = ToolMessageID(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel == "" || chatID == "" {
|
||||||
|
return &ToolResult{ForLLM: "No target channel/chat specified", IsError: true}
|
||||||
|
}
|
||||||
|
if messageID == "" {
|
||||||
|
return &ToolResult{ForLLM: "message_id is required", IsError: true}
|
||||||
|
}
|
||||||
|
if t.reactionCallback == nil {
|
||||||
|
return &ToolResult{ForLLM: "Reaction not configured", IsError: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.reactionCallback(ctx, channel, chatID, messageID); err != nil {
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: fmt.Sprintf("adding reaction: %v", err),
|
||||||
|
IsError: true,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ToolResult{
|
||||||
|
ForLLM: fmt.Sprintf("Reaction added to %s:%s message %s", channel, chatID, messageID),
|
||||||
|
Silent: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
96
pkg/tools/reaction_test.go
Normal file
96
pkg/tools/reaction_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReactionTool_Execute_UsesContextMessageIDByDefault(t *testing.T) {
|
||||||
|
tool := NewReactionTool()
|
||||||
|
|
||||||
|
var gotChannel, gotChatID, gotMessageID string
|
||||||
|
tool.SetReactionCallback(func(ctx context.Context, channel, chatID, messageID string) error {
|
||||||
|
gotChannel = channel
|
||||||
|
gotChatID = chatID
|
||||||
|
gotMessageID = messageID
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := WithToolInboundContext(context.Background(), "telegram", "chat-1", "msg-100", "")
|
||||||
|
result := tool.Execute(ctx, map[string]any{})
|
||||||
|
if result.IsError {
|
||||||
|
t.Fatalf("expected success, got error: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
if gotChannel != "telegram" || gotChatID != "chat-1" || gotMessageID != "msg-100" {
|
||||||
|
t.Fatalf("unexpected callback args: channel=%q chatID=%q messageID=%q", gotChannel, gotChatID, gotMessageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReactionTool_Execute_AllowsExplicitMessageIDOverride(t *testing.T) {
|
||||||
|
tool := NewReactionTool()
|
||||||
|
|
||||||
|
var gotMessageID string
|
||||||
|
tool.SetReactionCallback(func(ctx context.Context, channel, chatID, messageID string) error {
|
||||||
|
gotMessageID = messageID
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := WithToolInboundContext(context.Background(), "telegram", "chat-1", "msg-context", "")
|
||||||
|
result := tool.Execute(ctx, map[string]any{"message_id": "msg-explicit"})
|
||||||
|
if result.IsError {
|
||||||
|
t.Fatalf("expected success, got error: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
if gotMessageID != "msg-explicit" {
|
||||||
|
t.Fatalf("expected explicit message id, got %q", gotMessageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReactionTool_Execute_MissingMessageID(t *testing.T) {
|
||||||
|
tool := NewReactionTool()
|
||||||
|
tool.SetReactionCallback(func(ctx context.Context, channel, chatID, messageID string) error { return nil })
|
||||||
|
|
||||||
|
ctx := WithToolContext(context.Background(), "telegram", "chat-1")
|
||||||
|
result := tool.Execute(ctx, map[string]any{})
|
||||||
|
if !result.IsError {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
if result.ForLLM != "message_id is required" {
|
||||||
|
t.Fatalf("unexpected error message: %q", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReactionTool_Execute_CallbackError(t *testing.T) {
|
||||||
|
tool := NewReactionTool()
|
||||||
|
tool.SetReactionCallback(func(ctx context.Context, channel, chatID, messageID string) error {
|
||||||
|
return errors.New("unsupported")
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := WithToolInboundContext(context.Background(), "telegram", "chat-1", "msg-100", "")
|
||||||
|
result := tool.Execute(ctx, map[string]any{})
|
||||||
|
if !result.IsError {
|
||||||
|
t.Fatal("expected error")
|
||||||
|
}
|
||||||
|
if result.Err == nil {
|
||||||
|
t.Fatal("expected wrapped error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReactionTool_Parameters(t *testing.T) {
|
||||||
|
tool := NewReactionTool()
|
||||||
|
params := tool.Parameters()
|
||||||
|
|
||||||
|
props, ok := params["properties"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected properties map")
|
||||||
|
}
|
||||||
|
if _, ok := props["message_id"]; !ok {
|
||||||
|
t.Fatal("expected message_id parameter")
|
||||||
|
}
|
||||||
|
if _, ok := props["channel"]; !ok {
|
||||||
|
t.Fatal("expected channel parameter")
|
||||||
|
}
|
||||||
|
if _, ok := props["chat_id"]; !ok {
|
||||||
|
t.Fatal("expected chat_id parameter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -190,6 +190,33 @@ func TestToolRegistry_ExecuteWithContext_EmptyContext(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToolRegistry_ExecuteWithContext_PreservesMessageContext(t *testing.T) {
|
||||||
|
r := NewToolRegistry()
|
||||||
|
ct := &mockContextAwareTool{
|
||||||
|
mockRegistryTool: *newMockTool("ctx_tool", "needs context"),
|
||||||
|
}
|
||||||
|
r.Register(ct)
|
||||||
|
|
||||||
|
baseCtx := WithToolMessageContext(context.Background(), "msg-123", "msg-100")
|
||||||
|
r.ExecuteWithContext(baseCtx, "ctx_tool", nil, "telegram", "chat-42", nil)
|
||||||
|
|
||||||
|
if ct.lastCtx == nil {
|
||||||
|
t.Fatal("expected Execute to be called")
|
||||||
|
}
|
||||||
|
if got := ToolChannel(ct.lastCtx); got != "telegram" {
|
||||||
|
t.Errorf("expected channel 'telegram', got %q", got)
|
||||||
|
}
|
||||||
|
if got := ToolChatID(ct.lastCtx); got != "chat-42" {
|
||||||
|
t.Errorf("expected chatID 'chat-42', got %q", got)
|
||||||
|
}
|
||||||
|
if got := ToolMessageID(ct.lastCtx); got != "msg-123" {
|
||||||
|
t.Errorf("expected messageID 'msg-123', got %q", got)
|
||||||
|
}
|
||||||
|
if got := ToolReplyToMessageID(ct.lastCtx); got != "msg-100" {
|
||||||
|
t.Errorf("expected replyToMessageID 'msg-100', got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestToolRegistry_ExecuteWithContext_AsyncCallback(t *testing.T) {
|
func TestToolRegistry_ExecuteWithContext_AsyncCallback(t *testing.T) {
|
||||||
r := NewToolRegistry()
|
r := NewToolRegistry()
|
||||||
at := &mockAsyncRegistryTool{
|
at := &mockAsyncRegistryTool{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue