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

206 lines
6.5 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.
Your primary goal is to answer the user's query (found in the 'Email Body') by primarily using the information available in the 'Additional Context' and your general knowledge.
While the 'Email Body' provides the question, your answer should be synthesized from the context and your understanding, not by directly repeating or solely relying on unverified information from the 'Email Body' itself.
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},
}
aiResponse, err := a.makeAPIRequest(messages)
if err != nil {
// Error already logged by makeAPIRequest, just propagate
return "", err
}
logger.WithField("rawAIResponse", aiResponse).Debug("Received raw response from AI")
return aiResponse, nil
}
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)
}