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) detectLanguage(emailContent string) (string, error) { logger.WithField("emailContentLength", len(emailContent)).Debug("Starting language detection") systemMsg := "You are a language detection assistant. Analyze the provided text and respond ONLY with the language name (e.g., 'English', 'German', 'French', etc.). No other text or explanation." userMsg := fmt.Sprintf("Detect the language of this text:\n\n%s", emailContent) messages := []Message{ {Role: "system", Content: systemMsg}, {Role: "user", Content: userMsg}, } response, err := a.makeAPIRequest(messages) if err != nil { return "", fmt.Errorf("language detection failed: %v", err) } // The response should be just the language code langCode := response logger.WithField("detectedLanguage", langCode).Debug("Language detection completed") return langCode, nil } 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") // First, detect the language lang, err := a.detectLanguage(emailContent) if err != nil { return "", err } // 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 with language-specific instruction systemMsg := fmt.Sprintf(`You are a helpful assistant who responds to emails. Your primary goal is to answer the user's query based on the provided email and context. Instructions: - Language: Your response must be entirely in %s, regardless of the language used in the email content or context. - Format: Your reply must be raw HTML. Use appropriate HTML tags for structure and styling. - Markdown: Do NOT wrap the HTML in markdown code blocks (e.g., %s). - Extraneous Text: Do not include a subject line. Do not include explanations, commentary, or any extra text that is not part of the direct answer. - Closing: Avoid generic closing statements like "If you have further questions...". Focus solely on answering the email. `, lang, "```html ... ```") logger.WithFields(logrus.Fields{ "systemprompt": systemMsg, }).Debug("Generating system prompt") userMsg := fmt.Sprintf("### Additional Context:\n%s\n\n### Email Body:\n%s", context, emailContent) messages := []Message{ {Role: "system", Content: systemMsg}, {Role: "user", Content: userMsg}, } return a.makeAPIRequest(messages) } func (a *AI) makeAPIRequest(messages []Message) (string, error) { 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) }