199 lines
5.9 KiB
Go
199 lines
5.9 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 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)
|
|
}
|