8.8 KiB
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 startenonInteraction()— Slash-Command-Handler mit BerechtigungsprüfungonMessage()— @Mention-Handler inkl. PDF-Anhang-ErkennungrouteMessage()— Leitet @Mention-Text an passenden Agenten weiterstartDaemon()— Startet IMAP IDLE, RSS-Watcher, tägliche TimerdailyBriefing()— Morgen-Briefing (Tasks + Emails)nightlyIngest()— Archiv-Ordner in Qdrant importierenpatchEmailMoveChoices()— Discord-Choices dynamisch aus Config befüllenisAllowed(userID)— User-Berechtigungsprüfung
internal/config/
Konfigurationsstruktur (Config), Client-Factories und AllEmailAccounts().
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()— liestconfig.yml, validiert PflichtfelderAllEmailAccounts()— 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:
- Text nach Markdown-Überschriften (
#,##,###) aufteilen - Abschnitte > 800 Zeichen nach Paragraphen (
\n\n) aufteilen - 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:
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-ZusammenfassungtriageUnread()— LLM-Klassifikation + Qdrant-Lernen viatriagePackageCleanupArchiveFolders()— Alte Emails löschen nachretention_days
email/idle.go — IMAP IDLE-Watcher:
IdleWatcher— pro Account, race-sicher mitatomic.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 IntervallIngestAllFeeds()/FormatResults()— Batch-Import + Discord-Formatting
internal/triage/
Eigenes Package um Import-Zyklen zu vermeiden (brain ↔ email ↔ triage).
StoreTriage()— Triage-Entscheidung in Qdrant speichern (Typemail_triage)SearchSimilar()— Ähnliche frühere Entscheidungen finden (Score ≥ 0.7) als Few-Shot-Kontext
internal/diag/
RunAll()— Prüft Qdrant, LocalAI (Embedding + Chat), IMAP-VerbindungenFormat()/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.portembedding.url,embedding.modelchat.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 |