zwischenstand
This commit is contained in:
@@ -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 | 5–60s 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 |
|
||||
|
||||
Reference in New Issue
Block a user