mirror of
https://github.com/HugeFrog24/go-telegram-bot.git
synced 2026-06-29 22:07:12 +00:00
Support for images
This commit is contained in:
@@ -27,6 +27,11 @@ type Bot struct {
|
||||
userLimitersMu sync.RWMutex
|
||||
clock Clock
|
||||
botID uint // Reference to BotModel.ID
|
||||
// albumBuffers holds Telegram media_group items as they arrive, keyed by
|
||||
// MediaGroupID. Each pending album has a 1s flush timer (see album_buffer.go)
|
||||
// that triggers a single coalesced photo turn once arrivals stop.
|
||||
albumBuffers map[string]*pendingAlbum
|
||||
albumBuffersMu sync.Mutex
|
||||
}
|
||||
|
||||
// Helper function to determine message type
|
||||
@@ -94,6 +99,7 @@ func NewBot(db *gorm.DB, config BotConfig, clock Clock, tgClient TelegramClient)
|
||||
clock: clock,
|
||||
botID: botEntry.ID, // Ensure BotModel has ID field
|
||||
tgBot: tgClient,
|
||||
albumBuffers: make(map[string]*pendingAlbum),
|
||||
}
|
||||
|
||||
if tgClient == nil {
|
||||
@@ -251,6 +257,32 @@ func (b *Bot) getOrCreateChatMemory(chatID int64) *ChatMemory {
|
||||
return chatMemory
|
||||
}
|
||||
|
||||
// stripDeadFileIDFromMemory removes a single file_id from every message in the
|
||||
// chat's in-memory ChatMemory. Called by the runtime self-heal in
|
||||
// getAnthropicResponse after Anthropic 404s for that file_id, so the immediate
|
||||
// retry (and any subsequent turn replay) won't reference it. The corresponding
|
||||
// DB rows are stamped separately via markFilesPendingCleanup.
|
||||
func (b *Bot) stripDeadFileIDFromMemory(chatID int64, deadFileID string) {
|
||||
b.chatMemoriesMu.Lock()
|
||||
defer b.chatMemoriesMu.Unlock()
|
||||
cm, exists := b.chatMemories[chatID]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
for i := range cm.Messages {
|
||||
if len(cm.Messages[i].ImageFileIDs) == 0 {
|
||||
continue
|
||||
}
|
||||
survivors := make([]string, 0, len(cm.Messages[i].ImageFileIDs))
|
||||
for _, fid := range cm.Messages[i].ImageFileIDs {
|
||||
if fid != deadFileID {
|
||||
survivors = append(survivors, fid)
|
||||
}
|
||||
}
|
||||
cm.Messages[i].ImageFileIDs = survivors
|
||||
}
|
||||
}
|
||||
|
||||
// addMessageToChatMemory adds a new message to the chat memory, ensuring the memory size is maintained.
|
||||
func (b *Bot) addMessageToChatMemory(chatMemory *ChatMemory, message Message) {
|
||||
b.chatMemoriesMu.Lock()
|
||||
@@ -272,7 +304,7 @@ func (b *Bot) prepareContextMessages(chatMemory *ChatMemory) []anthropic.BetaMes
|
||||
// Debug logging
|
||||
InfoLogger.Printf("Chat memory contains %d messages", len(chatMemory.Messages))
|
||||
for i, msg := range chatMemory.Messages {
|
||||
InfoLogger.Printf("Message %d: IsUser=%v, Text=%q", i, msg.IsUser, msg.Text)
|
||||
InfoLogger.Printf("Message %d: IsUser=%v, Text=%q Images=%d", i, msg.IsUser, msg.Text, len(msg.ImageFileIDs))
|
||||
}
|
||||
|
||||
// Note: consecutive messages with the same role are permitted.
|
||||
@@ -282,20 +314,18 @@ func (b *Bot) prepareContextMessages(chatMemory *ChatMemory) []anthropic.BetaMes
|
||||
// See: https://platform.claude.com/docs/en/api/messages
|
||||
var contextMessages []anthropic.BetaMessageParam
|
||||
for _, msg := range chatMemory.Messages {
|
||||
textContent := strings.TrimSpace(msg.Text)
|
||||
if textContent == "" {
|
||||
// Skip empty messages
|
||||
blocks := contentBlocksForMessage(msg)
|
||||
if len(blocks) == 0 {
|
||||
// Skip turns that carry neither text nor images.
|
||||
continue
|
||||
}
|
||||
|
||||
block := anthropic.NewBetaTextBlock(textContent)
|
||||
var param anthropic.BetaMessageParam
|
||||
if msg.IsUser {
|
||||
param = anthropic.NewBetaUserMessage(block)
|
||||
param = anthropic.NewBetaUserMessage(blocks...)
|
||||
} else {
|
||||
param = anthropic.BetaMessageParam{
|
||||
Role: anthropic.BetaMessageParamRoleAssistant,
|
||||
Content: []anthropic.BetaContentBlockParamUnion{block},
|
||||
Content: blocks,
|
||||
}
|
||||
}
|
||||
contextMessages = append(contextMessages, param)
|
||||
@@ -303,6 +333,28 @@ func (b *Bot) prepareContextMessages(chatMemory *ChatMemory) []anthropic.BetaMes
|
||||
return contextMessages
|
||||
}
|
||||
|
||||
// contentBlocksForMessage assembles the Anthropic content blocks representing
|
||||
// one stored Message. Image blocks are emitted before the text block (Anthropic
|
||||
// docs: "Claude works best when images come before text"). Multi-image user
|
||||
// turns prepend each image with an "Image N:" label, as the docs explicitly
|
||||
// recommend for multi-image prompts. Assistant turns carry text only.
|
||||
func contentBlocksForMessage(msg Message) []anthropic.BetaContentBlockParamUnion {
|
||||
var blocks []anthropic.BetaContentBlockParamUnion
|
||||
if msg.IsUser && len(msg.ImageFileIDs) > 0 {
|
||||
multi := len(msg.ImageFileIDs) > 1
|
||||
for i, fileID := range msg.ImageFileIDs {
|
||||
if multi {
|
||||
blocks = append(blocks, anthropic.NewBetaTextBlock(fmt.Sprintf("Image %d:", i+1)))
|
||||
}
|
||||
blocks = append(blocks, anthropic.NewBetaImageBlock(anthropic.BetaFileImageSourceParam{FileID: fileID}))
|
||||
}
|
||||
}
|
||||
if textContent := strings.TrimSpace(msg.Text); textContent != "" {
|
||||
blocks = append(blocks, anthropic.NewBetaTextBlock(textContent))
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
func (b *Bot) isNewChat(chatID int64) bool {
|
||||
var count int64
|
||||
b.db.Model(&Message{}).Where("chat_id = ? AND bot_id = ?", chatID, b.botID).Count(&count)
|
||||
@@ -449,6 +501,27 @@ func (b *Bot) sendResponse(ctx context.Context, chatID int64, text string, busin
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendOneSegment delivers a single Telegram message without touching storage
|
||||
// or chat memory. Used by the streaming response path: each completed text
|
||||
// block fires this helper as it arrives, and the full turn is recorded once
|
||||
// at end-of-stream via screenOutgoingMessage. Keeps the 1-reply-per-prompt
|
||||
// storage invariant while letting the user see segments with natural rhythm.
|
||||
func (b *Bot) sendOneSegment(ctx context.Context, chatID int64, text, businessConnectionID string) error {
|
||||
params := &bot.SendMessageParams{
|
||||
ChatID: chatID,
|
||||
Text: text,
|
||||
}
|
||||
if businessConnectionID != "" {
|
||||
params.BusinessConnectionID = businessConnectionID
|
||||
}
|
||||
if _, err := b.tgBot.SendMessage(ctx, params); err != nil {
|
||||
ErrorLogger.Printf("[%s] Error sending segment to chat %d with BusinessConnectionID %s: %v",
|
||||
b.config.ID, chatID, businessConnectionID, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendStats sends the bot statistics to the specified chat.
|
||||
func (b *Bot) sendStats(ctx context.Context, chatID int64, userID int64, targetUserID int64, businessConnectionID string) {
|
||||
// If targetUserID is 0, show global stats
|
||||
|
||||
Reference in New Issue
Block a user