# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview **my-brain-importer** is a personal AI assistant and RAG (Retrieval-Augmented Generation) system written in Go. It ingests Markdown notes into a Qdrant vector database, answers questions using a local LLM (LocalAI), and is primarily controlled via Discord. A background daemon sends proactive email summaries and a daily morning briefing. ## Commands ```bash # Build all binaries (Linux + Windows cross-compile) bash build.sh # Primary entry point: Discord Bot (includes daemon) go run ./cmd/discord/ # CLI tools go run ./cmd/ingest/ # Markdown importieren go run ./cmd/ingest/ bild.json # JSON importieren go run ./cmd/ask/ "your question here" # Frage stellen # Test: IMAP-Verbindung go run ./cmd/mailtest/ # Test: LLM-Zusammenfassung ohne IMAP go run ./cmd/mailtest/ -llm-only # Run tests go test ./... # Run a single test go test ./internal/brain/ -run TestChunk -v # Debug-Logging (LLM-Prompts + Antworten) DEBUG=1 go run ./cmd/discord/ # Tidy dependencies go mod tidy # Integration test (prüft Erreichbarkeit aller externen Dienste + Unit-Tests) bash test-integration.sh # Deployment auf Remote-Server via Docker cp deploy.env.example deploy.env # einmalig: Credentials eintragen bash deploy.sh # rsync source + docker compose build + up ``` Binaries are output to `./bin/`. The `config.yml` file must exist in the working directory at runtime. ## Architecture ``` Discord (primäres Interface) ↓ Slash-Commands + @Mention cmd/discord/main.go ├── internal/agents/research/ → brain.AskQuery() + Konversationsverlauf ├── internal/agents/memory/ → brain.RunIngest(), brain.IngestChatMessage() ├── internal/agents/task/ → tasks.json (atomisches JSON, DueDate + Priority) └── internal/agents/tool/email/ → IMAP + LLM-Triage + Zusammenfassung + Move to Archive + Cleanup ↓ [Daemon-Goroutine] startDaemon() ├── IMAP IDLE (pro Account) → Echtzeit-Email-Benachrichtigung + LLM-Triage ├── Morgen-Briefing (täglich 8h) → Tasks + Emails kombiniert ├── Archiv-Aufräumen (täglich 2h) → CleanupArchiveFolders() nach retention_days └── Nacht-Ingest (täglich 23h) → brain.IngestEmailFolder() für alle Archiv-Ordner cmd/ingest/ + cmd/ask/ (CLI-Tools, direkt nutzbar) ↓ internal/brain/ (Core RAG-Logik) ↓ Qdrant (gRPC) + LocalAI (HTTP, OpenAI-kompatibel) ``` ### Packages | Package | Zweck | |---------|-------| | `cmd/discord/` | Discord Bot + Daemon (primärer Einstiegspunkt) | | `cmd/ask/` | CLI-Tool: Fragen stellen | | `cmd/ingest/` | CLI-Tool: Markdown/JSON importieren | | `cmd/mailtest/` | Testprogramm: IMAP + LLM-Test | | `internal/brain/` | Core RAG: Embeddings, Qdrant-Suche, LLM-Streaming | | `internal/config/` | Konfiguration + Client-Initialisierung (globale `Cfg`) | | `internal/agents/` | Agent-Interface (`Request`/`Response`/`HistoryMessage`) | | `internal/agents/research/` | Research-Agent: Wissensdatenbank-Abfragen (mit History) | | `internal/agents/memory/` | Memory-Agent: Ingest + Chat-Speicherung | | `internal/agents/task/` | Task-Agent: Aufgabenverwaltung (tasks.json) | | `internal/agents/tool/` | Tool-Dispatcher | | `internal/agents/tool/email/` | IMAP-Client + LLM-Triage + Email-Analyse + IDLE-Watcher + Move to Archive + CleanupOldEmails | | `internal/agents/tool/rss/` | RSS-Feed-Watcher: Feeds fetchen, Artikel in Qdrant importieren, Daemon-Integration | | `internal/triage/` | RAG-basiertes Lernen: Triage-Entscheidungen in Qdrant speichern + suchen (eigenes Package um Import-Zyklus brain↔email zu vermeiden) | ### Discord Commands | Slash-Command | @Mention | Funktion | |---------------|----------|---------| | `/ask`, `/research` | `@bot ` | Wissensdatenbank abfragen (mit Chat-Gedächtnis) | | `/deepask` | – | Tiefe Recherche mit Multi-Step Reasoning (max 3 iterative RAG-Suchen) | | `/asknobrain` | – | Direkt an LLM (kein RAG) | | `/memory store` | `@bot remember ` | Text speichern | | `/memory ingest` | `@bot ingest` | Markdown neu einlesen | | `/memory url ` | – | URL-Inhalt in Wissensdatenbank importieren | | `/memory profile ` | – | Fakt zum Kerngedächtnis hinzufügen (wird in jeden LLM-Prompt eingebaut) | | `/memory profile-show` | – | Kerngedächtnis anzeigen | | `/knowledge list` | – | Gespeicherte Quellen auflisten | | `/knowledge delete ` | – | Quelle aus Wissensdatenbank löschen | | `/task add [faellig] [prioritaet]` | `@bot task add [--due YYYY-MM-DD] [--priority hoch]` | Task hinzufügen | | `/task list/done/delete` | `@bot task ` | Aufgaben verwalten | | `/email summary/unread/remind` | `@bot email ` | Email-Analyse | | `/email ingest [ordner]` | `@bot email ingest [ordner]` | Emails aus IMAP-Ordner in Wissensdatenbank importieren (Standard: Archiv) | | `/email move ` | `@bot email move ` | Ungelesene Emails in Archivordner verschieben (Choices dynamisch aus `archive_folders`) | | `/status` | – | Bot-Gesundheit: alle Dienste + offene Tasks | | `/clear` | `@bot clear` | Gesprächsverlauf für diesen Channel löschen | | `/remember` | – | Alias für `/memory store` | | `/ingest` | – | Alias für `/memory ingest` | | *(PDF-Anhang)* | `@bot` + PDF-Datei | PDF direkt per Discord importieren | ## Key Patterns - **Deterministic IDs**: SHA256 of `source:text` — upserting the same content is always idempotent - **Excluded directories**: `05_Agents` and `.git` are skipped during markdown ingest - **config.yml** must be present in the working directory at runtime - **Agent Interface**: alle Agenten implementieren `Handle(Request) Response` - **Defer-first Pattern**: Discord-Handlers senden sofort Defer, dann berechnen — nie >3s warten - **LLM-Fallback**: Email-Zusammenfassung zeigt Rohliste wenn LLM nicht erreichbar - **Daemon**: läuft als Goroutine im Discord-Bot-Prozess (`startDaemon()`) - **config.Cfg**: globale Variable — bei Tests muss `config.LoadConfig()` aufgerufen oder Cfg direkt gesetzt werden - **Konversationsverlauf**: Pro Discord-Channel werden die letzten 10 Frage-Antwort-Paare in-memory gehalten und als History an `brain.AskQuery()` übergeben — Reset via `/clear` oder `@bot clear` - **Task-Felder**: `DueDate *time.Time` und `Priority string` (hoch/mittel/niedrig) — rückwärtskompatibel (omitempty) - **Email processed_folder**: Nach Zusammenfassung werden ungelesene Emails in konfigurierten IMAP-Ordner verschoben (leer = deaktiviert) - **Morgen-Briefing**: `dailyBriefing()` kombiniert offene Tasks (mit Fälligkeits-Highlighting) + ungelesene Emails täglich um 8:00 - **Archiv-Cleanup**: `email.CleanupArchiveFolders()` läuft täglich um `cleanup_hour` (Standard: 2:00) — iteriert alle Accounts/`archive_folders`, löscht Emails älter als `retention_days` via IMAP `\Deleted` + `EXPUNGE`. `retention_days: 0` = dauerhaft behalten (No-op). - **Email-Triage**: `email.triageUnread()` klassifiziert ungelesene Emails sequentiell (eine nach der anderen) als wichtig/unwichtig via LLM. Wichtige Emails werden in `triage_important_folder`, unwichtige in `triage_unimportant_folder` verschoben. Jede Entscheidung wird in Qdrant gespeichert (`type: email_triage`). Bei nächster Klassifizierung sucht `triage.SearchSimilar()` ähnliche Entscheidungen (Score ≥ 0.7) als Few-Shot-Kontext — das Modell lernt aus der Geschichte. Triage läuft vor `SummarizeUnreadAccount()`. - **Nacht-Ingest**: `nightlyIngest()` läuft täglich um `ingest_hour` (Standard: 23:00) — importiert alle Emails aller Archiv-Ordner in Qdrant via `brain.IngestEmailFolder()`. - **User-Permissions**: `discord.allowed_users: ["user-id1", "user-id2"]` — wenn gesetzt, dürfen nur diese Discord-User-IDs den Bot nutzen. Leer = keine Einschränkung. - **URL-Ingest**: `brain.IngestURL(url)` — fetcht URL, extrahiert sichtbaren HTML-Text (skippt script/style/nav/footer), chunked und importiert in Qdrant. Via `/memory url `. - **PDF-Ingest**: `brain.IngestPDF(path, source)` — extrahiert Text aus PDF via `github.com/ledongthuc/pdf`, chunked und importiert. Trigger: PDF-Anhang an @Bot-Mention. - **Knowledge Management**: `brain.ListSources()` (Qdrant Scroll) + `brain.DeleteBySource()` (Qdrant Filter-Delete). Via `/knowledge list` und `/knowledge delete `. - **Core Memory**: `brain_root/core_memory.md` — persistente Fakten über den Nutzer. `brain.LoadCoreMemory()` wird in `AskQuery()` in den System-Prompt eingefügt. Via `/memory profile ` und `/memory profile-show`. - **RSS-Watcher**: `rss.Watcher` — fetcht alle `rss_feeds` aus Config, importiert neue Artikel via `brain.IngestText()`. Läuft als Goroutine im Daemon. - **IngestText**: `brain.IngestText(text, source, type)` — generische Ingest-Funktion für beliebige Texte (kein Datei-I/O nötig). - **Dynamische /email move Choices**: `patchEmailMoveChoices()` wird in `main()` nach `config.LoadConfig()` aufgerufen und ersetzt die statischen Discord-Choices mit konfigurierten `archive_folders`. Fallback auf Legacy-Hardcoding (`2Jahre`/`5Jahre`/`Archiv`) wenn keine `archive_folders` konfiguriert. - **Archive folder resolution**: `resolveArchiveFolder(name)` in `tool/agent.go` sucht case-insensitiv in `acc.ArchiveFolders` (Name oder IMAPFolder), dann Legacy-Fallback. Gilt für Slash-Commands und `@bot email move `. - **IMAP IDLE**: `email.IdleWatcher` pro Account — Echtzeit-Benachrichtigung bei neuen Emails, kein Polling mehr. Race-sichere Implementierung mit `atomic.Uint32` für `numMsgs`. Automatischer Reconnect nach 60s bei Fehler. - **Mehrere Email-Accounts**: `config.AllEmailAccounts()` gibt alle Accounts zurück — zuerst `email_accounts:` (Liste), Fallback auf Legacy `email:` Block. Alle Email-Funktionen iterieren über alle Accounts. - **`/status`**: Ruft `diag.RunAll()` auf + Task-Zähler — zeigt Echtzeit-Status aller externen Dienste ## config.yml – Alle Felder ```yaml # Einzelner Email-Account (Legacy, abwärtskompatibel): email: host: imap.strato.de port: 143 user: user@example.de password: "..." starttls: true # oder tls: true für implizites TLS (Port 993) folder: INBOX # optional, Standard: INBOX processed_folder: "Processed" # nach Zusammenfassung verschieben (leer = deaktiviert) triage_important_folder: "Wichtig" # LLM-Triage: wichtige Emails hierhin (leer = in INBOX lassen) triage_unimportant_folder: "Unwichtig" # LLM-Triage: unwichtige Emails hierhin (leer = deaktiviert) model: "" # optional: eigenes LLM-Modell für Email-Analyse archive_folders: # optional: Archivordner mit automatischer Bereinigung - name: "Archiv" imap_folder: "Archiv" retention_days: 0 # 0 = dauerhaft behalten (kein Cleanup) - name: "5Jahre" imap_folder: "5Jahre" retention_days: 1825 - name: "2Jahre" imap_folder: "2Jahre" retention_days: 730 # Mehrere Email-Accounts (hat Vorrang vor email:): email_accounts: - name: "Privat" host: imap.strato.de port: 143 starttls: true user: privat@example.de password: "..." processed_folder: "Processed" archive_folders: # optional, wie im email:-Block oben - name: "Archiv" imap_folder: "Archiv" retention_days: 0 - name: "Arbeit" host: imap.firma.de port: 993 tls: true user: jacek@firma.de password: "..." daemon: channel_id: "123456789" # Discord-Channel für Daemon-Nachrichten email_interval_min: 30 # (veraltet, IDLE ersetzt Polling) task_reminder_hour: 8 # Uhrzeit des Morgen-Briefings (Standard: 8) cleanup_hour: 2 # Uhrzeit des täglichen Archiv-Aufräumens (Standard: 2) ingest_hour: 23 # Uhrzeit des nächtlichen Email-Ingests (Standard: 23) # Discord User-Permissions (optional): discord: token: "..." guild_id: "..." # optional: nur für diese Guild registrieren allowed_users: # optional: leer = alle dürfen den Bot nutzen - "123456789" # Discord User-ID # RSS-Feeds (optional): rss_feeds: - url: "https://example.com/feed.xml" interval_hours: 24 # Polling-Intervall (Standard: 24h) - url: "https://news.example.de/rss" interval_hours: 6 ``` ## Deployment Deployment läuft über Docker auf dem Remote-Server. `deploy.sh` cross-compiliert das Binary **lokal** (`CGO_ENABLED=0 GOOS=linux GOARCH=amd64`), überträgt Binary + Dockerfile + docker-compose.yml + config.yml per `sshpass`/`scp` und startet den Container auf dem Server via `docker compose up -d --build`. Kein Go-Toolchain auf dem Server nötig. ```bash # deploy.env (nicht in Git): DEPLOY_HOST=192.168.1.118 DEPLOY_USER=christoph DEPLOY_PASS=... DEPLOY_DIR=/home/christoph/brain-bot # Deploy: lokal bauen + scp + docker compose up bash deploy.sh # Status/Logs auf dem Server: ssh user@host 'cd /home/christoph/brain-bot && docker compose ps' ssh user@host 'cd /home/christoph/brain-bot && docker compose logs -f' ``` Docker-Setup: Minimales Runtime-Image (`alpine:3.21`), `Dockerfile` kopiert nur das fertige Binary. `docker-compose.yml` mountet `config.yml`, `tasks.json` und `brain_data/`. ### Systemd (DEPRECATED) Der alte Systemd-Ansatz wird nicht mehr aktiv genutzt. `deploy.sh` deaktiviert einen vorhandenen systemd-Service automatisch bei der Migration zu Docker. ```bash # Einmalig auf dem Server (falls noch nicht migriert): sudo systemctl stop brain-bot sudo systemctl disable brain-bot # Manueller Fallback ohne Docker: scp bin/discord-bot user@host:/home/christoph/brain-bot/ ssh user@host 'sudo systemctl restart brain-bot' ``` ## Model Limitations Das konfigurierte Modell (`Qwen3.5-4B-Claude-4.6-Opus-Reasoning-Distilled-GGUF`) hat folgende Grenzen: - **Kontextfenster**: Begrenzt — bei sehr langen Email-Listen oder vielen Chunks kann die Antwort abgeschnitten werden (`MaxTokens: 600`) - **Latenz**: Lokales Modell auf NAS — Antwortzeiten variieren (5–60s je nach Last) - **Encoding**: Betreffzeilen in `windows-1252` (Strato) werden nicht dekodiert — das LLM interpretiert sie trotzdem meist korrekt - **Halluzinationen**: Das Modell kann bei unklarem Kontext eigenes Wissen einmischen — ist im System-Prompt mit "Aus meinem Wissen:" markiert - **Streaming-Timeout**: Kein expliziter Timeout auf LLM-Calls — bei Hänger wird Discord-Interaktion erst nach 15min ungültig ## External Services - **Qdrant** (`192.168.1.4:6334`) — Vektordatenbank, gRPC - **LocalAI** (`192.168.1.118:8080`) — lokales LLM, OpenAI-kompatibles API - **Strato IMAP** (`imap.strato.de:143`, STARTTLS) — Email-Abruf - **Discord** — primäres Interface (Bot-Token in `config.yml`)