Merge a7df4e8cee into 8807d8254f
This commit is contained in:
commit
2a5e5aea13
15 changed files with 343 additions and 16 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -44,3 +44,5 @@ tasks/
|
||||||
|
|
||||||
# Added by goreleaser init:
|
# Added by goreleaser init:
|
||||||
dist/
|
dist/
|
||||||
|
.env
|
||||||
|
config/config.json
|
||||||
|
|
|
||||||
|
|
@ -405,6 +405,9 @@ func agentCmd() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply workspace upgrades before starting the agent
|
||||||
|
migrate.UpgradeWorkspace(cfg.WorkspacePath())
|
||||||
|
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
|
@ -540,6 +543,9 @@ func gatewayCmd() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply workspace upgrades before starting the agent
|
||||||
|
migrate.UpgradeWorkspace(cfg.WorkspacePath())
|
||||||
|
|
||||||
msgBus := bus.NewMessageBus()
|
msgBus := bus.NewMessageBus()
|
||||||
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
|
||||||
|
|
||||||
|
|
|
||||||
10
docker-compose.override.yml
Normal file
10
docker-compose.override.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# docker-compose.override.yml
|
||||||
|
services:
|
||||||
|
picoclaw-agent:
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 64M
|
||||||
|
cpus: '0.5'
|
||||||
|
picoclaw-gateway:
|
||||||
|
restart: unless-stopped
|
||||||
|
|
@ -140,7 +140,7 @@ The following skills extend your capabilities. To use a skill, read its SKILL.md
|
||||||
|
|
||||||
func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
func (cb *ContextBuilder) LoadBootstrapFiles() string {
|
||||||
bootstrapFiles := []string{
|
bootstrapFiles := []string{
|
||||||
"AGENTS.md",
|
"AGENT.md",
|
||||||
"SOUL.md",
|
"SOUL.md",
|
||||||
"USER.md",
|
"USER.md",
|
||||||
"IDENTITY.md",
|
"IDENTITY.md",
|
||||||
|
|
|
||||||
|
|
@ -94,11 +94,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, media []string) error {
|
||||||
msgBus.PublishOutbound(bus.OutboundMessage{
|
msgBus.PublishOutbound(bus.OutboundMessage{
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
ChatID: chatID,
|
ChatID: chatID,
|
||||||
Content: content,
|
Content: content,
|
||||||
|
Media: media,
|
||||||
})
|
})
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -114,6 +115,32 @@ func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send media files
|
||||||
|
for _, mediaPath := range msg.Media {
|
||||||
|
f, err := os.Open(mediaPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("discord", "Failed to open media file", map[string]any{
|
||||||
|
"path": mediaPath,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msgSend := &discordgo.MessageSend{
|
||||||
|
Files: []*discordgo.File{{
|
||||||
|
Name: filepath.Base(mediaPath),
|
||||||
|
Reader: f,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
_, sendErr := c.session.ChannelMessageSendComplex(channelID, msgSend)
|
||||||
|
f.Close()
|
||||||
|
if sendErr != nil {
|
||||||
|
logger.ErrorCF("discord", "Failed to send media file", map[string]any{
|
||||||
|
"path": mediaPath,
|
||||||
|
"error": sendErr.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -130,6 +131,40 @@ func (c *SlackChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
return fmt.Errorf("failed to send slack message: %w", err)
|
return fmt.Errorf("failed to send slack message: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upload media files
|
||||||
|
for _, mediaPath := range msg.Media {
|
||||||
|
fi, err := os.Stat(mediaPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("slack", "Failed to stat media file", map[string]interface{}{
|
||||||
|
"path": mediaPath,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f, err := os.Open(mediaPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("slack", "Failed to open media file", map[string]interface{}{
|
||||||
|
"path": mediaPath,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err = c.api.UploadFileV2Context(ctx, slack.UploadFileV2Parameters{
|
||||||
|
Reader: f,
|
||||||
|
FileSize: int(fi.Size()),
|
||||||
|
Filename: filepath.Base(mediaPath),
|
||||||
|
Channel: channelID,
|
||||||
|
ThreadTimestamp: threadTS,
|
||||||
|
})
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("slack", "Failed to upload media file", map[string]interface{}{
|
||||||
|
"path": mediaPath,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ref, ok := c.pendingAcks.LoadAndDelete(msg.ChatID); ok {
|
if ref, ok := c.pendingAcks.LoadAndDelete(msg.ChatID); ok {
|
||||||
msgRef := ref.(slackMessageRef)
|
msgRef := ref.(slackMessageRef)
|
||||||
c.api.AddReaction("white_check_mark", slack.ItemRef{
|
c.api.AddReaction("white_check_mark", slack.ItemRef{
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
@ -180,12 +181,65 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
})
|
})
|
||||||
tgMsg.ParseMode = ""
|
tgMsg.ParseMode = ""
|
||||||
_, err = c.bot.SendMessage(ctx, tgMsg)
|
_, err = c.bot.SendMessage(ctx, tgMsg)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send media files
|
||||||
|
for _, mediaPath := range msg.Media {
|
||||||
|
if err := c.sendMediaFile(ctx, chatID, mediaPath); err != nil {
|
||||||
|
logger.ErrorCF("telegram", "Failed to send media file", map[string]interface{}{
|
||||||
|
"path": mediaPath,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TelegramChannel) sendMediaFile(ctx context.Context, chatID int64, filePath string) error {
|
||||||
|
f, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open media file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
fileName := filepath.Base(filePath)
|
||||||
|
ext := strings.ToLower(filepath.Ext(filePath))
|
||||||
|
|
||||||
|
switch ext {
|
||||||
|
case ".jpg", ".jpeg", ".png", ".gif", ".webp":
|
||||||
|
photo := &telego.SendPhotoParams{
|
||||||
|
ChatID: tu.ID(chatID),
|
||||||
|
Photo: telego.InputFile{File: f},
|
||||||
|
}
|
||||||
|
_, err = c.bot.SendPhoto(ctx, photo)
|
||||||
|
case ".mp4", ".avi", ".mov", ".mkv":
|
||||||
|
video := &telego.SendVideoParams{
|
||||||
|
ChatID: tu.ID(chatID),
|
||||||
|
Video: telego.InputFile{File: f},
|
||||||
|
}
|
||||||
|
_, err = c.bot.SendVideo(ctx, video)
|
||||||
|
case ".mp3", ".ogg", ".wav", ".flac", ".aac", ".m4a":
|
||||||
|
audio := &telego.SendAudioParams{
|
||||||
|
ChatID: tu.ID(chatID),
|
||||||
|
Audio: telego.InputFile{File: f},
|
||||||
|
}
|
||||||
|
_, err = c.bot.SendAudio(ctx, audio)
|
||||||
|
default:
|
||||||
|
doc := &telego.SendDocumentParams{
|
||||||
|
ChatID: tu.ID(chatID),
|
||||||
|
Document: telego.InputFile{File: f},
|
||||||
|
Caption: fileName,
|
||||||
|
}
|
||||||
|
_, err = c.bot.SendDocument(ctx, doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Message) error {
|
func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Message) error {
|
||||||
if message == nil {
|
if message == nil {
|
||||||
return fmt.Errorf("message is nil")
|
return fmt.Errorf("message is nil")
|
||||||
|
|
|
||||||
96
pkg/migrate/upgrade.go
Normal file
96
pkg/migrate/upgrade.go
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
package migrate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpgradeWorkspace applies incremental upgrades to an existing workspace.
|
||||||
|
// Each upgrade is idempotent — it checks whether it has already been applied
|
||||||
|
// before making changes.
|
||||||
|
func UpgradeWorkspace(workspace string) {
|
||||||
|
upgradeAgentMediaSection(workspace)
|
||||||
|
upgradeAgentGrounding(workspace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgradeAgentMediaSection ensures AGENT.md contains the media sending instructions.
|
||||||
|
// Added in v0.x to teach the LLM it can send files via the message tool.
|
||||||
|
func upgradeAgentMediaSection(workspace string) {
|
||||||
|
agentPath := filepath.Join(workspace, "AGENT.md")
|
||||||
|
|
||||||
|
data, err := os.ReadFile(agentPath)
|
||||||
|
if err != nil {
|
||||||
|
return // File doesn't exist or unreadable — skip
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
// Already applied
|
||||||
|
if strings.Contains(content, "## Media & File Sending") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
section := `
|
||||||
|
|
||||||
|
## Media & File Sending
|
||||||
|
|
||||||
|
You CAN send files directly to users. When you need to share a file (image, document, audio, video), use the ` + "`message`" + ` tool with the ` + "`media`" + ` parameter containing the local file path(s). The file will be delivered natively through the user's channel (Telegram, Discord, Slack, etc.). Do NOT tell users you cannot send files — just send them.`
|
||||||
|
|
||||||
|
content += section
|
||||||
|
|
||||||
|
if err := os.WriteFile(agentPath, []byte(content), 0644); err != nil {
|
||||||
|
logger.ErrorCF("migrate", "Failed to upgrade AGENT.md", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.InfoC("migrate", "Upgraded AGENT.md with media sending instructions")
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgradeAgentGrounding ensures AGENT.md tells the LLM it is already connected
|
||||||
|
// and should not suggest manual workarounds like tokens or curl commands.
|
||||||
|
func upgradeAgentGrounding(workspace string) {
|
||||||
|
agentPath := filepath.Join(workspace, "AGENT.md")
|
||||||
|
|
||||||
|
data, err := os.ReadFile(agentPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
// Already applied
|
||||||
|
if strings.Contains(content, "NEVER suggest manual workarounds") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the old generic intro with the grounded version
|
||||||
|
oldIntro := "You are a helpful AI assistant. Be concise, accurate, and friendly."
|
||||||
|
newIntro := "You are a helpful AI assistant running inside picoclaw. You are ALREADY connected to the user's chat channel (Telegram, Discord, Slack, etc.). When you use the `message` tool, your message is delivered directly to the user — you do NOT need API keys, bot tokens, or any external setup. Everything is already wired up for you. Just use your tools."
|
||||||
|
|
||||||
|
if strings.Contains(content, oldIntro) {
|
||||||
|
content = strings.Replace(content, oldIntro, newIntro, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add guardrail lines to guidelines if not present
|
||||||
|
if !strings.Contains(content, "NEVER tell users you lack access") {
|
||||||
|
oldGuideline := "- Learn from user feedback"
|
||||||
|
newGuideline := `- Learn from user feedback
|
||||||
|
- NEVER tell users you lack access to send messages, files, or perform actions — use your tools instead
|
||||||
|
- NEVER suggest manual workarounds (curl commands, scripts, tokens) for things your tools already do`
|
||||||
|
content = strings.Replace(content, oldGuideline, newGuideline, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(agentPath, []byte(content), 0644); err != nil {
|
||||||
|
logger.ErrorCF("migrate", "Failed to upgrade AGENT.md grounding", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.InfoC("migrate", "Upgraded AGENT.md with grounding instructions")
|
||||||
|
}
|
||||||
70
pkg/migrate/upgrade_test.go
Normal file
70
pkg/migrate/upgrade_test.go
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
package migrate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpgradeAgentMediaSection(t *testing.T) {
|
||||||
|
t.Run("appends media section to existing AGENT.md", func(t *testing.T) {
|
||||||
|
workspace := t.TempDir()
|
||||||
|
agentPath := filepath.Join(workspace, "AGENT.md")
|
||||||
|
os.WriteFile(agentPath, []byte("# Agent Instructions\n\nBe helpful."), 0644)
|
||||||
|
|
||||||
|
UpgradeWorkspace(workspace)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(agentPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reading AGENT.md: %v", err)
|
||||||
|
}
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
if !strings.Contains(content, "## Media & File Sending") {
|
||||||
|
t.Error("expected media section to be appended")
|
||||||
|
}
|
||||||
|
if !strings.Contains(content, "You CAN send files directly") {
|
||||||
|
t.Error("expected media instructions in content")
|
||||||
|
}
|
||||||
|
// Original content preserved
|
||||||
|
if !strings.Contains(content, "# Agent Instructions") {
|
||||||
|
t.Error("original content should be preserved")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("idempotent — does not duplicate section", func(t *testing.T) {
|
||||||
|
workspace := t.TempDir()
|
||||||
|
agentPath := filepath.Join(workspace, "AGENT.md")
|
||||||
|
os.WriteFile(agentPath, []byte("# Agent Instructions\n\nBe helpful."), 0644)
|
||||||
|
|
||||||
|
UpgradeWorkspace(workspace)
|
||||||
|
UpgradeWorkspace(workspace)
|
||||||
|
|
||||||
|
data, _ := os.ReadFile(agentPath)
|
||||||
|
count := strings.Count(string(data), "## Media & File Sending")
|
||||||
|
if count != 1 {
|
||||||
|
t.Errorf("expected exactly 1 media section, got %d", count)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("skips when AGENT.md does not exist", func(t *testing.T) {
|
||||||
|
workspace := t.TempDir()
|
||||||
|
// No AGENT.md created — should not panic or error
|
||||||
|
UpgradeWorkspace(workspace)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("skips when section already present", func(t *testing.T) {
|
||||||
|
workspace := t.TempDir()
|
||||||
|
agentPath := filepath.Join(workspace, "AGENT.md")
|
||||||
|
original := "# Agent\n\n## Media & File Sending\n\nAlready here."
|
||||||
|
os.WriteFile(agentPath, []byte(original), 0644)
|
||||||
|
|
||||||
|
UpgradeWorkspace(workspace)
|
||||||
|
|
||||||
|
data, _ := os.ReadFile(agentPath)
|
||||||
|
if string(data) != original {
|
||||||
|
t.Error("file should not be modified when section already exists")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SendCallback func(channel, chatID, content string) error
|
type SendCallback func(channel, chatID, content string, media []string) error
|
||||||
|
|
||||||
type MessageTool struct {
|
type MessageTool struct {
|
||||||
sendCallback SendCallback
|
sendCallback SendCallback
|
||||||
|
|
@ -23,7 +23,7 @@ func (t *MessageTool) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *MessageTool) Description() string {
|
func (t *MessageTool) Description() string {
|
||||||
return "Send a message to user on a chat channel. Use this when you want to communicate something."
|
return "Send a message to user on a chat channel. Use this when you want to communicate something. You can also attach local files (images, documents, audio, video) using the media parameter — the files will be delivered natively through the channel."
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *MessageTool) Parameters() map[string]interface{} {
|
func (t *MessageTool) Parameters() map[string]interface{} {
|
||||||
|
|
@ -42,6 +42,13 @@ func (t *MessageTool) Parameters() map[string]interface{} {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional: target chat/user ID",
|
"description": "Optional: target chat/user ID",
|
||||||
},
|
},
|
||||||
|
"media": map[string]interface{}{
|
||||||
|
"type": "array",
|
||||||
|
"description": "Optional: list of local file paths to send as media attachments",
|
||||||
|
"items": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"content"},
|
"required": []string{"content"},
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +78,15 @@ 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)
|
||||||
|
|
||||||
|
var media []string
|
||||||
|
if rawMedia, ok := args["media"].([]interface{}); ok {
|
||||||
|
for _, item := range rawMedia {
|
||||||
|
if path, ok := item.(string); ok {
|
||||||
|
media = append(media, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if channel == "" {
|
if channel == "" {
|
||||||
channel = t.defaultChannel
|
channel = t.defaultChannel
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +102,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, media); 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,7 +11,7 @@ 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 {
|
tool.SetSendCallback(func(channel, chatID, content string, media []string) error {
|
||||||
sentChannel = channel
|
sentChannel = channel
|
||||||
sentChatID = chatID
|
sentChatID = chatID
|
||||||
sentContent = content
|
sentContent = content
|
||||||
|
|
@ -63,7 +63,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, media []string) error {
|
||||||
sentChannel = channel
|
sentChannel = channel
|
||||||
sentChatID = chatID
|
sentChatID = chatID
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -99,7 +99,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, media []string) error {
|
||||||
return sendErr
|
return sendErr
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -153,7 +153,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, media []string) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ type ToolResult struct {
|
||||||
// When true, the tool will complete later and notify via callback.
|
// When true, the tool will complete later and notify via callback.
|
||||||
Async bool `json:"async"`
|
Async bool `json:"async"`
|
||||||
|
|
||||||
|
// Media contains local file paths to send alongside the message.
|
||||||
|
Media []string `json:"media,omitempty"`
|
||||||
|
|
||||||
// Err is the underlying error (not JSON serialized).
|
// Err is the underlying error (not JSON serialized).
|
||||||
// Used for internal error handling and logging.
|
// Used for internal error handling and logging.
|
||||||
Err error `json:"-"`
|
Err error `json:"-"`
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
# Agent Instructions
|
# Agent Instructions
|
||||||
|
|
||||||
You are a helpful AI assistant. Be concise, accurate, and friendly.
|
You are a helpful AI assistant running inside picoclaw. You are ALREADY connected to the user's chat channel (Telegram, Discord, Slack, etc.). When you use the `message` tool, your message is delivered directly to the user — you do NOT need API keys, bot tokens, or any external setup. Everything is already wired up for you. Just use your tools.
|
||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
|
|
||||||
- Always explain what you're doing before taking actions
|
- Always explain what you're doing before taking actions
|
||||||
- Ask for clarification when request is ambiguous
|
- Ask for clarification when request is ambiguous
|
||||||
- Use tools to help accomplish tasks
|
- Use tools to help accomplish tasks — they are your primary way of interacting with the world
|
||||||
- Remember important information in your memory files
|
- Remember important information in your memory files
|
||||||
- Be proactive and helpful
|
- Be proactive and helpful
|
||||||
- Learn from user feedback
|
- Learn from user feedback
|
||||||
|
- NEVER tell users you lack access to send messages, files, or perform actions — use your tools instead
|
||||||
|
- NEVER suggest manual workarounds (curl commands, scripts, tokens) for things your tools already do
|
||||||
|
|
||||||
|
## Media & File Sending
|
||||||
|
|
||||||
|
You CAN send files directly to users. When you need to share a file (image, document, audio, video), use the `message` tool with the `media` parameter containing the local file path(s). The file will be delivered natively through the user's channel (Telegram, Discord, Slack, etc.). Do NOT tell users you cannot send files — just send them.
|
||||||
Loading…
Add table
Reference in a new issue