mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 00:16:38 +00:00
fix(agent): channel-aware tool filtering, group chat title in system prompt
- Add filteredToolNamesForChannel() so system prompt Tooling section excludes channel-specific tools for wrong channels (e.g. list_group_members hidden for Telegram, create_forum_topic hidden for Feishu) - Add ChannelAware interface to CreateForumTopicTool (Telegram-only) - Add coreToolSummaries for list_group_members and create_forum_topic (were showing as "(custom tool)") - Inject group chat title into system prompt identity line (e.g. "telegram (group chat \"My Group\")" instead of "a group chat") - Sanitize ChatTitle to prevent prompt injection (strip quotes/newlines, truncate to 100 chars)
This commit is contained in:
1 parent
8b08dec04c
commit
7f796b0120
6 files changed
+46
-3
No files matched your search
@@ -336,6 +336,7 @@ func processNormalMessage(
|
||||
ForwardMedia: fwdMedia,
|
||||
Channel: msg.Channel,
|
||||
ChannelType: resolveChannelType(channelMgr, msg.Channel),
|
||||
ChatTitle: msg.Metadata["chat_title"],
|
||||
ChatID: msg.ChatID,
|
||||
PeerKind: peerKind,
|
||||
LocalKey: msg.Metadata["local_key"],
|
||||
|
||||
@@ -65,7 +65,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error)
|
||||
|
||||
// buildMessages resolves context files once and also detects BOOTSTRAP.md presence
|
||||
// (hadBootstrap) — no extra DB roundtrip needed for bootstrap detection.
|
||||
messages, hadBootstrap := l.buildMessages(ctx, history, summary, req.Message, req.ExtraSystemPrompt, req.SessionKey, req.Channel, req.ChannelType, req.PeerKind, req.UserID, req.HistoryLimit, req.SkillFilter, req.LightContext)
|
||||
messages, hadBootstrap := l.buildMessages(ctx, history, summary, req.Message, req.ExtraSystemPrompt, req.SessionKey, req.Channel, req.ChannelType, req.ChatTitle, req.PeerKind, req.UserID, req.HistoryLimit, req.SkillFilter, req.LightContext)
|
||||
|
||||
// 1b. Determine image routing strategy.
|
||||
// If read_image tool has a dedicated vision provider, images are NOT attached inline
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -41,6 +42,29 @@ func (l *Loop) filteredToolNames() []string {
|
||||
return names
|
||||
}
|
||||
|
||||
// filteredToolNamesForChannel returns tool names after applying both policy
|
||||
// and ChannelAware filters. Tools that implement ChannelAware and don't list
|
||||
// the current channelType are excluded — keeps the system prompt Tooling
|
||||
// section consistent with the actual tool definitions sent to the LLM.
|
||||
func (l *Loop) filteredToolNamesForChannel(channelType string) []string {
|
||||
names := l.filteredToolNames()
|
||||
if channelType == "" {
|
||||
return names
|
||||
}
|
||||
filtered := names[:0:0]
|
||||
for _, name := range names {
|
||||
if tool, ok := l.tools.Get(name); ok {
|
||||
if ca, ok := tool.(tools.ChannelAware); ok {
|
||||
if !slices.Contains(ca.RequiredChannelTypes(), channelType) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
filtered = append(filtered, name)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// buildCredentialCLIContext generates the TOOLS.md supplement for credentialed CLIs.
|
||||
// Returns empty string if no secure CLI store is configured or no enabled CLIs.
|
||||
func (l *Loop) buildCredentialCLIContext(ctx context.Context) string {
|
||||
@@ -75,7 +99,7 @@ func (l *Loop) buildMCPToolDescs(toolNames []string) map[string]string {
|
||||
// buildMessages constructs the full message list for an LLM request.
|
||||
// Returns the messages and whether BOOTSTRAP.md was present in context files
|
||||
// (used by the caller for auto-cleanup without an extra DB roundtrip).
|
||||
func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, summary, userMessage, extraSystemPrompt, sessionKey, channel, channelType, peerKind, userID string, historyLimit int, skillFilter []string, lightContext bool) ([]providers.Message, bool) {
|
||||
func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, summary, userMessage, extraSystemPrompt, sessionKey, channel, channelType, chatTitle, peerKind, userID string, historyLimit int, skillFilter []string, lightContext bool) ([]providers.Message, bool) {
|
||||
var messages []providers.Message
|
||||
|
||||
// Build full system prompt using the new builder (matching TS buildAgentSystemPrompt)
|
||||
@@ -155,7 +179,9 @@ func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, s
|
||||
}
|
||||
|
||||
// Build tool list, filtering out skill_manage when skill_evolve is off.
|
||||
toolNames := l.filteredToolNames()
|
||||
// Also applies ChannelAware filtering so channel-specific tools don't
|
||||
// appear in ## Tooling when the current channel doesn't support them.
|
||||
toolNames := l.filteredToolNamesForChannel(channelType)
|
||||
if !l.skillEvolve {
|
||||
filtered := toolNames[:0:0]
|
||||
for _, n := range toolNames {
|
||||
@@ -191,6 +217,7 @@ func (l *Loop) buildMessages(ctx context.Context, history []providers.Message, s
|
||||
Workspace: promptWorkspace,
|
||||
Channel: channel,
|
||||
ChannelType: channelType,
|
||||
ChatTitle: chatTitle,
|
||||
PeerKind: peerKind,
|
||||
OwnerIDs: l.ownerIDs,
|
||||
Mode: mode,
|
||||
|
||||
@@ -400,6 +400,7 @@ type RunRequest struct {
|
||||
ForwardMedia []bus.MediaFile // media files to forward to output (from delegation results)
|
||||
Channel string // source channel instance name (e.g. "my-telegram-bot")
|
||||
ChannelType string // platform type (e.g. "zalo_personal", "telegram") — for system prompt context
|
||||
ChatTitle string // group chat display name (e.g. Telegram group title)
|
||||
ChatID string // source chat ID
|
||||
PeerKind string // "direct" or "group" (for session key building and tool context)
|
||||
RunID string // unique run identifier
|
||||
|
||||
@@ -27,6 +27,7 @@ type SystemPromptConfig struct {
|
||||
Workspace string
|
||||
Channel string // runtime channel instance name (e.g. "my-telegram-bot")
|
||||
ChannelType string // platform type (e.g. "zalo_personal", "telegram")
|
||||
ChatTitle string // group chat display name (shown in identity line)
|
||||
PeerKind string // "direct" or "group"
|
||||
OwnerIDs []string // owner sender IDs
|
||||
Mode PromptMode // full or minimal
|
||||
@@ -106,6 +107,8 @@ var coreToolSummaries = map[string]string{
|
||||
"create_audio": "Generate music or sound effects from text descriptions using AI",
|
||||
"knowledge_graph_search": "Find people, projects, and their connections — use for relationship questions (who works with whom, project dependencies) that memory_search may miss",
|
||||
"team_tasks": "Team task board — track progress, manage dependencies (spawn auto-creates delegation tasks)",
|
||||
"list_group_members": "List all members of the current group chat (Feishu/Lark only)",
|
||||
"create_forum_topic": "Create a forum topic in a Telegram supergroup",
|
||||
|
||||
// Legacy tool aliases — kept for backward compatibility with older clients
|
||||
"edit_file": "Alias for edit — Edit a file by replacing exact text matches",
|
||||
@@ -138,6 +141,15 @@ func BuildSystemPrompt(cfg SystemPromptConfig) string {
|
||||
chatType := "a direct chat"
|
||||
if cfg.PeerKind == "group" {
|
||||
chatType = "a group chat"
|
||||
if cfg.ChatTitle != "" {
|
||||
// Sanitize: strip quotes/newlines, truncate to prevent prompt injection
|
||||
// (group admins control the title).
|
||||
title := strings.NewReplacer("\"", "", "\n", " ", "\r", "").Replace(cfg.ChatTitle)
|
||||
if len([]rune(title)) > 100 {
|
||||
title = string([]rune(title)[:100])
|
||||
}
|
||||
chatType = fmt.Sprintf("group chat \"%s\"", title)
|
||||
}
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("You are a personal assistant running in %s (%s).", channelLabel, chatType))
|
||||
lines = append(lines, "")
|
||||
|
||||
@@ -28,6 +28,8 @@ func NewCreateForumTopicTool(provider ForumTopicCreatorProvider) *CreateForumTop
|
||||
|
||||
func (t *CreateForumTopicTool) Name() string { return "create_forum_topic" }
|
||||
|
||||
func (t *CreateForumTopicTool) RequiredChannelTypes() []string { return []string{"telegram"} }
|
||||
|
||||
func (t *CreateForumTopicTool) Description() string {
|
||||
return "Create a new forum topic in a Telegram supergroup. " +
|
||||
"Returns the topic's message_thread_id which can be used for routing messages to the topic."
|
||||
|
||||
Reference in new issue
Block a user