package main import ( "context" "errors" "fmt" "net/http" "strings" "time" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/packages/param" ) // ErrModelNotFound is returned when the configured Anthropic model is no longer available // (deprecated or removed). Callers can use errors.Is to detect this and surface an // 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) { // Use prompts from config var systemMessage string if isNewChat { systemMessage = b.config.SystemPrompts["new_chat"] } else { systemMessage = b.config.SystemPrompts["continue_conversation"] } // Combine default prompt with custom instructions systemMessage = b.config.SystemPrompts["default"] + " " + b.config.SystemPrompts["custom_instructions"] + " " + systemMessage // Handle username placeholder usernameValue := username if username == "" { usernameValue = "unknown" // Use "unknown" when username is not available } systemMessage = strings.ReplaceAll(systemMessage, "{username}", usernameValue) // Handle firstname placeholder firstnameValue := firstName if firstName == "" { firstnameValue = "unknown" // Use "unknown" when first name is not available } systemMessage = strings.ReplaceAll(systemMessage, "{firstname}", firstnameValue) // Handle lastname placeholder lastnameValue := lastName if lastName == "" { lastnameValue = "" // Empty string when last name is not available } systemMessage = strings.ReplaceAll(systemMessage, "{lastname}", lastnameValue) // Handle language code placeholder langValue := languageCode if languageCode == "" { langValue = "en" // Default to English when language code is not available } systemMessage = strings.ReplaceAll(systemMessage, "{language}", langValue) // Handle premium status premiumStatus := "regular user" if isPremium { premiumStatus = "premium user" } systemMessage = strings.ReplaceAll(systemMessage, "{premium_status}", premiumStatus) // Handle time awareness timeObj := time.Unix(int64(messageTime), 0) hour := timeObj.Hour() var timeContext string if hour >= 5 && hour < 12 { timeContext = "morning" } else if hour >= 12 && hour < 18 { timeContext = "afternoon" } else if hour >= 18 && hour < 22 { timeContext = "evening" } else { timeContext = "night" } systemMessage = strings.ReplaceAll(systemMessage, "{time_context}", timeContext) if !isOwner { systemMessage += " " + b.config.SystemPrompts["avoid_sensitive"] } if isEmojiOnly { systemMessage += " " + b.config.SystemPrompts["respond_with_emojis"] } // Debug logging InfoLogger.Printf("Sending %d messages to Anthropic", len(messages)) params := anthropic.BetaMessageNewParams{ Model: b.config.Model, MaxTokens: 1000, Messages: messages, System: []anthropic.BetaTextBlockParam{{Text: systemMessage}}, } // Apply temperature if set in config if b.config.Temperature != nil { params.Temperature = param.NewOpt(float64(*b.config.Temperature)) } // MCP servers + matching toolset entries. The mcp-client-2025-11-20 beta // requires per-tool filtering on the toolset (Configs + DefaultConfig), // NOT the deprecated per-server tool_configuration block. if len(b.config.MCPServers) > 0 { mcpServers := make([]anthropic.BetaRequestMCPServerURLDefinitionParam, 0, len(b.config.MCPServers)) tools := make([]anthropic.BetaToolUnionParam, 0, len(b.config.MCPServers)) for _, s := range b.config.MCPServers { srv := anthropic.BetaRequestMCPServerURLDefinitionParam{ Name: s.Name, URL: s.URL, } if s.AuthorizationToken != "" { srv.AuthorizationToken = param.NewOpt(s.AuthorizationToken) } mcpServers = append(mcpServers, srv) toolset := &anthropic.BetaMCPToolsetParam{ MCPServerName: s.Name, } if len(s.AllowedTools) > 0 { toolset.DefaultConfig = anthropic.BetaMCPToolDefaultConfigParam{ Enabled: param.NewOpt(false), } toolset.Configs = make(map[string]anthropic.BetaMCPToolConfigParam, len(s.AllowedTools)) for _, tool := range s.AllowedTools { toolset.Configs[tool] = anthropic.BetaMCPToolConfigParam{ Enabled: param.NewOpt(true), } } } tools = append(tools, anthropic.BetaToolUnionParam{OfMCPToolset: toolset}) } params.MCPServers = mcpServers params.Tools = tools params.Betas = []anthropic.AnthropicBeta{anthropic.AnthropicBetaMCPClient2025_11_20} } resp, err := b.anthropicClient.Beta.Messages.New(ctx, params) if err != nil { var apiErr *anthropic.Error if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound { return nil, fmt.Errorf("%w: %s", ErrModelNotFound, b.config.Model) } return nil, fmt.Errorf("error creating Anthropic message: %w", err) } // 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": 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)) case "mcp_tool_result": preview := block.JSON.Content.Raw() if len(preview) > 500 { preview = preview[:500] + "...(truncated)" } InfoLogger.Printf("[mcp] tool_result tool_use_id=%q server=%q is_error=%v content=%s", block.ToolUseID, block.ServerName, block.IsError, preview) default: InfoLogger.Printf("[mcp] block type=%q (unhandled)", block.Type) } } if len(segments) == 0 { return nil, fmt.Errorf("unexpected response format from Anthropic") } return segments, nil }