mirror of
https://github.com/HugeFrog24/go-telegram-bot.git
synced 2026-06-29 22:07:12 +00:00
A
This commit is contained in:
+15
-9
@@ -17,7 +17,7 @@ import (
|
||||
// actionable message to admins/owners while keeping the response vague for regular users.
|
||||
var ErrModelNotFound = errors.New("model not found or deprecated")
|
||||
|
||||
func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.BetaMessageParam, isNewChat, isOwner, isEmojiOnly bool, username string, firstName string, lastName string, isPremium bool, languageCode string, messageTime int) (string, error) {
|
||||
func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.BetaMessageParam, isNewChat, isOwner, isEmojiOnly bool, username string, firstName string, lastName string, isPremium bool, languageCode string, messageTime int) ([]string, error) {
|
||||
// Use prompts from config
|
||||
var systemMessage string
|
||||
if isNewChat {
|
||||
@@ -143,16 +143,23 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.Bet
|
||||
if err != nil {
|
||||
var apiErr *anthropic.Error
|
||||
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound {
|
||||
return "", fmt.Errorf("%w: %s", ErrModelNotFound, b.config.Model)
|
||||
return nil, fmt.Errorf("%w: %s", ErrModelNotFound, b.config.Model)
|
||||
}
|
||||
return "", fmt.Errorf("error creating Anthropic message: %w", err)
|
||||
return nil, fmt.Errorf("error creating Anthropic message: %w", err)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
// Collect text blocks as separate segments so the Telegram delivery layer
|
||||
// can render each as its own message (matches the conversational rhythm
|
||||
// Claude uses around tool calls). Callers that want one joined string
|
||||
// (voice TTS, stickers) do strings.Join themselves.
|
||||
var segments []string
|
||||
for _, block := range resp.Content {
|
||||
switch block.Type {
|
||||
case "text":
|
||||
sb.WriteString(block.Text)
|
||||
t := strings.TrimSpace(block.Text)
|
||||
if t != "" {
|
||||
segments = append(segments, t)
|
||||
}
|
||||
case "mcp_tool_use":
|
||||
InfoLogger.Printf("[mcp] tool_use server=%q name=%q id=%q input=%s",
|
||||
block.ServerName, block.Name, block.ID, string(block.Input))
|
||||
@@ -167,9 +174,8 @@ func (b *Bot) getAnthropicResponse(ctx context.Context, messages []anthropic.Bet
|
||||
InfoLogger.Printf("[mcp] block type=%q (unhandled)", block.Type)
|
||||
}
|
||||
}
|
||||
out := sb.String()
|
||||
if out == "" {
|
||||
return "", fmt.Errorf("unexpected response format from Anthropic")
|
||||
if len(segments) == 0 {
|
||||
return nil, fmt.Errorf("unexpected response format from Anthropic")
|
||||
}
|
||||
return out, nil
|
||||
return segments, nil
|
||||
}
|
||||
|
||||
@@ -449,6 +449,44 @@ func (b *Bot) sendResponse(ctx context.Context, chatID int64, text string, busin
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendMultiResponse delivers a multi-block LLM response as separate Telegram
|
||||
// messages while keeping a single logical assistant turn in storage. The DB
|
||||
// row and chat memory hold the joined text (segments separated by blank lines),
|
||||
// so the model's next-turn context sees one assistant turn — matching today's
|
||||
// 1-reply-per-prompt invariant — even though the user saw N bubbles.
|
||||
//
|
||||
// Partial send failures (a later segment fails after earlier ones succeeded)
|
||||
// are logged but do not abort the remaining sends. The DB record is canonical:
|
||||
// the model's next turn will reference what it intended to say.
|
||||
func (b *Bot) sendMultiResponse(ctx context.Context, chatID int64, segments []string, businessConnectionID string) error {
|
||||
if len(segments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fullText := strings.Join(segments, "\n\n")
|
||||
if _, err := b.screenOutgoingMessage(chatID, fullText); err != nil {
|
||||
ErrorLogger.Printf("Error storing assistant message: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for i, seg := range segments {
|
||||
params := &bot.SendMessageParams{
|
||||
ChatID: chatID,
|
||||
Text: seg,
|
||||
}
|
||||
if businessConnectionID != "" {
|
||||
params.BusinessConnectionID = businessConnectionID
|
||||
}
|
||||
if _, err := b.tgBot.SendMessage(ctx, params); err != nil {
|
||||
ErrorLogger.Printf("[%s] Error sending segment %d/%d to chat %d with BusinessConnectionID %s: %v",
|
||||
b.config.ID, i+1, len(segments), chatID, businessConnectionID, err)
|
||||
// Keep going: earlier segments are already in the user's chat,
|
||||
// and the DB has the full turn recorded.
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
Binary file not shown.
+42
-10
@@ -55,7 +55,7 @@ func (b *Bot) handleVoiceMessage(ctx context.Context, message *models.Message, u
|
||||
|
||||
chatMemory := b.getOrCreateChatMemory(chatID)
|
||||
contextMessages := b.prepareContextMessages(chatMemory)
|
||||
response, err := b.getAnthropicResponse(ctx, contextMessages, isNewChat, isOwner, false, username, firstName, lastName, isPremium, languageCode, messageTime)
|
||||
segments, err := b.getAnthropicResponse(ctx, contextMessages, isNewChat, isOwner, false, username, firstName, lastName, isPremium, languageCode, messageTime)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error getting Anthropic response for voice: %v", err)
|
||||
if err := b.sendResponse(ctx, chatID, b.anthropicErrorResponse(err, userID), businessConnectionID); err != nil {
|
||||
@@ -64,6 +64,10 @@ func (b *Bot) handleVoiceMessage(ctx context.Context, message *models.Message, u
|
||||
return
|
||||
}
|
||||
|
||||
// Voice replies always synthesize as one audio clip — tool-call narration
|
||||
// across multiple TTS clips would be jarring, so we join here.
|
||||
response := strings.Join(segments, "\n\n")
|
||||
|
||||
audioReader, err := b.generateSpeech(ctx, response)
|
||||
if err != nil {
|
||||
// TTS failed — fall back to text so the user still gets a reply.
|
||||
@@ -92,16 +96,37 @@ func (b *Bot) handleVoiceMessage(ctx context.Context, message *models.Message, u
|
||||
}
|
||||
|
||||
// anthropicErrorResponse returns the message to send back to the user when getAnthropicResponse
|
||||
// fails. Admins and owners receive an actionable hint when the model is deprecated; regular users
|
||||
// always get the generic fallback to avoid leaking internal details.
|
||||
// fails. Admins and owners (anyone with model:set scope) receive the underlying API error so they
|
||||
// can act on it — actionable hint for model-deprecation, raw status+body+request-id for everything
|
||||
// else. Regular users always get the generic fallback to avoid leaking internal details.
|
||||
func (b *Bot) anthropicErrorResponse(err error, userID int64) string {
|
||||
if errors.Is(err, ErrModelNotFound) && b.hasScope(userID, ScopeModelSet) {
|
||||
isElevated := b.hasScope(userID, ScopeModelSet)
|
||||
|
||||
if errors.Is(err, ErrModelNotFound) && isElevated {
|
||||
return fmt.Sprintf(
|
||||
"⚠️ Model `%s` is no longer available (deprecated or removed by Anthropic).\n"+
|
||||
"Use /set_model <model-id> to switch. Current models: https://platform.claude.com/docs/en/about-claude/models/overview",
|
||||
b.config.Model,
|
||||
)
|
||||
}
|
||||
|
||||
if isElevated {
|
||||
var apiErr *anthropic.Error
|
||||
if errors.As(err, &apiErr) {
|
||||
body := apiErr.RawJSON()
|
||||
if len(body) > 800 {
|
||||
body = body[:800] + "...(truncated)"
|
||||
}
|
||||
out := fmt.Sprintf("⚠️ Anthropic API error %d:\n%s", apiErr.StatusCode, body)
|
||||
if apiErr.RequestID != "" {
|
||||
out += fmt.Sprintf("\nRequest-ID: %s", apiErr.RequestID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
// Non-API errors (network, context cancel, etc.) — show the Go error text.
|
||||
return fmt.Sprintf("⚠️ Anthropic call failed: %v", err)
|
||||
}
|
||||
|
||||
return "I'm sorry, I'm having trouble processing your request right now."
|
||||
}
|
||||
|
||||
@@ -341,14 +366,19 @@ func (b *Bot) handleUpdate(ctx context.Context, tgBot *bot.Bot, update *models.U
|
||||
isEmojiOnly := isOnlyEmojis(text)
|
||||
|
||||
// Get response from Anthropic
|
||||
response, err := b.getAnthropicResponse(ctx, contextMessages, isNewChatFlag, isOwner, isEmojiOnly, username, firstName, lastName, isPremium, languageCode, messageTime)
|
||||
segments, err := b.getAnthropicResponse(ctx, contextMessages, isNewChatFlag, isOwner, isEmojiOnly, username, firstName, lastName, isPremium, languageCode, messageTime)
|
||||
if err != nil {
|
||||
ErrorLogger.Printf("Error getting Anthropic response: %v", err)
|
||||
response = b.anthropicErrorResponse(err, userID)
|
||||
// Errors go out as a single message — no need to fan out a one-line error.
|
||||
if err := b.sendResponse(ctx, chatID, b.anthropicErrorResponse(err, userID), businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send the response
|
||||
if err := b.sendResponse(ctx, chatID, response, businessConnectionID); err != nil {
|
||||
// Successful LLM reply: deliver each text block as its own Telegram bubble,
|
||||
// matching the conversational rhythm Claude uses around tool calls.
|
||||
if err := b.sendMultiResponse(ctx, chatID, segments, businessConnectionID); err != nil {
|
||||
ErrorLogger.Printf("Error sending response: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -389,11 +419,13 @@ func (b *Bot) generateStickerResponse(ctx context.Context, message Message, cont
|
||||
// "Sent a sticker: <emoji>"), so the full conversation history is preserved.
|
||||
if message.StickerFileID != "" {
|
||||
messageTime := int(message.Timestamp.Unix())
|
||||
response, err := b.getAnthropicResponse(ctx, contextMessages, false, false, true, message.Username, "", "", false, "", messageTime)
|
||||
segments, err := b.getAnthropicResponse(ctx, contextMessages, false, false, true, message.Username, "", "", false, "", messageTime)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return response, nil
|
||||
// Sticker reactions are casual chit-chat; tool use is unusual here, so
|
||||
// join into one message rather than fanning out as multiple bubbles.
|
||||
return strings.Join(segments, "\n\n"), nil
|
||||
}
|
||||
|
||||
return "Hmm, that's interesting!", nil
|
||||
|
||||
Reference in New Issue
Block a user