// config.go – Konfiguration, Clients und gemeinsame Verbindungen package config import ( "fmt" "log" "os" openai "github.com/sashabaranov/go-openai" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "gopkg.in/yaml.v3" ) // ArchiveFolder beschreibt einen IMAP-Archivordner mit optionaler Aufbewahrungsdauer. type ArchiveFolder struct { Name string `yaml:"name"` // Anzeigename (z.B. "5Jahre") IMAPFolder string `yaml:"imap_folder"` // Echter IMAP-Ordnername (z.B. "5Jahre") RetentionDays int `yaml:"retention_days"` // 0 = dauerhaft behalten } // EmailAccount beschreibt einen einzelnen IMAP-Account. type EmailAccount struct { Name string `yaml:"name"` Host string `yaml:"host"` Port int `yaml:"port"` User string `yaml:"user"` Password string `yaml:"password"` TLS bool `yaml:"tls"` StartTLS bool `yaml:"starttls"` Folder string `yaml:"folder"` ProcessedFolder string `yaml:"processed_folder"` Model string `yaml:"model"` ArchiveFolders []ArchiveFolder `yaml:"archive_folders"` TriageImportantFolder string `yaml:"triage_important_folder"` // Ordner für wichtige Emails (leer = in INBOX lassen) TriageUnimportantFolder string `yaml:"triage_unimportant_folder"` // Ordner für unwichtige Emails (leer = kein Triage) } type Config struct { Qdrant struct { Host string `yaml:"host"` Port string `yaml:"port"` APIKey string `yaml:"api_key"` Collection string `yaml:"collection"` } `yaml:"qdrant"` Embedding struct { URL string `yaml:"url"` Model string `yaml:"model"` Dimensions uint64 `yaml:"dimensions"` } `yaml:"embedding"` Chat struct { URL string `yaml:"url"` Model string `yaml:"model"` } `yaml:"chat"` Discord struct { Token string `yaml:"token"` GuildID string `yaml:"guild_id"` AllowedUsers []string `yaml:"allowed_users"` // Wenn gesetzt, dürfen nur diese User-IDs den Bot nutzen } `yaml:"discord"` // Email ist der Legacy-Block für einen einzelnen Account. Email struct { Host string `yaml:"host"` Port int `yaml:"port"` User string `yaml:"user"` Password string `yaml:"password"` TLS bool `yaml:"tls"` StartTLS bool `yaml:"starttls"` Folder string `yaml:"folder"` ProcessedFolder string `yaml:"processed_folder"` Model string `yaml:"model"` ArchiveFolders []ArchiveFolder `yaml:"archive_folders"` TriageImportantFolder string `yaml:"triage_important_folder"` TriageUnimportantFolder string `yaml:"triage_unimportant_folder"` } `yaml:"email"` // EmailAccounts ermöglicht mehrere IMAP-Accounts. Hat Vorrang vor email:. EmailAccounts []EmailAccount `yaml:"email_accounts"` Tasks struct { StorePath string `yaml:"store_path"` } `yaml:"tasks"` Daemon struct { ChannelID string `yaml:"channel_id"` EmailIntervalMin int `yaml:"email_interval_min"` TaskReminderHour int `yaml:"task_reminder_hour"` CleanupHour int `yaml:"cleanup_hour"` // Uhrzeit für tägliches Archiv-Aufräumen (Standard: 2) IngestHour int `yaml:"ingest_hour"` // Uhrzeit für nächtlichen Email-Ingest (Standard: 23, 0 = deaktiviert) } `yaml:"daemon"` BrainRoot string `yaml:"brain_root"` TopK uint64 `yaml:"top_k"` ScoreThreshold float32 `yaml:"score_threshold"` // RSSFeeds definiert RSS-Feeds die automatisch überwacht werden. RSSFeeds []RSSFeed `yaml:"rss_feeds"` } // RSSFeed beschreibt einen RSS-Feed mit Polling-Intervall. type RSSFeed struct { URL string `yaml:"url"` IntervalHours int `yaml:"interval_hours"` // 0 = Standard 24h } var Cfg Config // AllEmailAccounts gibt alle konfigurierten Email-Accounts zurück. // Wenn email_accounts konfiguriert ist, hat das Vorrang vor dem Legacy-email:-Block. func AllEmailAccounts() []EmailAccount { if len(Cfg.EmailAccounts) > 0 { return Cfg.EmailAccounts } if Cfg.Email.Host == "" { return nil } return []EmailAccount{{ Name: "Email", Host: Cfg.Email.Host, Port: Cfg.Email.Port, User: Cfg.Email.User, Password: Cfg.Email.Password, TLS: Cfg.Email.TLS, StartTLS: Cfg.Email.StartTLS, Folder: Cfg.Email.Folder, ProcessedFolder: Cfg.Email.ProcessedFolder, Model: Cfg.Email.Model, ArchiveFolders: Cfg.Email.ArchiveFolders, TriageImportantFolder: Cfg.Email.TriageImportantFolder, TriageUnimportantFolder: Cfg.Email.TriageUnimportantFolder, }} } // NewQdrantConn öffnet eine gRPC-Verbindung zur Qdrant-Instanz. // Der Aufrufer ist verantwortlich für conn.Close(). func NewQdrantConn() *grpc.ClientConn { conn, err := grpc.Dial( fmt.Sprintf("%s:%s", Cfg.Qdrant.Host, Cfg.Qdrant.Port), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { log.Fatalf("❌ Qdrant Verbindung fehlgeschlagen: %v", err) } return conn } // NewEmbeddingClient erstellt einen Client für LocalAI (Embeddings). func NewEmbeddingClient() *openai.Client { c := openai.DefaultConfig("localai") c.BaseURL = Cfg.Embedding.URL return openai.NewClientWithConfig(c) } // NewChatClient erstellt einen Client für Chat-Completion (LocalAI). func NewChatClient() *openai.Client { c := openai.DefaultConfig("localai") c.BaseURL = Cfg.Chat.URL return openai.NewClientWithConfig(c) } // LoadConfig liest config.yml aus dem aktuellen Verzeichnis und validiert Pflichtfelder. func LoadConfig() { data, err := os.ReadFile("config.yml") if err != nil { log.Fatalf("❌ config.yml nicht gefunden: %v\n Lege config.yml im selben Verzeichnis an.", err) } if err := yaml.Unmarshal(data, &Cfg); err != nil { log.Fatalf("❌ config.yml ungültig: %v", err) } validateConfig() } // validateConfig prüft Pflichtfelder und gibt früh eine klare Fehlermeldung. func validateConfig() { var errs []string if Cfg.Qdrant.Host == "" { errs = append(errs, "qdrant.host fehlt") } if Cfg.Qdrant.Port == "" { errs = append(errs, "qdrant.port fehlt") } if Cfg.Embedding.URL == "" { errs = append(errs, "embedding.url fehlt") } if Cfg.Embedding.Model == "" { errs = append(errs, "embedding.model fehlt") } if Cfg.Chat.URL == "" { errs = append(errs, "chat.url fehlt") } if Cfg.Chat.Model == "" { errs = append(errs, "chat.model fehlt") } if len(errs) > 0 { for _, e := range errs { log.Printf("❌ config.yml: %s", e) } log.Fatal("❌ Konfiguration unvollständig – Bot wird nicht gestartet.") } }