// email/summary.go – LLM-Zusammenfassung von Emails via LocalAI package email import ( "context" "fmt" "log/slog" "strings" "time" openai "github.com/sashabaranov/go-openai" "my-brain-importer/internal/config" ) // Summarize verbindet sich mit IMAP, holt die letzten 20 Emails und fasst sie per LLM zusammen. func Summarize() (string, error) { return fetchAndSummarize(20, "Fasse diese Emails kurz zusammen und hebe wichtige oder dringende hervor.") } // SummarizeUnread fasst ungelesene Emails zusammen. // Wenn email.processed_folder konfiguriert ist, werden die Emails danach dorthin verschoben. func SummarizeUnread() (string, error) { cl, err := Connect() if err != nil { return "", fmt.Errorf("Email-Verbindung: %w", err) } defer cl.Close() processedFolder := config.Cfg.Email.ProcessedFolder var msgs []Message var seqNums []uint32 if processedFolder != "" { msgs, seqNums, err = cl.FetchUnreadSeqNums() } else { msgs, err = cl.FetchUnread() } if err != nil { return "", fmt.Errorf("Emails abrufen: %w", err) } if len(msgs) == 0 { return "📭 Keine ungelesenen Emails.", nil } slog.Info("Email-Zusammenfassung gestartet", "anzahl", len(msgs), "typ", "unread") result, err := summarizeWithLLM(msgs, "Fasse diese ungelesenen Emails zusammen. Hebe wichtige, dringende oder actionable Emails hervor.") if err != nil { return "", err } // Nach dem Zusammenfassen: Emails in Processed-Ordner verschieben if processedFolder != "" && len(seqNums) > 0 { if moveErr := cl.MoveMessages(seqNums, processedFolder); moveErr != nil { slog.Warn("Emails konnten nicht verschoben werden", "fehler", moveErr, "ordner", processedFolder) } else { slog.Info("Emails verschoben", "anzahl", len(seqNums), "ordner", processedFolder) } } return result, nil } // ExtractReminders sucht in den letzten Emails nach Terminen/Deadlines. func ExtractReminders() (string, error) { return fetchAndSummarize(30, "Extrahiere alle Termine, Deadlines, Erinnerungen und wichtigen Daten aus diesen Emails. Liste sie strukturiert auf.") } // SummarizeMessages fasst eine übergebene Liste von Nachrichten zusammen (für Tests ohne IMAP). func SummarizeMessages(msgs []Message, instruction string) (string, error) { return summarizeWithLLM(msgs, instruction) } func fetchAndSummarize(n uint32, instruction string) (string, error) { cl, err := Connect() if err != nil { return "", fmt.Errorf("Email-Verbindung: %w", err) } defer cl.Close() msgs, err := cl.FetchRecent(n) if err != nil { return "", fmt.Errorf("Emails abrufen: %w", err) } if len(msgs) == 0 { return "📭 Keine Emails gefunden.", nil } slog.Info("Email-Zusammenfassung gestartet", "anzahl", len(msgs)) return summarizeWithLLM(msgs, instruction) } // emailModel gibt das konfigurierte Modell für Email-Zusammenfassungen zurück. // Fällt auf chat.model zurück wenn email.model nicht gesetzt ist. func emailModel() string { if config.Cfg.Email.Model != "" { return config.Cfg.Email.Model } return config.Cfg.Chat.Model } // formatEmailList formatiert Emails als lesbaren Text (Fallback und Eingabe fürs LLM). func formatEmailList(msgs []Message) string { var sb strings.Builder for i, m := range msgs { fmt.Fprintf(&sb, "[%d] Von: %s | Datum: %s | Betreff: %s\n", i+1, m.From, m.Date, m.Subject) } return sb.String() } func summarizeWithLLM(msgs []Message, instruction string) (string, error) { emailList := formatEmailList(msgs) model := emailModel() chatClient := config.NewChatClient() ctx := context.Background() systemPrompt := `Du bist ein hilfreicher persönlicher Assistent. Analysiere Email-Listen und antworte auf Deutsch, präzise und strukturiert.` userPrompt := fmt.Sprintf("%s\n\nEmail-Liste:\n%s", instruction, emailList) slog.Debug("[LLM] Email Prompt", "model", model, "emails", len(msgs), "system", systemPrompt, "user", userPrompt, ) start := time.Now() stream, err := chatClient.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{ Model: model, Messages: []openai.ChatCompletionMessage{ {Role: openai.ChatMessageRoleSystem, Content: systemPrompt}, {Role: openai.ChatMessageRoleUser, Content: userPrompt}, }, Temperature: 0.5, MaxTokens: 600, }) if err != nil { slog.Warn("[LLM] nicht erreichbar, Fallback-Liste", "fehler", err) return fallbackList(msgs), nil } defer stream.Close() var answer strings.Builder for { resp, err := stream.Recv() if err != nil { break } if len(resp.Choices) > 0 { answer.WriteString(resp.Choices[0].Delta.Content) } } result := answer.String() slog.Debug("[LLM] Email Antwort", "dauer", time.Since(start).Round(time.Millisecond), "zeichen", len(result), "antwort", result, ) if strings.TrimSpace(result) == "" { slog.Warn("[LLM] leere Antwort, Fallback-Liste") return fallbackList(msgs), nil } slog.Info("[LLM] Email-Zusammenfassung abgeschlossen", "dauer", time.Since(start).Round(time.Millisecond), "zeichen", len(result)) return result, nil } // fallbackList gibt eine einfache formatierte Liste zurück wenn das LLM nicht verfügbar ist. func fallbackList(msgs []Message) string { var sb strings.Builder sb.WriteString("⚠️ *LLM nicht verfügbar – ungefilterte Email-Liste:*\n\n") for i, m := range msgs { fmt.Fprintf(&sb, "**[%d]** %s\n📤 %s\n📌 %s\n\n", i+1, m.Date, m.From, m.Subject) } return sb.String() }