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

268 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <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
```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 (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`)