Files
ai-mailer/internal/ai/ai.go

190 lines
5.6 KiB
Go

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.Regardless of the language used in the email content or context, your response must be entirely in %s. Format your reply solely in HTML using appropriate HTML tags for structure and styling. Do not include a subject line, explanations, commentary, or any extra text.", lang)
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)
}