Files
ai-agent/CLAUDE.md
2026-03-24 08:05:24 +01:00

15 KiB
Raw Blame History

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_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 <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 <source>.
  • 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 <text> 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 <name>.
  • 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

# 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 (560s 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)