package ai import ( "bytes" "encoding/json" "fmt" "net/http" "paraclub-ai-mailer/config" "paraclub-ai-mailer/internal/logger" "github.com/sirupsen/logrus" ) type AI struct { config config.AIConfig client *http.Client } type OpenRouterRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` Temperature float32 `json:"temperature"` MaxTokens int `json:"max_tokens"` } type Message struct { Role string `json:"role"` Content string `json:"content"` } type OpenRouterResponse struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` } `json:"choices"` } func New(cfg config.AIConfig) *AI { return &AI{ config: cfg, client: &http.Client{}, } } func (a *AI) GenerateReply(emailContent string, contextContent map[string]string) (string, error) { logger.WithFields(logrus.Fields{ "emailContentLength": len(emailContent), "contextUrls": len(contextContent), }).Debug("Starting AI reply generation") // Prepare context from all URLs var context string for url, content := range contextContent { context += fmt.Sprintf("\nContext from %s:\n%s\n", url, content) logger.WithFields(logrus.Fields{ "url": url, "contentLength": len(content), }).Debug("Added context from URL") } // Prepare the system message and user message // systemMsg := "You are a helpful assistant responding to emails. Analyze the language of the incoming email and translate the response to the same language. Format your response in HTML. Do not include any explanations or extra text - just write the email response directly in HTML format. Use appropriate HTML tags for formatting." // systemMsg := "You are a helpful assistant who responds to emails. For each incoming email, first detect its language and then generate your response in the exact same language. Your reply must be written directly in HTML format using appropriate HTML tags for structure and styling. Do not include any explanations, commentary, or extra text—simply output the email response in HTML." // systemMsg := "You are a helpful assistant who responds to emails. For each incoming email, first detect its language and then generate your response in the exact same language. Your reply must be written directly in HTML format using appropriate HTML tags for structure and styling. Do not include a subject line, explanations, commentary, or any extra text—simply output the email response content in HTML." // systemMsg := "You are a helpful assistant who responds to emails. For every incoming email, carefully detect and confirm its language. Then, generate your email response entirely in that same language without deviation. Format your reply solely in HTML using appropriate HTML tags for structure and styling, and do not include a subject line, explanations, or any extra text. Ensure that every part of your response exactly matches the language of the incoming email." systemMsg := "You are a helpful assistant who responds to emails. For every incoming email, strictly use only the email's content to detect its language and ignore any external or additional context. Then, generate your email response entirely in that same language without deviation. Format your reply solely in HTML using appropriate HTML tags for structure and styling, and do not include a subject line, explanations, commentary, or any extra text. Ensure that every part of your response exactly matches the language of the incoming email." userMsg := fmt.Sprintf("Using the following context:\n%s\n\nPlease analyze the language of and generate a reply in the same language for this email:\n%s", context, emailContent) messages := []Message{ {Role: "system", Content: systemMsg}, {Role: "user", Content: userMsg}, } const maxRetries = 3 var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { logger.WithField("attempt", attempt+1).Debug("Making API request attempt") reqBody := OpenRouterRequest{ Model: a.config.Model, Messages: messages, Temperature: a.config.Temperature, MaxTokens: a.config.MaxTokens, } logger.WithFields(logrus.Fields{ "model": reqBody.Model, "temperature": reqBody.Temperature, "maxTokens": reqBody.MaxTokens, }).Debug("Prepared API request") jsonData, err := json.Marshal(reqBody) if err != nil { logger.WithError(err).Error("Failed to marshal request body") return "", err } req, err := http.NewRequest("POST", "https://openrouter.ai/api/v1/chat/completions", bytes.NewBuffer(jsonData)) if err != nil { logger.WithError(err).Error("Failed to create HTTP request") return "", err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.config.OpenRouterAPIKey)) resp, err := a.client.Do(req) if err != nil { logger.WithFields(logrus.Fields{ "error": err, "attempt": attempt + 1, }).Debug("API request failed, will retry") lastErr = err continue } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { logger.WithFields(logrus.Fields{ "statusCode": resp.StatusCode, "attempt": attempt + 1, }).Debug("API returned non-200 status code") lastErr = fmt.Errorf("OpenRouter API returned status code: %d", resp.StatusCode) continue } var result OpenRouterResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { logger.WithFields(logrus.Fields{ "error": err, "attempt": attempt + 1, }).Debug("Failed to decode API response") lastErr = err continue } if len(result.Choices) == 0 || result.Choices[0].Message.Content == "" { logger.WithField("attempt", attempt+1).Debug("Received empty response from API") lastErr = fmt.Errorf("empty response received") continue } responseLength := len(result.Choices[0].Message.Content) logger.WithFields(logrus.Fields{ "attempt": attempt + 1, "responseLength": responseLength, }).Debug("Successfully generated AI reply") return result.Choices[0].Message.Content, nil } logger.WithFields(logrus.Fields{ "maxRetries": maxRetries, "lastError": lastErr, }).Error("Failed to generate AI reply after all attempts") return "", fmt.Errorf("failed after %d attempts, last error: %v", maxRetries, lastErr) }