zwischenstand

This commit is contained in:
Christoph K.
2026-03-20 23:24:56 +01:00
parent b1a576f61e
commit 905981cd1e
25 changed files with 3607 additions and 217 deletions

View File

@@ -1,60 +1,234 @@
# Architektur
```
AI_Brain/
*.md Dateien
bin/ingest Embeddings via LocalAI
Qdrant (NAS) ◄──── bin/ask ──► LM Studio (Chat)
```
- **Embeddings**: LocalAI unter `embedding.url` (Modell konfigurierbar)
- **Vektordatenbank**: Qdrant auf dem NAS
- **Chat-Completion**: LocalAI unter `chat.url` (Modell konfigurierbar)
## Projektstruktur
```
AI-Agent/
cmd/
ingest/main.go Entry Point für ingest-Binary
ask/main.go Entry Point für ask-Binary
internal/
config/config.go Config-Struct, Clients, Verbindungen
brain/
ingest.go Markdown-Import, Chunking
ingest_json.go JSON-Import (Bildbeschreibungen)
ask.go Suche + LLM-Antwort
bin/ Kompilierte Binaries (von build.sh erzeugt)
config.yml Alle Einstellungen
build.sh Baut beide Binaries
```
## Konfiguration
Alle Einstellungen in `config.yml` (muss im Arbeitsverzeichnis liegen):
```yaml
qdrant:
host: "192.168.1.4"
port: "6334"
api_key: "..."
collection: "jacek-brain"
embedding:
url: "http://192.168.1.118:8080/v1"
model: "qwen3-embedding-4b"
dimensions: 2560 # muss zum Modell passen
chat:
url: "http://192.168.1.118:8080/v1"
model: "qwen3.5-4b-claude-4.6-opus-reasoning-distilled"
brain_root: "/mnt/c/Users/jacek/AI_Brain"
top_k: 3
```
> **Wichtig:** Wenn du `embedding.model` oder `dimensions` änderst, muss die Qdrant-Collection neu erstellt werden (im Dashboard löschen, dann `ingest` erneut ausführen).
# 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 <frage>`
```
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 | 560s 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 |