15 KiB
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
# 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 <frage> |
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> |
Text speichern |
/memory ingest |
@bot ingest |
Markdown neu einlesen |
/memory url <url> |
– | URL-Inhalt in Wissensdatenbank importieren |
/memory profile <text> |
– | 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 <source> |
– | Quelle aus Wissensdatenbank löschen |
/task add <text> [faellig] [prioritaet] |
@bot task add <text> [--due YYYY-MM-DD] [--priority hoch] |
Task hinzufügen |
/task list/done/delete |
@bot task <aktion> |
Aufgaben verwalten |
/email summary/unread/remind |
@bot email <aktion> |
Email-Analyse |
/email ingest [ordner] |
@bot email ingest [ordner] |
Emails aus IMAP-Ordner in Wissensdatenbank importieren (Standard: Archiv) |
/email move <ordner> |
@bot email move <ordner> |
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_Agentsand.gitare 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/clearoder@bot clear - Task-Felder:
DueDate *time.TimeundPriority 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 umcleanup_hour(Standard: 2:00) — iteriert alle Accounts/archive_folders, löscht Emails älter alsretention_daysvia 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 intriage_important_folder, unwichtige intriage_unimportant_folderverschoben. Jede Entscheidung wird in Qdrant gespeichert (type: email_triage). Bei nächster Klassifizierung suchttriage.SearchSimilar()ähnliche Entscheidungen (Score ≥ 0.7) als Few-Shot-Kontext — das Modell lernt aus der Geschichte. Triage läuft vorSummarizeUnreadAccount(). - Nacht-Ingest:
nightlyIngest()läuft täglich umingest_hour(Standard: 23:00) — importiert alle Emails aller Archiv-Ordner in Qdrant viabrain.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 <url>. - PDF-Ingest:
brain.IngestPDF(path, source)— extrahiert Text aus PDF viagithub.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 listund/knowledge delete <source>. - Core Memory:
brain_root/core_memory.md— persistente Fakten über den Nutzer.brain.LoadCoreMemory()wird inAskQuery()in den System-Prompt eingefügt. Via/memory profile <text>und/memory profile-show. - RSS-Watcher:
rss.Watcher— fetcht allerss_feedsaus Config, importiert neue Artikel viabrain.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 inmain()nachconfig.LoadConfig()aufgerufen und ersetzt die statischen Discord-Choices mit konfiguriertenarchive_folders. Fallback auf Legacy-Hardcoding (2Jahre/5Jahre/Archiv) wenn keinearchive_folderskonfiguriert. - Archive folder resolution:
resolveArchiveFolder(name)intool/agent.gosucht case-insensitiv inacc.ArchiveFolders(Name oder IMAPFolder), dann Legacy-Fallback. Gilt für Slash-Commands und@bot email move <name>. - IMAP IDLE:
email.IdleWatcherpro Account — Echtzeit-Benachrichtigung bei neuen Emails, kein Polling mehr. Race-sichere Implementierung mitatomic.Uint32fürnumMsgs. Automatischer Reconnect nach 60s bei Fehler. - Mehrere Email-Accounts:
config.AllEmailAccounts()gibt alle Accounts zurück — zuerstemail_accounts:(Liste), Fallback auf Legacyemail:Block. Alle Email-Funktionen iterieren über alle Accounts. /status: Ruftdiag.RunAll()auf + Task-Zähler — zeigt Echtzeit-Status aller externen Dienste
config.yml – Alle Felder
# 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.
# 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.
# 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)