# Architektur ## Übersicht ``` Discord (primäres Interface) ↓ Slash-Commands + @Mention + PDF-Anhänge cmd/discord/main.go ├── Research-Agent → brain.AskQuery() + Konversationsverlauf pro Channel ├── Memory-Agent → brain.RunIngest(), IngestChatMessage(), IngestURL(), CoreMemory ├── Task-Agent → tasks.json (atomares JSON, DueDate + Priority) ├── Tool-Agent → Dispatcher für Email-Aktionen └── Daemon-Goroutinen: ├── IMAP IDLE (pro Account) → Echtzeit-Triage + Discord-Benachrichtigung ├── RSS-Watcher → Artikel-Import in Qdrant ├── Morgen-Briefing (08:00) → Tasks + Emails kombiniert ├── Archiv-Cleanup (02:00) → CleanupArchiveFolders() nach retention_days └── Nacht-Ingest (23:00) → brain.IngestEmailFolder() für alle Archive ↓ Qdrant (gRPC, 192.168.1.4:6334) LocalAI (HTTP, 192.168.1.118:8080) Vektordatenbank Embedding-Modell + Chat-Modell ``` --- ## Packages ### `cmd/discord/` Primärer Einstiegspunkt. Registriert Discord Slash-Commands, verarbeitet Interaktionen und @Mentions, startet den Daemon. **Wichtige Funktionen:** - `main()` — Config laden, Discord verbinden, Commands registrieren, Daemon starten - `onInteraction()` — Slash-Command-Handler mit Berechtigungsprüfung - `onMessage()` — @Mention-Handler inkl. PDF-Anhang-Erkennung - `routeMessage()` — Leitet @Mention-Text an passenden Agenten weiter - `startDaemon()` — Startet IMAP IDLE, RSS-Watcher, tägliche Timer - `dailyBriefing()` — Morgen-Briefing (Tasks + Emails) - `nightlyIngest()` — Archiv-Ordner in Qdrant importieren - `patchEmailMoveChoices()` — Discord-Choices dynamisch aus Config befüllen - `isAllowed(userID)` — User-Berechtigungsprüfung ### `internal/config/` Konfigurationsstruktur (`Config`), Client-Factories und `AllEmailAccounts()`. ```go type Config struct { Qdrant, Embedding, Chat // Externe Dienste Discord // Token, GuildID, AllowedUsers Email / EmailAccounts // IMAP (Legacy/Multi-Account) Tasks // JSON-Pfad Daemon // Timer-Uhrzeiten, Channel-ID BrainRoot, TopK, ScoreThreshold RSSFeeds // RSS-Feed-URLs + Intervalle } ``` - `LoadConfig()` — liest `config.yml`, validiert Pflichtfelder - `AllEmailAccounts()` — gibt alle Accounts zurück (Multi-Account-Vorrang über Legacy) - `NewQdrantConn()`, `NewEmbeddingClient()`, `NewChatClient()` — Client-Factories ### `internal/brain/` Core RAG-Logik. Alle Funktionen sind zustandslos (keine globalen Verbindungen). | Datei | Inhalt | |-------|--------| | `ask.go` | `AskQuery()` (Suche + LLM), `ChatDirect()`, `searchKnowledge()` | | `ingest.go` | `RunIngest()` (Markdown), `IngestChatMessage()`, `IngestText()`, Chunking | | `ingest_json.go` | JSON-Import (Bildbeschreibungen) | | `ingest_email.go` | `IngestEmailFolder()` — IMAP-Ordner → Qdrant | | `ingest_url.go` | `IngestURL()` — HTTP-Fetch + HTML-Text-Extraktion → Qdrant | | `ingest_pdf.go` | `IngestPDF()` — PDF-Text-Extraktion → Qdrant | | `knowledge.go` | `ListSources()` (Scroll), `DeleteBySource()` (Filter-Delete) | | `core_memory.go` | `LoadCoreMemory()`, `AppendCoreMemory()` — `brain_root/core_memory.md` | **ID-Schema:** `SHA256(source + ":" + text)[:16]` als Hex — deterministische Upserts, keine Duplikate. **Chunking-Strategie:** 1. Text nach Markdown-Überschriften (`#`, `##`, `###`) aufteilen 2. Abschnitte > 800 Zeichen nach Paragraphen (`\n\n`) aufteilen 3. Minimum-Länge 20 Zeichen **Core Memory in AskQuery:** ``` System-Prompt = Basis-Prompt + "\n\n## Fakten über den Nutzer:\n" + core_memory.md (wenn nicht leer) ``` ### `internal/agents/` **`agent.go`** — Gemeinsame Interfaces: ```go type Agent interface { Handle(Request) Response } type Request struct { Action, Args, Author, Source, History } type Response struct { Text, RawAnswer string; Error error } type HistoryMessage struct { Role, Content string } ``` **`actions.go`** — Alle Action-Konstanten (typsicher, keine Magic Strings). **`memory/agent.go`** — Delegiert an `brain.*`: - `store` → `brain.IngestChatMessage()` - `ingest` → `brain.RunIngest()` **`research/agent.go`** — Ruft `brain.AskQuery()` auf, formatiert Antwort mit Quellenangaben. **`task/agent.go`** + **`task/store.go`** — Task-CRUD über atomares `tasks.json`: - Felder: `ID` (UUID), `Text`, `Done bool`, `DueDate *time.Time`, `Priority string` - Atomares Schreiben: temp-Datei → rename (kein Datenverlust bei Absturz) ### `internal/agents/tool/` **`agent.go`** — Dispatcht Email-Actions an `email`-Package-Funktionen. `ResolveArchiveFolder()` für case-insensitive Ordnersuche. **`email/client.go`** — IMAP-Client-Wrapper: - `ConnectAccount()` — Verbindung (STARTTLS oder implizites TLS) - `FetchUnread()`, `FetchWithBody()`, `FetchRecentForSelect()` - `MoveMessages()`, `MoveOldEmails()`, `DeleteByAge()` **`email/summary.go`** — Email-Zusammenfassung + Triage: - `SummarizeUnread()` — Alle Accounts, LLM-Zusammenfassung - `triageUnread()` — LLM-Klassifikation + Qdrant-Lernen via `triage` Package - `CleanupArchiveFolders()` — Alte Emails löschen nach `retention_days` **`email/idle.go`** — IMAP IDLE-Watcher: - `IdleWatcher` — pro Account, race-sicher mit `atomic.Uint32` - Automatischer Reconnect nach 60s bei Fehler - Callback bei neuen Emails → `triageUnread()` + Discord-Nachricht **`rss/watcher.go`** — RSS-Feed-Watcher: - `IngestFeed(url)` — gofeed Parser → Artikel als Text → `brain.IngestText()` - `Watcher.Run(ctx)` — Goroutine, pollt alle Feeds im minimalen Intervall - `IngestAllFeeds()` / `FormatResults()` — Batch-Import + Discord-Formatting ### `internal/triage/` Eigenes Package um Import-Zyklen zu vermeiden (`brain` ↔ `email` ↔ `triage`). - `StoreTriage()` — Triage-Entscheidung in Qdrant speichern (Typ `email_triage`) - `SearchSimilar()` — Ähnliche frühere Entscheidungen finden (Score ≥ 0.7) als Few-Shot-Kontext ### `internal/diag/` - `RunAll()` — Prüft Qdrant, LocalAI (Embedding + Chat), IMAP-Verbindungen - `Format()` / `Log()` — Ausgabe für Discord und Konsole --- ## Datenflüsse ### Slash-Command: `/ask ` ``` Discord → onInteraction → researchAgent.Handle(ActionQuery) → research/agent.go → brain.AskQuery(frage, history) → searchKnowledge() → Qdrant Search → LoadCoreMemory() → LLM Stream (LocalAI) → Discord Edit (Deferred Response) ``` ### @Bot + PDF-Anhang ``` Discord → onMessage → Attachment .pdf erkannt → http.Get(att.URL) → temp-Datei → brain.IngestPDF(tmpFile, att.Filename) → ledongthuc/pdf → Text extrahieren → ingestChunks() → Qdrant Upsert → Discord Reply ``` ### IMAP IDLE → Neue Email ``` email.IdleWatcher.Run() → IMAP IDLE Command (Server-Push) → triageUnread() → triage.SearchSimilar() (Few-Shot aus Qdrant) → LLM: wichtig / unwichtig? → triage.StoreTriage() (Entscheidung → Qdrant) → email.MoveMessages() (IMAP Move) → SummarizeUnreadAccount() (LLM-Zusammenfassung) → Discord ChannelMessageSend ``` ### RSS-Watcher ``` rss.Watcher.Run(ctx) → gofeed.Parser.ParseURL(feedURL) → buildArticleText(item) (Titel + Datum + URL + Beschreibung) → brain.IngestText() → splitLongSection() → ingestChunks() → Qdrant Upsert → Discord ChannelMessageSend (Zusammenfassung) ``` --- ## Konfigurationsvalidierung `validateConfig()` prüft beim Start: - `qdrant.host`, `qdrant.port` - `embedding.url`, `embedding.model` - `chat.url`, `chat.model` Fehlt eines dieser Felder → fataler Fehler, Bot startet nicht. Discord-Token wird separat in `main()` geprüft. --- ## Deployment-Architektur ``` Entwickler-PC (WSL2) │ bash deploy.sh │ sshpass + scp ▼ Home-Server (192.168.1.118) ├── systemd: brain-bot.service │ ExecStart: /home/christoph/brain-bot/brain-bot │ WorkingDirectory: /home/christoph/brain-bot/ │ (config.yml liegt hier) │ ├── LocalAI (Port 8080) — Embeddings + Chat └── Qdrant (auf 192.168.1.4, Port 6334) — Vektordatenbank ``` --- ## Bekannte Grenzen | Grenze | Details | |--------|---------| | LLM-Kontextfenster | `MaxTokens: 600` — lange Email-Listen werden abgeschnitten | | LLM-Latenz | 5–60s je nach Modell + NAS-Last | | IMAP-Encoding | Strato: `windows-1252` Betreffzeilen werden nicht dekodiert | | Streaming-Timeout | Kein expliziter LLM-Timeout — Discord-Interaktion läuft nach 15min ab | | PDF-Extraktion | Nur Text-PDFs; gescannte PDFs (nur Bilder) liefern keinen Text | | Discord Message-Limit | 2000 Zeichen — lange Antworten werden abgeschnitten |