diff --git a/cmd/gateway_consumer_normal.go b/cmd/gateway_consumer_normal.go index 6b93c55e..4348bb5d 100644 --- a/cmd/gateway_consumer_normal.go +++ b/cmd/gateway_consumer_normal.go @@ -255,6 +255,16 @@ func processNormalMessage( extraPrompt += tsp } + // Append channel-provided self-identity hint (e.g. "You are @bot (Name) on Telegram"). + // Prevents the LLM from treating its own platform handle as another bot when users + // @mention it directly or reference it alongside another bot in multi-bot groups. + if identity := msg.Metadata[tools.MetaChannelSelfIdentity]; identity != "" { + if extraPrompt != "" { + extraPrompt += "\n\n" + } + extraPrompt += identity + } + // Per-topic skill filter override (from group/topic config hierarchy). var skillFilter []string if ts := msg.Metadata[tools.MetaTopicSkills]; ts != "" { diff --git a/internal/channels/telegram/channel.go b/internal/channels/telegram/channel.go index 7788589c..b667ed0b 100644 --- a/internal/channels/telegram/channel.go +++ b/internal/channels/telegram/channel.go @@ -39,6 +39,7 @@ type Channel struct { reactions sync.Map // localKey string → *StatusReactionController threadIDs sync.Map // localKey string → messageThreadID int (for forum topic routing) mentionMode string // "strict" (default) or "yield" + botDisplayName string // bot's first_name from GetMe (e.g. "ViệtBot"); captured once at Start pollCancel context.CancelFunc // cancels the long polling context pollDone chan struct{} // closed when polling goroutine exits handlerWg sync.WaitGroup // tracks in-flight handler goroutines for graceful shutdown @@ -189,6 +190,7 @@ func (c *Channel) Start(ctx context.Context) error { username := "" if me != nil { username = me.Username + c.botDisplayName = me.FirstName } // Create a cancellable context for the polling goroutine. diff --git a/internal/channels/telegram/handlers.go b/internal/channels/telegram/handlers.go index 9d268cfe..7999103c 100644 --- a/internal/channels/telegram/handlers.go +++ b/internal/channels/telegram/handlers.go @@ -595,6 +595,12 @@ func (c *Channel) handleMessage(ctx context.Context, update telego.Update) { metadata[tools.MetaDMThreadID] = fmt.Sprintf("%d", dmThreadID) metadata[tools.MetaMessageThreadID] = fmt.Sprintf("%d", dmThreadID) } + // Self-identity hint so the LLM knows its own Telegram handle and does not + // confuse other bots' @mentions (preserved after stripBotMention) for its own. + if identity := buildSelfIdentityPrompt(c.bot.Username(), c.botDisplayName); identity != "" { + metadata[tools.MetaChannelSelfIdentity] = identity + } + if topicCfg.systemPrompt != "" { metadata[tools.MetaTopicSystemPrompt] = topicCfg.systemPrompt } diff --git a/internal/channels/telegram/handlers_utils.go b/internal/channels/telegram/handlers_utils.go index 020cdec3..bffce9f5 100644 --- a/internal/channels/telegram/handlers_utils.go +++ b/internal/channels/telegram/handlers_utils.go @@ -1,12 +1,28 @@ package telegram import ( + "fmt" "regexp" "strings" "github.com/mymmrac/telego" ) +// buildSelfIdentityPrompt returns a short system-prompt snippet telling the LLM +// which Telegram handle represents itself, so it does not confuse its own +// @mention for a different bot — especially useful in multi-bot groups where +// other bots' mentions remain in the content after stripBotMention. +// Returns empty string when the bot username has not been resolved yet. +func buildSelfIdentityPrompt(botUsername, displayName string) string { + if botUsername == "" { + return "" + } + if displayName != "" { + return fmt.Sprintf("You are @%s (%s) on this Telegram channel.", botUsername, displayName) + } + return fmt.Sprintf("You are @%s on this Telegram channel.", botUsername) +} + // stripBotMention removes @botUsername tokens from text (case-insensitive). // Applied after the mention gate passes so the LLM does not see its own Telegram handle // and mistake itself for another bot (e.g. persona "Tiểu Hổ" receiving "@viet_super_bot vẽ..."). diff --git a/internal/channels/telegram/handlers_utils_test.go b/internal/channels/telegram/handlers_utils_test.go index c0c71139..a5bbaa1e 100644 --- a/internal/channels/telegram/handlers_utils_test.go +++ b/internal/channels/telegram/handlers_utils_test.go @@ -272,6 +272,30 @@ func TestStripBotMention_OnlyMentionBecomesEmpty(t *testing.T) { } } +// --- buildSelfIdentityPrompt --- + +func TestBuildSelfIdentityPrompt_WithDisplayName(t *testing.T) { + got := buildSelfIdentityPrompt("viet_super_bot", "ViệtBot") + want := "You are @viet_super_bot (ViệtBot) on this Telegram channel." + if got != want { + t.Errorf("buildSelfIdentityPrompt = %q, want %q", got, want) + } +} + +func TestBuildSelfIdentityPrompt_NoDisplayName(t *testing.T) { + got := buildSelfIdentityPrompt("viet_super_bot", "") + want := "You are @viet_super_bot on this Telegram channel." + if got != want { + t.Errorf("buildSelfIdentityPrompt = %q, want %q", got, want) + } +} + +func TestBuildSelfIdentityPrompt_EmptyUsername(t *testing.T) { + if got := buildSelfIdentityPrompt("", "Name"); got != "" { + t.Errorf("buildSelfIdentityPrompt with empty username should return empty; got %q", got) + } +} + // --- isServiceMessage --- func TestIsServiceMessage_WithText(t *testing.T) { diff --git a/internal/tools/team_metadata_keys.go b/internal/tools/team_metadata_keys.go index 5e52b0e9..b2177baf 100644 --- a/internal/tools/team_metadata_keys.go +++ b/internal/tools/team_metadata_keys.go @@ -44,6 +44,11 @@ const ( MetaUserName = "user_name" MetaTopicSystemPrompt = "topic_system_prompt" MetaTopicSkills = "topic_skills" + // MetaChannelSelfIdentity carries a channel-provided self-identity hint + // (e.g. "You are @viet_super_bot (ViệtBot) on this Telegram channel.") + // appended to the agent's system prompt so the LLM does not confuse its own + // platform handle for a different bot when users @mention it. + MetaChannelSelfIdentity = "channel_self_identity" ) // Task metadata keys stored in store.TeamTaskData.Metadata.