From 18d2360ad96584195d433ea5d34d7e8ce2a0ee16 Mon Sep 17 00:00:00 2001 From: Florian Krebs <51504220+noheton@users.noreply.github.com> Date: Mon, 4 May 2026 20:49:45 +0000 Subject: [PATCH] initial commit --- nexa-core/.env.example | 25 + nexa-core/DEPLOYMENT.md | 629 ++++++++++++++++++ nexa-core/ai-prompts/system_prime.txt | 14 +- nexa-core/config/classification_logic.md | 19 + nexa-core/config/infra_manifest.yaml | 27 + nexa-core/config/workflows_spec.md | 16 + nexa-core/doc/command_system.md | 131 ++++ nexa-core/doc/graphrag_architecture.md | 273 ++++++++ nexa-core/doc/integration_matrix.md | 79 +++ nexa-core/doc/scope.md | 80 +++ .../phase-1/1_1_discovery_workflow.json | 127 ++++ .../phase-1/1_2_memos_bridge.json | 58 ++ .../phase-1/1_2_memos_bridge_v2.json | 143 ++++ .../phase-2/2_1_email_butler.json | 193 ++++++ 14 files changed, 1811 insertions(+), 3 deletions(-) create mode 100644 nexa-core/.env.example create mode 100644 nexa-core/DEPLOYMENT.md create mode 100644 nexa-core/config/classification_logic.md create mode 100644 nexa-core/config/infra_manifest.yaml create mode 100644 nexa-core/config/workflows_spec.md create mode 100644 nexa-core/doc/command_system.md create mode 100644 nexa-core/doc/graphrag_architecture.md create mode 100644 nexa-core/doc/integration_matrix.md create mode 100644 nexa-core/doc/scope.md create mode 100644 nexa-core/n8n-workflows/phase-1/1_1_discovery_workflow.json create mode 100644 nexa-core/n8n-workflows/phase-1/1_2_memos_bridge.json create mode 100644 nexa-core/n8n-workflows/phase-1/1_2_memos_bridge_v2.json create mode 100644 nexa-core/n8n-workflows/phase-2/2_1_email_butler.json diff --git a/nexa-core/.env.example b/nexa-core/.env.example new file mode 100644 index 0000000..e59fa37 --- /dev/null +++ b/nexa-core/.env.example @@ -0,0 +1,25 @@ +# NEXA - Minimalist .env Template +# Nur absolut essenzielle Secrets. Alles andere wird via #nexa:config kommandos konfiguriert & autodiscovered. + +# MEMOS (Primary Interface) +MEMOS_HOST=http://memos.local:5230 +MEMOS_API_KEY=your-access-token + +# SAIA / LiteLLM (Intelligence) +SAIA_HOST=https://saia.example.com +SAIA_API_KEY=your-litellm-api-key + +# NEXTCLOUD (Tasks, Mail, Calendar - alles wird autodiscovered) +NC_HOST=https://nextcloud.example.com +NC_USERNAME=nexa_automation +NC_APP_PASSWORD=your-app-password + +# QDRANT (Vector Memory - Semantic Search / Dense Retrieval) +QDRANT_HOST=http://qdrant.local:6333 +QDRANT_API_KEY=your-api-key + +# GRAPHDB (Ontotext - Semantic Graph / SPARQL) +GRAPHDB_HOST=http://graphdb.local:7200 +GRAPHDB_REPOSITORY=nexa_knowledge +GRAPHDB_USERNAME=nexa +GRAPHDB_PASSWORD=your-graphdb-password diff --git a/nexa-core/DEPLOYMENT.md b/nexa-core/DEPLOYMENT.md new file mode 100644 index 0000000..6a3b458 --- /dev/null +++ b/nexa-core/DEPLOYMENT.md @@ -0,0 +1,629 @@ +# NEXA Deployment Guide + +## 🚀 Architektur-Überblick + +``` +┌─────────────────────────────────────────────────┐ +│ MEMOS (Port 5230) │ +│ Interface, Voice-Input, Webhooks │ +└────────────────────┬────────────────────────────┘ + │ + ┌───────────┼───────────┐ + ↓ ↓ ↓ + ┌────────┐ ┌────────┐ ┌──────────┐ + │ n8n │ │ SAIA │ │ Nextcloud│ + │Logic │ │LiteLLM │ │Tasks/Mail│ + │7879 │ │4000 │ │80/443 │ + └────┬───┘ └────┬───┘ └─────┬────┘ + │ │ │ + ┌────┴───────────┼────────────┴────┐ + ↓ ↓ ↓ +┌─────────┐ ┌────────────┐ ┌──────────────┐ +│ QDRANT │ │ GraphDB │ │ Obsidian │ +│Vector │ │ (Ontotext) │ │(local sync) │ +│6333 │ │7200 │ │ │ +└─────────┘ └────────────┘ └──────────────┘ +``` + +--- + +## 📋 Voraussetzungen + +### Hardware +- **CPU**: Min. 4 Cores (empfohlen 8) +- **RAM**: Min. 16 GB (empfohlen 32 GB) +- **Storage**: Min. 100 GB SSD (für Datenbanken & Backups) +- **Network**: Stabile Verbindung, HTTPS-ready + +### Software +- Docker & Docker Compose (Version 20.10+) +- Git +- Bash +- curl/wget +- `.env` Datei aus `.env.example` (mit Secrets gefüllt) + +### Accounts & Credentials +- Nextcloud Instanz mit Admin-Zugang +- OpenAI API-Key (für SAIA/LiteLLM) +- Obsidian Vault Zugang lokalem Filesystem + +--- + +## 🐳 Schritt 1: Docker Compose Setup + +### 1.1 Repository klonen + +```bash +git clone https://github.com/noheton/nexa.git +cd nexa/nexa-core +``` + +### 1.2 .env konfigurieren + +```bash +cp .env.example .env +# WICHTIG: Alle Secrets eintragen! +# - MEMOS_API_KEY +# - SAIA_API_KEY +# - NC_APP_PASSWORD +# - QDRANT_API_KEY +# - GRAPHDB_PASSWORD +nano .env +``` + +### 1.3 docker-compose.yml erstellen + +```bash +cat > docker-compose.yml << 'EOF' +version: '3.8' + +services: + # ===== MEMOS (Interface) ===== + memos: + image: neosmemo/memos:latest + container_name: nexa-memos + ports: + - "5230:5230" + volumes: + - ./data/memos:/root/.memos + environment: + MEMOS_PORT: "5230" + MEMOS_DRIVER: sqlite + MEMOS_DSN: /root/.memos/memos.db + networks: + - nexa + restart: unless-stopped + + # ===== n8n (Workflow Engine) ===== + n8n: + image: n8nio/n8n:latest + container_name: nexa-n8n + ports: + - "5678:5678" + volumes: + - ./data/n8n:/home/node/.n8n + - ./n8n-workflows:/workflows + environment: + N8N_BASIC_AUTH_ACTIVE: "true" + N8N_BASIC_AUTH_USER: "${N8N_USER:-admin}" + N8N_BASIC_AUTH_PASSWORD: "${N8N_PASSWORD:-nexa_secure_pass}" + N8N_HOST: "${N8N_DOMAIN:-localhost}" + N8N_PORT: "5678" + N8N_PROTOCOL: https + N8N_WEBHOOK_URL: "https://${N8N_DOMAIN:-localhost}/webhook" + GENERIC_TIMEZONE: Europe/Berlin + DB_TYPE: sqlite + DB_SQLITE_PATH: /home/node/.n8n/nexa.db + networks: + - nexa + restart: unless-stopped + depends_on: + - memos + + # ===== QDRANT (Vector DB) ===== + qdrant: + image: qdrant/qdrant:latest + container_name: nexa-qdrant + ports: + - "6333:6333" + volumes: + - ./data/qdrant:/qdrant/storage + environment: + QDRANT_API_KEY: "${QDRANT_API_KEY}" + networks: + - nexa + restart: unless-stopped + + # ===== GraphDB (Ontotext SPARQL) ===== + graphdb: + image: ontotext/graphdb:10-ee + container_name: nexa-graphdb + ports: + - "7200:7200" + volumes: + - ./data/graphdb:/opt/graphdb/data + - ./config/graphdb-init.ttl:/graphdb-import/import.ttl + environment: + GDB_JAVA_OPTS: "-Xmx8g -Xms4g" + GDB_PASSWORD: "${GRAPHDB_PASSWORD}" + networks: + - nexa + restart: unless-stopped + + # ===== SAIA / LiteLLM Proxy (optional - wenn lokal) ===== + # (Falls SAIA extern gehostet: nicht nötig) + +networks: + nexa: + driver: bridge + +volumes: + nexa_memos: + nexa_n8n: + nexa_qdrant: + nexa_graphdb: +EOF +``` + +--- + +## 🔧 Schritt 2: Individuelle Service-Konfiguration + +### 2.1 MEMOS Initialisierung + +```bash +# Nach memos startup (ca. 30s) +curl -X POST http://localhost:5230/api/v1/auth/signup \ + -H "Content-Type: application/json" \ + -d '{ + "username": "nexa", + "password": "secure_password" + }' + +# API-Token generieren +TOKEN=$(curl -X POST http://localhost:5230/api/v1/auth/signin \ + -H "Content-Type: application/json" \ + -d '{"username":"nexa","password":"secure_password"}' \ + | jq -r '.data.token') + +echo "MEMOS_API_KEY=$TOKEN" >> .env +``` + +### 2.2 Qdrant Konfiguration + +```bash +# Warte auf Qdrant startup +sleep 10 + +# Erstelle Collection für Nexa +curl -X PUT "http://localhost:6333/collections/nexa_knowledge" \ + -H "Content-Type: application/json" \ + -H "api-key: ${QDRANT_API_KEY}" \ + -d '{ + "vectors": { + "size": 1536, + "distance": "Cosine" + }, + "optimizers_config": { + "deleted_threshold": 0.2, + "vacuum_threshold": 0.5, + "default_segment_number": 5, + "max_segment_number": 30, + "memmap_threshold": 268435456, + "indexing_threshold": 20000, + "flush_interval_sec": 10, + "max_optimization_threads": 4 + } + }' + +echo "✅ Qdrant Collection erstellt" +``` + +### 2.3 GraphDB Initialisierung + +```bash +# Warte auf GraphDB startup +sleep 15 + +# Erstelle Repository +GRAPHDB_URL="http://localhost:7200" + +curl -X POST "$GRAPHDB_URL/rest/repositories" \ + -H "Content-Type: application/json" \ + -u "admin:${GRAPHDB_PASSWORD}" \ + -d '{ + "id": "nexa_knowledge", + "title": "Nexa Knowledge Graph", + "type": "free", + "params": { + "baseURL": { + "label": "Base URL", + "value": "http://nexa.local/" + }, + "defaultLanguage": { + "label": "Default language", + "value": "en" + } + } + }' + +echo "✅ GraphDB Repository erstellt" +``` + +### 2.4 n8n Workflows Import + +```bash +# Warte auf n8n startup (ca. 60s) +sleep 60 + +# Importiere Discovery Workflow +curl -X POST http://localhost:5678/api/v1/workflows \ + -H "Content-Type: application/json" \ + -u "admin:${N8N_PASSWORD}" \ + -d @n8n-workflows/phase-1/1_1_discovery_workflow.json + +# Importiere Memos Bridge +curl -X POST http://localhost:5678/api/v1/workflows \ + -H "Content-Type: application/json" \ + -u "admin:${N8N_PASSWORD}" \ + -d @n8n-workflows/phase-1/1_2_memos_bridge_v2.json + +# Importiere Email Butler +curl -X POST http://localhost:5678/api/v1/workflows \ + -H "Content-Type: application/json" \ + -u "admin:${N8N_PASSWORD}" \ + -d @n8n-workflows/phase-2/2_1_email_butler.json + +echo "✅ n8n Workflows importiert" +``` + +--- + +## 🚀 Schritt 3: Deployment starten + +### 3.1 Docker Compose Up + +```bash +docker-compose up -d + +# Logs überwachen +docker-compose logs -f +``` + +### 3.2 Health Checks + +```bash +# Health Check Script +check_service() { + local name=$1 + local url=$2 + echo "Checking $name..." + if curl -s "$url" > /dev/null; then + echo "✅ $name: OK" + else + echo "❌ $name: FAILED" + fi +} + +sleep 30 + +check_service "Memos" "http://localhost:5230" +check_service "n8n" "http://localhost:5678" +check_service "Qdrant" "http://localhost:6333/health" +check_service "GraphDB" "http://localhost:7200/health" + +echo "" +echo "🎯 Service URLs:" +echo " - Memos: http://localhost:5230" +echo " - n8n: http://localhost:5678" +echo " - Qdrant: http://localhost:6333" +echo " - GraphDB: http://localhost:7200" +``` + +--- + +## ⚙️ Schritt 4: Konfiguration via Command System + +### 4.1 Discovery-Workflow triggern + +```bash +# In Memos Memo erstellen: +# #nexa:config + +# Oder via API: +curl -X POST http://localhost:5678/api/v1/workflows/run \ + -H "Content-Type: application/json" \ + -u "admin:${N8N_PASSWORD}" \ + -d '{ + "workflowId": "discovery-workflow-id" + }' +``` + +### 4.2 Automatische Konfiguration laden + +Nach erfolgreichem Discovery werden Nextcloud-Listen-IDs: + +```bash +# Es wird automatisch aktualisiert in: +# - Qdrant: als Metadata +# - n8n: als Umgebungsvariablen +# - Memos: als Confirmation-Memo +``` + +--- + +## 🔐 Schritt 5: Reverse Proxy Setup (Production) + +### 5.1 Nginx Configuration + +```bash +cat > /etc/nginx/sites-available/nexa.conf << 'EOF' +upstream memos_backend { + server localhost:5230; +} + +upstream n8n_backend { + server localhost:5678; +} + +upstream qdrant_backend { + server localhost:6333; +} + +upstream graphdb_backend { + server localhost:7200; +} + +server { + listen 443 ssl http2; + server_name nexa.yourdomain.com; + + ssl_certificate /etc/letsencrypt/live/nexa.yourdomain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/nexa.yourdomain.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + # MEMOS + location / { + proxy_pass http://memos_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # n8n + location /n8n/ { + proxy_pass http://n8n_backend/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Qdrant (nur intern, wenn nötig) + location /qdrant/ { + allow 10.0.0.0/8; + deny all; + proxy_pass http://qdrant_backend/; + } + + # GraphDB (nur intern) + location /graphdb/ { + allow 10.0.0.0/8; + deny all; + proxy_pass http://graphdb_backend/; + } + + # Webhook endpoint für externe Services + location /webhook/ { + proxy_pass http://n8n_backend/webhook/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} + +server { + listen 80; + server_name nexa.yourdomain.com; + return 301 https://$server_name$request_uri; +} +EOF + +# Aktivieren +sudo ln -s /etc/nginx/sites-available/nexa.conf /etc/nginx/sites-enabled/ +sudo nginx -t +sudo systemctl reload nginx +``` + +--- + +## 📊 Schritt 6: Monitoring & Backups + +### 6.1 Backup Script + +```bash +cat > scripts/backup_nexa.sh << 'EOF' +#!/bin/bash + +BACKUP_DIR="./backups/$(date +%Y%m%d_%H%M%S)" +mkdir -p "$BACKUP_DIR" + +echo "🔄 Backing up Nexa..." + +# Backup Datenbanken +docker-compose exec -T qdrant tar czf - /qdrant/storage > "$BACKUP_DIR/qdrant.tar.gz" +docker-compose exec -T graphdb tar czf - /opt/graphdb/data > "$BACKUP_DIR/graphdb.tar.gz" +docker-compose exec -T memos tar czf - /root/.memos > "$BACKUP_DIR/memos.tar.gz" +docker-compose exec -T n8n tar czf - /home/node/.n8n > "$BACKUP_DIR/n8n.tar.gz" + +# Backup Configs +cp .env "$BACKUP_DIR/.env.backup" +cp docker-compose.yml "$BACKUP_DIR/docker-compose.yml.backup" + +echo "✅ Backup completed: $BACKUP_DIR" + +# Optional: zu S3 hochladen +# aws s3 sync "$BACKUP_DIR" s3://nexa-backups/$(date +%Y%m%d)/ +EOF + +chmod +x scripts/backup_nexa.sh + +# Tägliches Backup (cron) +echo "0 2 * * * /path/to/nexa/scripts/backup_nexa.sh" | crontab - +``` + +### 6.2 Health Check Monitoring + +```bash +cat > scripts/monitor_nexa.sh << 'EOF' +#!/bin/bash + +# Monitore Service Status +SERVICES=("memos" "n8n" "qdrant" "graphdb") + +for service in "${SERVICES[@]}"; do + if docker-compose ps $service | grep -q "Up"; then + echo "✅ $service: Running" + else + echo "❌ $service: DOWN" + # Restart bei Fehler + docker-compose restart $service + fi +done +EOF + +chmod +x scripts/monitor_nexa.sh + +# Alle 5 Minuten +echo "*/5 * * * * /path/to/nexa/scripts/monitor_nexa.sh" | crontab - +``` + +--- + +## 🧪 Schritt 7: Validation & Testing + +### 7.1 End-to-End Test + +```bash +cat > scripts/test_nexa.sh << 'EOF' +#!/bin/bash + +echo "🧪 Testing Nexa Installation..." + +# Test 1: Memos API +echo "Test 1: Memos API" +if curl -s http://localhost:5230/api/v1/auth/status | jq . > /dev/null 2>&1; then + echo "✅ Memos: OK" +else + echo "❌ Memos: FAILED" +fi + +# Test 2: n8n API +echo "Test 2: n8n API" +if curl -s -u "admin:${N8N_PASSWORD}" http://localhost:5678/api/v1/workflows | jq . > /dev/null 2>&1; then + echo "✅ n8n: OK" +else + echo "❌ n8n: FAILED" +fi + +# Test 3: Qdrant Collection +echo "Test 3: Qdrant Collection" +if curl -s -H "api-key: ${QDRANT_API_KEY}" http://localhost:6333/collections/nexa_knowledge | jq . > /dev/null 2>&1; then + echo "✅ Qdrant: OK" +else + echo "❌ Qdrant: FAILED" +fi + +# Test 4: GraphDB Repository +echo "Test 4: GraphDB Repository" +if curl -s -u "admin:${GRAPHDB_PASSWORD}" http://localhost:7200/repositories | jq . > /dev/null 2>&1; then + echo "✅ GraphDB: OK" +else + echo "❌ GraphDB: FAILED" +fi + +echo "" +echo "🎯 Create Test Memo:" +MEMO=$(curl -s -X POST http://localhost:5230/api/v1/memos \ + -H "Authorization: Bearer ${MEMOS_API_KEY}" \ + -H "Content-Type: application/json" \ + -d '{"content": "Test memo - #nexa:config"}' | jq -r '.id') + +echo "✅ Test Memo created: $MEMO" +echo "Check Memos UI to verify webhook triggers Discovery" +EOF + +chmod +x scripts/test_nexa.sh +./scripts/test_nexa.sh +``` + +--- + +## 🆘 Troubleshooting + +### Problem: Memos kann sich nicht mit n8n verbinden + +```bash +# Überprüfe n8n Webhook URL in Memos config +curl http://localhost:5678/webhook/memos + +# Falls 404: Workflow ist nicht aktiviert/deployed +docker-compose logs n8n | grep webhook +``` + +### Problem: GraphDB startet nicht + +```bash +# Memory-Limit erhöhen +docker-compose exec graphdb -e "GDB_JAVA_OPTS=-Xmx16g -Xms8g" restart + +# Logs checken +docker-compose logs graphdb +``` + +### Problem: Qdrant Collection leer + +```bash +# Überprüfe ob Memos Bridge Workflow läuft +docker-compose logs n8n | grep "memos_bridge" + +# Manuell test +curl -X POST http://localhost:5230/api/v1/memos \ + -H "Authorization: Bearer ${MEMOS_API_KEY}" \ + -d '{"content": "- [ ] Test task #nexa"}' +``` + +--- + +## 📈 Next Steps + +1. ✅ **Phase 1 deployt**: Memos Bridge & Discovery laufen +2. ⏳ **Phase 2 aktivieren**: Email Butler (wenn Nextcloud Mail konfiguriert) +3. ⏳ **Phase 3 setup**: Qdrant Embedding Pipeline +4. ⏳ **Phase 4 integration**: GraphRAG Queries testen +5. ⏳ **Phase 5**: Home Assistant Voice Integration + +--- + +## 📞 Support & Debugging + +```bash +# Alle Logs in eine Datei sammeln +docker-compose logs > deployment_logs.txt + +# Spezifischer Service +docker-compose logs n8n --tail 100 + +# Wiederherstellung aus Backup +./scripts/restore_nexa.sh backup/2024-01-15_120000/ +``` + +--- + +**Deployment abgeschlossen! Nexa ist nun betriebsbereit.** 🚀 diff --git a/nexa-core/ai-prompts/system_prime.txt b/nexa-core/ai-prompts/system_prime.txt index 8e1ada2..703609d 100644 --- a/nexa-core/ai-prompts/system_prime.txt +++ b/nexa-core/ai-prompts/system_prime.txt @@ -1,5 +1,13 @@ Name: Nexa Rolle: Zentrales Nervensystem -Eigenschaften: Präzise, proaktiv, leicht trocken-humorvoll. -Aufgabe: Überwacht 20 E-Mails/Tag, kuratiert Memos, schlägt Timeboxing für Nextcloud vor und verwaltet das Wissen in Qdrant. -Instruktion: Wenn der User eine Notiz in Memos mit "Nexa:" beginnt, analysiere den Kontext und antworte kurz und bündig. +Aufgabe: Überwacht E-Mails, kuratiert Wissen und plant Tasks. + +WICHTIGE LOGIK: +- Tasks müssen strikt zwischen "Work" und "Personal" unterschieden werden. +- Analysiere den Inhalt eines Memos: Bezieht es sich auf professionelle Projekte (Obsidian Context) -> Work List. Bezieht es sich auf Haushalt, Einkäufe oder Freizeit -> Personal List. +- Im Zweifel: Nachfragen via Memos-Kommentar. + +KONSUM-LOGIK: +- Wenn ein Link von Karakeep kommt, analysiere das Produkt. +- Haushaltswaren/Verbrauchsgüter -> NC_LIST_ID_SHOPPING. +- Besondere Wünsche/Hobby/Technik -> NC_LIST_ID_WISHES. diff --git a/nexa-core/config/classification_logic.md b/nexa-core/config/classification_logic.md new file mode 100644 index 0000000..3e8321c --- /dev/null +++ b/nexa-core/config/classification_logic.md @@ -0,0 +1,19 @@ +# Nexa Klassifizierungs-Logik + +Dieser Fragebogen dient der Feinabstimmung von SAIA, um Tasks korrekt zu routen. + +### A. Schlüsselwörter & Projekte (Arbeit) +*Welche Begriffe triggern zwingend die Work-Liste?* +- [ ] Projekt-Namen (z.B. "Nexa-Core", "Infrastruktur-Audit") +- [ ] Rollenspezifische Begriffe ("Meeting", "Report", "Deadline") +- [ ] Tools, die nur im Job vorkommen. + +### B. Ausschlusskriterien (Privat) +*Was darf niemals in die Work-Liste?* +- [ ] Lebensmittel, Rezepte, Haushalt. +- [ ] Bluesky-Input (sofern nicht explizit als Recherche markiert). +- [ ] Finanz-Mails (Privat-Bank). + +### C. Umgang mit Unschärfe +- **Default:** Wenn < 70% Sicherheit -> `Personal`. +- **Interaktion:** Nexa erstellt einen Kommentar: "In welche Liste soll das? [W/P]". \ No newline at end of file diff --git a/nexa-core/config/infra_manifest.yaml b/nexa-core/config/infra_manifest.yaml new file mode 100644 index 0000000..a85d7d4 --- /dev/null +++ b/nexa-core/config/infra_manifest.yaml @@ -0,0 +1,27 @@ +# NEXA Infrastructure Manifest +services: + memos: + url: "${MEMOS_URL}" + auth_type: "Bearer Token" + features: ["webhooks", "comments_api"] + + n8n: + environment: "LXC Docker" + backup_path: "./n8n-workflows" + + ai_gateway: + provider: "LiteLLM / SAIA" + model_primary: "gpt-4-o" # oder dein lokales Modell + model_embeddings: "text-embedding-3-small" + + storage: + qdrant: "http://localhost:6333" + s3_provider: "Custom S3" + nextcloud: + url: "${NC_URL}" + lists: + work: "ID_COMING_SOON" + personal: "ID_COMING_SOON" + calendars: + work: "CAL_ID_WORK" + personal: "CAL_ID_PERSONAL" \ No newline at end of file diff --git a/nexa-core/config/workflows_spec.md b/nexa-core/config/workflows_spec.md new file mode 100644 index 0000000..17b1b26 --- /dev/null +++ b/nexa-core/config/workflows_spec.md @@ -0,0 +1,16 @@ +# Spezifikation: Phase 2 - Task-Router + +## 1. Trigger +* **Typ:** Webhook (Memos) +* **Filter:** `payload.content` enthält `- [ ]` ODER `#todo`. + +## 2. Intelligence Node (SAIA) +* **Prompt:** "Analysiere folgende Notiz. Gib als JSON zurück: { "context": "work" | "personal", "summary": "string", "urgency": 1-5 }." +* **Kontext-Zufuhr:** Lade die letzten 5 Obsidian-Projekttitel aus Qdrant als Referenz hoch. + +## 3. Switch Node +* **Route 1:** Context == "work" -> Nextcloud Create Task (List ID: Work). +* **Route 2:** Context == "personal" -> Nextcloud Create Task (List ID: Personal). + +## 4. Feedback Loop +* Poste die Task-ID und den gewählten Kontext als Kommentar unter das Original-Memo. \ No newline at end of file diff --git a/nexa-core/doc/command_system.md b/nexa-core/doc/command_system.md new file mode 100644 index 0000000..d497fc9 --- /dev/null +++ b/nexa-core/doc/command_system.md @@ -0,0 +1,131 @@ +# Nexa Command System - #nexa:command + +Nexa "hört" auf folgende Kommandos in Memos-Kommentaren oder als Memo-Inhalt mit Hashtag: + +## 🔧 Konfigurations-Kommandos + +### #nexa:config +- **Beschreibung**: Triggert Autodiscovery aller Services (Nextcloud, Qdrant, etc.) +- **Antwort**: Postet Report mit gefundenen Konfigurationen +- **Speichert**: Settings in DynoDB / Qdrant Metadata (nicht in .env) + +``` +#nexa:config +``` + +**Beispiel-Response:** +``` +✅ Autodiscovery abgeschlossen: +- Nextcloud Listen: Work (id=123), Personal (id=456), Shopping (id=789), Wishes (id=1011) +- Qdrant Collection: nexa_knowledge (1536 dims, Cosine) +- Nextcloud Mail: 3 Accounts erkannt (Office 365, Gmail, Privat) +``` + +### #nexa:status +- **Beschreibung**: Zeigt den aktuellen System-Status +- **Antwort**: Uptime, verbundene Systeme, Memory-Stats + +``` +#nexa:status +``` + +### #nexa:sync-obsidian +- **Beschreibung**: Triggert sofortige Synchronisierung mit Obsidian Vault +- **Optional Parameter**: `--vault=/path/to/vault` oder `--force` für Neuindexierung + +``` +#nexa:sync-obsidian --force +``` + +## 📊 Memory & RAG Kommandos + +### #nexa:ask [question] +- **Beschreibung**: Semantische Suche in Qdrant (RAG) basierend auf Obsidian + Memos +- **Antwort**: Top-3 Treffer mit Quellen + +``` +#nexa:ask Wie implementierten wir das JWT-Middleware-Pattern? +``` + +### #nexa:learn [topic] +- **Beschreibung**: Explizit neue Information in Qdrant speichern (mit Tags) +- **Optional**: `--tag=architecture` `--source=obsidian/notes/arch.md` + +``` +#nexa:learn Das Routing-Schema unterscheidet Work vs. Personal via SAIA-Kontext-Analyse --tag=architecture +``` + +## 🎯 Workflow-Kommandos + +### #nexa:route-test [text] +- **Beschreibung**: Testet die Klassifizierung (Work/Personal) ohne Task zu erstellen + +``` +#nexa:route-test Muss morgen die Präsentation für den Client fertigstellen +``` + +**Response:** +``` +💬 Klassifizierung (Test-Mode): +- Kontext: WORK +- Vertrauen: 0.95 +- Begründung: "Client-Präsentation → professioneller Kontext" +``` + +### #nexa:email-digest +- **Beschreibung**: Triggert sofortige Email-Zusammenfassung (sonst tägl. 7 Uhr) + +``` +#nexa:email-digest +``` + +## 🔐 Admin-Kommandos + +### #nexa:reset-config +- **Beschreibung**: Löscht gespeicherte Konfiguration, triggert neuen Autodiscovery +- **Warnung**: Alle benutzerdefinierten Settings werden vergessen + +``` +#nexa:reset-config +``` + +### #nexa:export-state +- **Beschreibung**: Exportiert aktuellen State als JSON (für Backups) + +``` +#nexa:export-state +``` + +--- + +## 📝 Implementierung in n8n + +Ein **Command Parser** läuft immer mit: + +1. **Memos Webhook** empfängt alle Memos +2. **Regex Check**: Sucht nach `#nexa:command` +3. **Router**: Versendet an entsprechenden n8n-Workflow +4. **Antwort**: Postet Reply als Kommentar/Edit + +**Command Parser Regex:** +``` +^#nexa:(\w+)(?:\s+([^\n]*?))?(?:$|\s*--) +``` + +Extrahiert: `[command, parameters]` + +--- + +## 🎛️ Dynamische Konfigurationsspeicherung + +Statt .env zu editieren: + +1. **First Run**: `#nexa:config` speichert zu lokalen Metadata +2. **Speich-Ziel**: + - **Primary**: Qdrant Metadata (als `_config` Namespace) + - **Fallback**: `nexa-core/config/runtime_config.json` (gitignored) + - **Notfall**: .env (nur initial) + +3. **Reload-Logik**: Bei jedem Workflow-Start werden Settings aus Qdrant geladen + +Das macht Nexa vollständig selbstständig nach dem initialem Setup! diff --git a/nexa-core/doc/graphrag_architecture.md b/nexa-core/doc/graphrag_architecture.md new file mode 100644 index 0000000..40e4184 --- /dev/null +++ b/nexa-core/doc/graphrag_architecture.md @@ -0,0 +1,273 @@ +# GraphRAG für Nexa - Strukturelles Wissen & Beziehungen + +## 🧠 Zwei-Säulen-Memory-Architektur + +Nexa nutzt zwei komplementäre Speicher für vollständiges Verständnis: + +### Säule 1: Qdrant (Vector DB) - Semantische Ähnlichkeit +- **Frage**: "Was ist ähnlich, was ist relevant?" +- **Speichert**: Embeddings von Memos, Obsidian-Notes, E-Mail-Digests +- **Suche**: Cosine Similarity über 1536-dimensionale Vektoren +- **Antwort**: Top-k semantisch ähnliche Dokumente (RAG) + +**Beispiel:** +``` +Frage: "Wie war die JWT-Implementation in unserem letzten Projekt?" +→ Qdrant findet: 10 ähnliche Snippets → Prompt-Context +``` + +### Säule 2: Neo4j (Graph DB) - Strukturelle Beziehungen +- **Frage**: "Wer ist beteiligt? Was hängt davon ab? Wie ist es organisiert?" +- **Speichert**: + - **Knoten (Nodes)**: Projects, Tasks, People, Technologies, Dates + - **Kanten (Relationships)**: `USES`, `OWNS`, `DEPENDS_ON`, `MENTIONS`, `CREATED_BY` +- **Suche**: Cypher-Queries über Netzwerk-Topologie +- **Antwort**: Pfade, zentrale Knoten, Abhängigkeitsanalyse + +**Beispiel:** +```cypher +MATCH (tech:Technology)-[USES*]->*(service:Service), + (tech)-[:MENTIONED_IN]->(task:Task) +WHERE tech.name = "JWT" AND task.status = "DONE" +RETURN tech, service, task +``` + +--- + +## 📊 Neo4j Schema für Nexa + +### Node-Typen + +```cypher +(Project {name, startDate, status, priority}) +(Task {title, description, status, urgency: 1-5, context: "work"|"personal"}) +(Person {name, role, email}) +(Technology {name, category: "framework"|"language"|"tool"}) +(Topic {name, domain}) +(File {path, type, lastModified}) +(Note {id, content_hash, created_at}) +``` + +### Relationship-Typen + +| Beziehung | Von | Zu | Beschreibung | +|-----------|-----|-----------|-------------| +| `OWNS` | Person | Task/Project | Wer ist verantwortlich | +| `USES` | Task/Project | Technology | Welche Tech wird verwendet | +| `DEPENDS_ON` | Task/Project | Task/Project | Hängt davon ab | +| `CREATED_BY` | Note | Person | Autor einer Notiz | +| `MENTIONS` | Note/Task | Topic/Technology | Verweist auf | +| `RELATED_TO` | Task | Task | Zusammenhängend | +| `CHILD_OF` | Task | Project | Task gehört zu Project | +| `SCHEDULED_FOR` | Task | Date | Zeitliche Zuordnung | + +--- + +## 🔄 Synchronisierungs-Flows + +### 1. **Memos → Neo4j** (automatisch beim Erstellen) + +Wenn ein Memo mit `- [ ]` erstellt wird: + +``` +Memo-Content: "Muss JWT-Middleware für Auth-Service refaktorieren" + ↓ +SAIA extrahiert Entitäten: + - Technologie: "JWT-Middleware", "Auth-Service" + - Kontext: "work" + - Urgenz: 3 + ↓ +Erstelle in Neo4j: + - Task node (title="JWT-Middleware refaktorieren") + - Technology nodes: JWT, Auth-Service + - Relationships: Task -[USES]-> JWT, Task -[USES]-> Auth-Service + - Task -[CREATED_BY]-> Me (Person node) +``` + +### 2. **Obsidian → Neo4j** (#nexa:sync-obsidian) + +Beim Sync von Obsidian-Vault: + +``` +File: obsidian/projects/nexa-deployment.md + ↓ +Header Struktur wird zu Graph: + - Project "Nexa Deployment" + - Technologies: Docker, n8n, Nextcloud + - Dependencies: "requires infrastructure setup" + ↓ +Erstelle: + - Project nodes mit Metadata + - Hierarchie: Project -[CONTAINS]-> Task -[USES]-> Technology +``` + +### 3. **Nextcloud Tasks → Neo4j** (bidirektional) + +Wenn Task in Nextcloud erstellt/gelöcht: + +``` +Nextcloud Task: "Review JWT implementation" (in Work-List) + ↓ +n8n Trigger erkennt Änderung + ↓ +Update Neo4j: + - Task node status = "active" + - Task -[DEPENDS_ON]-> related Task (falls Beziehung existiert) +``` + +--- + +## 🤖 GraphRAG Queries - Praktische Anwendungen + +### Query 1: "Zeige alle offenen Tasks, die JWT betreffen" + +```cypher +MATCH (tech:Technology {name:"JWT"}), + (tech)-[:MENTIONED_IN|USES]-(task:Task), + (task)-[:CHILD_OF]->(project:Project) +WHERE task.status IN ["NEEDS-ACTION", "IN-PROGRESS"] +RETURN project.name, task.title, task.urgency +ORDER BY task.urgency DESC +``` + +### Query 2: "Von welchen Tasks/Projects hängt Auth-Service ab?" + +```cypher +MATCH path = (dependent)-[:DEPENDS_ON*]->(service:Technology {name:"Auth-Service"}) +RETURN [x IN nodes(path) | x.name], length(path) +ORDER BY length(path) ASC +``` + +### Query 3: "Wer arbeitet an diesem Projekt und auf welchen Technologien?" + +```cypher +MATCH (person:Person)-[:OWNS]->(task:Task)-[:CHILD_OF]->(project:Project {name:"Nexa"}), + (task)-[:USES]->(tech:Technology) +RETURN person.name, collect(task.title), collect(tech.name) +``` + +### Query 4: "Was ist seit gestern neu an Technologien/Themen gelernt?" + +```cypher +MATCH (note:Note)-[:MENTIONS]->(topic:Topic) +WHERE note.created_at > datetime.now() - duration('P1D') +RETURN DISTINCT topic.name, count(note) as mentions +ORDER BY mentions DESC +``` + +--- + +## 🔗 GraphRAG-Enhanced Answer Generation + +Wenn Nutzer `#nexa:ask` fragt: + +``` +#nexa:ask Wie stehen die JWT-Tasks zur Deadline? + ↓ + ┌─────────────┴──────────────┐ + ↓ ↓ + [Qdrant] [Neo4j] + Semantic Structural + Search Search + ↓ ↓ + Top-3 Notes (JWT)-[DEPENDS_ON] + mit ähnlichem → Connected Tasks + Kontext → Critical Path + ↓ ↓ + Combine: Merge + Rank + ↓ + System Prompt (SAIA): + "Basierend auf diesem Wissen und diesen Abhängigkeiten, beantworte:" + ↓ + Response mit: + - Semantisch relevanter Information (Qdrant) + - Strukturellem Kontext (Neo4j) + - Abhängigkeitsanalyse +``` + +--- + +## 🛠️ n8n Integration (Phase 3) + +### Workflow: Graph-Sync Trigger + +``` +[Memos Webhook] + ↓ +[Parse Content] + ↓ +[SAIA: Extract Entities] + ├→ Tasks, Projects, Technologies, People + └→ Relationships (uses, depends_on, mentions) + ↓ +[Neo4j: Create/Update Nodes & Relationships] + ↓ +[Index in Qdrant Metadata: Add graph_node_id] +``` + +### Workflow: Question Router + +``` +[#nexa:ask Query] + ↓ +[Parallel Search] + ├→ [Qdrant: Vector Similarity] + ├→ [Neo4j: Graph Query (auto-generated Cypher)] + └→ [Rank & Merge Results] + ↓ +[SAIA: Generate Answer with Context] +``` + +--- + +## 📋 Commands für Graph-Management + +### #nexa:graph-status +Zeigt Statistiken der Graph-DB +``` +#nexa:graph-status +``` + +Response: +``` +📊 Neo4j Graph Status: +- Nodes: 342 (Tasks: 145, Projects: 12, Technologies: 52, Notes: 133) +- Relationships: 1,247 +- Density: 0.34 +- Most Connected: "JWT-Middleware" (27 connections) +``` + +### #nexa:graph-trace [entity] +Zeigt alle Verbindungen für eine Entität +``` +#nexa:graph-trace JWT-Middleware +``` + +### #nexa:graph-rebuild +Rekonstruiert Graph aus Obsidian + Memos (einmalig, danach inkrementell) +``` +#nexa:graph-rebuild --force +``` + +--- + +## 🚀 Phase 3 Roadmap (Graph Integration) + +- [ ] **3.1 Neo4j Setup**: Collection, Constraints, Indexes +- [ ] **3.2 Graph-Sync Workflow**: Automatische Entity Extraction +- [ ] **3.3 GraphRAG Pipeline**: Kombinierte Qdrant + Neo4j Queries +- [ ] **3.4 Dependency Tracker**: Warnt vor zirkulären Dependencies +- [ ] **3.5 Critical Path Analysis**: Berechnet längsten Weg im Graph + +--- + +## 💡 Warum zwei DBs? + +| Szenario | Qdrant | Neo4j | Kombi | +|----------|--------|-------|-------| +| "Welche Note war ähnlich?" | ✅ Best | ❌ | Qdrant | +| "Welche Tasks blockieren diesen Task?" | ❌ | ✅ Best | Neo4j | +| "Erkläre mir dieses Projekt umfassend" | ✅ Context | ✅ Structure | **Beide!** | +| "Finde alle JWT-bezogenen offenen Work" | ✅ Semantic | ✅ Crisp | **Beide!** | + +Kombiniert: **Vollständiges Verständnis** statt nur Suchindex oder nur Struktur. diff --git a/nexa-core/doc/integration_matrix.md b/nexa-core/doc/integration_matrix.md new file mode 100644 index 0000000..dfc7da7 --- /dev/null +++ b/nexa-core/doc/integration_matrix.md @@ -0,0 +1,79 @@ +# 🔗 Nexa Integrations-Matrix (V2) + +Diese Matrix definiert die logische Trennung zwischen privaten und beruflichen Datenströmen sowie die Anbindung der Infrastruktur. + +--- + +## 1. Die Dualität: Arbeit vs. Privat +Nexa muss strikt zwischen zwei Kontexten unterscheiden, da die Datenquellen variieren: + +### A. Bereich: ARBEIT (Work) +* **Sichtbarkeit:** Eingeschränkt (kein Zugriff auf Arbeits-E-Mails oder Firmen-Server). +* **Datenquellen:** + * Separate Nextcloud Task-Liste (`Work_Tasks`). + * Separater Nextcloud Kalender (`Work_Calendar`). + * Memos mit Tag `#work` oder semantischer Erkennung. +* **Nexa-Fokus:** Timeblocking, Fokus-Zeiten, Vorbereitung von Meetings basierend auf Obsidian-Notizen. + +### B. Bereich: PRIVAT (Personal) +* **Sichtbarkeit:** Vollständig. +* **Datenquellen:** + * Nextcloud Task-Liste (`Personal_Tasks`). + * Haupt-Kalender. + * E-Mails (IMAP - 20 Mails/Tag). + * Bluesky, RSS, Karakeep. +* **Nexa-Fokus:** Automatisierung des Alltags, Kuratierung von Wissen, E-Mail-Management. + +### Karakeep & Shopping-Logik +* **Schnittstelle:** Karakeep API / RSS. +* **Listen-Routing:** + * **Einkaufsliste:** Automatischer Sync für Items mit hoher Frequenz (Lebensmittel, Drogerie). + * **Wunschliste:** Speicherort für "Entdeckungen". Nexa fügt bei Wunschlisten-Items automatisch den aktuellen Preis und eine kurze SAIA-Zusammenfassung hinzu ("Warum du das speichern wolltest"). +* **Review-Cycle:** Einmal im Monat fragt Nexa bei Wunschlisten-Items nach: "Immer noch interessiert oder kann das weg?" + +--- + +## 2. Kern-Infrastruktur (The Brain & Spine) + +| Komponente | Rolle im System | +| :--- | :--- | +| **Memos** | Zentraler Input (Drafts, Ideen, schnelle Tasks). Schnittstelle für Nexa-Antworten. | +| **n8n** | Logik-Engine. Führt die Klassifizierung Arbeit vs. Privat durch. | +| **SAIA (LiteLLM)** | Entscheidet anhand des Inhalts, in welchen Kalender/Liste ein Eintrag gehört. | +| **Qdrant** | Langzeitgedächtnis. Speichert Projektwissen (Work) und privates Wissen getrennt. | +| **Arcane** | Verwaltung der Docker-Container (n8n, Qdrant, Memos). | + +--- + +## 3. Spezifische Datenflüsse & Logik + +### Task-Routing (Das Gehirn-Filter) +1. **Input:** Neues Memo oder Spracheingabe. +2. **Analyse:** SAIA prüft: "Ist das Business oder Privat?" +3. **Routing:** * `Arbeits-Kontext` -> Eintrag in `Work_Tasks`. + * `Privat-Kontext` -> Eintrag in `Personal_Tasks`. +4. **Timeboxing:** Nexa scannt den `Work_Calendar` auf Lücken und schlägt Slots für `Work_Tasks` vor. + +### E-Mail Management (Nur Privat) +* Nexa filtert die 20 täglichen Privat-Mails. +* Wichtige private Termine werden in den privaten Kalender extrahiert. +* Newsletter landen als Zusammenfassung in Memos. + +### Wissens-Management (Obsidian & Qdrant) +* Obsidian-Notizen (Arbeit & Privat) werden in Qdrant indiziert. +* Nexa nutzt dieses Wissen, um Arbeits-Tasks besser zu beschreiben, auch wenn sie keinen Zugriff auf die Arbeits-E-Mails hat. + +--- + +## 4. Monitoring & Hardware (Home Assistant) + +| Dienst | Reiz-Typ | Nexa-Reaktion | +| :--- | :--- | :--- | +| **Home Assistant** | Voice / Sensorik | Nexa nimmt Sprachbefehle entgegen und meldet Alarme (Haus). | +| **Proxmox** | Stabilität | Meldung an Nexa bei Hardware-Problemen. | +| **S3 Server** | Archiv | Langzeit-Backups der Wissensdatenbank. | + +--- + +## 5. Ergänzungen für das Setup (Vorschlag) +* **Zwei-Faktor-Klassifizierung:** Einführung von expliziten Tags (`#w` für Work, `#p` für Privat) für Grenzfälle, in denen SAIA den Kontext nicht eindeutig bestimmen kann. \ No newline at end of file diff --git a/nexa-core/doc/scope.md b/nexa-core/doc/scope.md new file mode 100644 index 0000000..925ad72 --- /dev/null +++ b/nexa-core/doc/scope.md @@ -0,0 +1,80 @@ +📑 Projekt Nexa: Scoping & Vision +1. Vision & Zielsetzung +Nexa ist als "Zentrales Nervensystem" konzipiert. Das Ziel ist die Schaffung einer intelligenten Middleware zwischen Input-Quellen (Memos, E-Mail, RSS), Wissensspeichern (Obsidian, Qdrant) und Organisations-Tools (Nextcloud Calendar/Tasks). + +Kernziele: +Kognitive Entlastung: Nexa sortiert, filtert und schlägt vor, anstatt nur zu speichern. + +Kontext-Trennung: Saubere, KI-gestützte Unterscheidung zwischen Privat und Arbeit. + +Single Point of Interaction: Memos fungiert als primäre Schnittstelle ("The Voice & Ear"). + +Wissens-Synergie: Verknüpfung von flüchtigen Memos mit tiefem Wissen in Obsidian via semantischer Suche. + +2. Abgrenzung (Scope) +Um die Komplexität beherrschbar zu halten, wird das Projekt wie folgt abgegrenzt: + +In-Scope (Was Nexa tut): +Triaging: Klassifizierung von 20 Mails/Tag (Privat). + +Task-Routing: Verteilung von Aufgaben auf die richtige Nextcloud-Liste (Work vs. Personal). + +Scheduling: Vorschlagen von Timeboxen im Arbeitskalender. + +Memory: Aufbau eines Langzeitgedächtnisses in Qdrant. + +Monitoring: Aggregation kritischer IT-Alarme (Proxmox, Backrest). + +Out-of-Scope (Was Nexa NICHT tut): +Kein direkter Zugriff auf Arbeits-IT (E-Mail/Server). + +Keine aktive Bearbeitung von Dokumenten (außer Metadaten-Extraktion). + +Keine Ersetzung von Spezial-UIs (z.B. Arcane oder Portainer für Container-Management). + +3. Phasenmodell der Entwicklung +Der Aufbau erfolgt iterativ, um frühzeitig Mehrwert zu generieren. + +Phase 1: Die Wirbelsäule (Konnektivität) +Fokus: Datenfluss zwischen Memos, n8n und SAIA. + +Meilenstein: Nexa antwortet auf Memos und kann einfache Fragen beantworten. + +Technik: Webhooks, REST-API, LiteLLM-Proxy. + +Phase 2: Sensorik & Klassifizierung (The Senses) +Fokus: E-Mail-Filterung und das Routing-System. + +Meilenstein: Nexa erkennt den Unterschied zwischen "Arbeits-Task" und "Privat-Task" ohne manuelle Tags. + +Technik: IMAP-Node, n8n-Logic-Router, System-Prompts für Kontext-Analyse. + +Phase 3: Das semantische Gedächtnis (The Brain) +Fokus: Anbindung von Qdrant und Obsidian. + +Meilenstein: RAG (Retrieval Augmented Generation). Nexa beantwortet Fragen basierend auf deinen archivierten Notizen. + +Technik: Embeddings-Pipeline, Qdrant-Vektor-Suche. + +Phase 4: Proaktive Motorik (The Assistant) +Fokus: Kalender-Integration und Timeboxing. + +Meilenstein: Nexa schlägt morgens proaktiv Fokus-Zeiten im Arbeitskalender vor. + +Technik: Nextcloud CalDAV/CardDAV Integration, Optimierungs-Logik für freie Slots. + +Phase 5: Physische Präsenz (Voice & Alarms) +Fokus: Home Assistant Voice & Monitoring-Integration. + +Meilenstein: Nexa spricht über HA-Voice und meldet kritische Systemzustände proaktiv. + +Technik: Home Assistant API, Wyoming Protocol, ntfy/Memos-Push. + +4. Erfolgsmetriken +Woran merken wir, dass Nexa funktioniert? + +Reduktion der Mail-Interaktion: Weniger Zeit in der Mail-App, mehr Fokus auf den Nexa-Digest. + +Kalender-Füllrate: Fokus-Slots für Arbeit sind automatisch geblockt. + +Such-Geschwindigkeit: Informationen aus alten Memos/Obsidian werden via #ask sofort gefunden. \ No newline at end of file diff --git a/nexa-core/n8n-workflows/phase-1/1_1_discovery_workflow.json b/nexa-core/n8n-workflows/phase-1/1_1_discovery_workflow.json new file mode 100644 index 0000000..d2fa4e6 --- /dev/null +++ b/nexa-core/n8n-workflows/phase-1/1_1_discovery_workflow.json @@ -0,0 +1,127 @@ +{ + "name": "Phase 1.1: Nextcloud Lists Discovery", + "description": "Discovers and maps Nextcloud task list IDs (Work, Personal, Shopping, Wishes) for environment configuration", + "nodes": [ + { + "parameters": {}, + "name": "Start", + "type": "n8n-nodes-base.start", + "typeVersion": 1, + "position": [50, 50] + }, + { + "parameters": { + "method": "GET", + "url": "{{ $env.NC_HOST }}/remote.php/dav/lists/", + "authentication": "basicAuth", + "basicAuth": { + "user": "{{ $env.NC_USERNAME }}", + "password": "{{ $env.NC_APP_PASSWORD }}" + }, + "responseFormat": "text" + }, + "name": "Fetch Nextcloud Lists (WebDAV)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [250, 50], + "onError": "continueErrorOutput" + }, + { + "parameters": { + "jsCode": "// Parse XML response from WebDAV\nconst xmlText = $input.item.json.body || '';\n\n// Extract list names and IDs from XML\nconst listRegex = /(\\/remote\\.php\\/dav\\/lists\\/[^/]+\\/)<\\/d:href>[\\s\\S]*?([^<]+)<\\/d:displayname>/g;\nconst lists = [];\nlet match;\n\nwhile ((match = listRegex.exec(xmlText)) !== null) {\n const href = match[1];\n const displayname = match[2];\n const listId = href.split('/').filter(p => p)[3]; // Extract ID from path\n lists.push({\n name: displayname,\n id: listId,\n url: href\n });\n}\n\n// Identify special lists by name patterns\nconst result = {\n discovery_timestamp: new Date().toISOString(),\n lists_found: lists,\n mapping: {\n work: lists.find(l => l.name.toLowerCase().includes('work'))?.id || 'NOT_FOUND',\n personal: lists.find(l => l.name.toLowerCase().includes('personal'))?.id || 'NOT_FOUND',\n shopping: lists.find(l => l.name.toLowerCase().includes('shopping'))?.id || 'NOT_FOUND',\n wishes: lists.find(l => l.name.toLowerCase().includes('wishes') || l.name.toLowerCase().includes('want'))?.id || 'NOT_FOUND'\n }\n};\n\nreturn result;" + }, + "name": "Parse WebDAV Response", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [450, 50] + }, + { + "parameters": { + "jsCode": "// Generate .env snippet for configuration\nconst mapping = $input.item.json.mapping;\nconst timestamp = new Date().toISOString();\n\nconst envSnippet = `# NEXA Nextcloud Lists - Discovered on ${timestamp}\nNC_LIST_ID_WORK=${mapping.work}\nNC_LIST_ID_PERSONAL=${mapping.personal}\nNC_LIST_ID_SHOPPING=${mapping.shopping}\nNC_LIST_ID_WISHES=${mapping.wishes}`;\n\nreturn {\n env_snippet: envSnippet,\n discovered_lists: $input.item.json.lists_found,\n ready_for_deployment: Object.values(mapping).every(v => v !== 'NOT_FOUND')\n};" + }, + "name": "Generate .env Configuration", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [650, 50] + }, + { + "parameters": { + "method": "POST", + "url": "{{ $env.MEMOS_HOST }}/api/v1/memos", + "authentication": "headerAuth", + "headerAuth": { + "Authorization": "Bearer {{ $env.MEMOS_API_KEY }}" + }, + "sendBody": true, + "bodyParametersUi": "json", + "body": "{\n \"content\": \"🔍 **Nexa Discovery Report**\\n\\nNextcloud Lists gefunden:\\n\\n- **Work:** {{ $input.item.json.env_snippet.split('=')[1].split('\\\\n')[0] }}\\n- **Personal:** {{ $input.item.json.env_snippet.split('=')[2].split('\\\\n')[0] }}\\n- **Shopping:** {{ $input.item.json.env_snippet.split('=')[3].split('\\\\n')[0] }}\\n- **Wishes:** {{ $input.item.json.env_snippet.split('=')[4].split('\\\\n')[0] }}\\n\\nBitte diese IDs in .env eintragen.\",\n \"visibility\": \"PRIVATE\"\n}" + }, + "name": "Post Report to Memos", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [850, 50], + "onError": "continueErrorOutput" + }, + { + "parameters": { + "fromFile": true, + "fileName": "discovery_result.json" + }, + "name": "Output Discovery Result", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [1050, 50] + } + ], + "connections": { + "Start": { + "main": [ + [ + { + "node": "Fetch Nextcloud Lists (WebDAV)", + "type": "main", + "index": 0 + } + ] + ] + }, + "Fetch Nextcloud Lists (WebDAV)": { + "main": [ + [ + { + "node": "Parse WebDAV Response", + "type": "main", + "index": 0 + } + ] + ] + }, + "Parse WebDAV Response": { + "main": [ + [ + { + "node": "Generate .env Configuration", + "type": "main", + "index": 0 + } + ] + ] + }, + "Generate .env Configuration": { + "main": [ + [ + { + "node": "Post Report to Memos", + "type": "main", + "index": 0 + }, + { + "node": "Output Discovery Result", + "type": "main", + "index": 0 + } + ] + ] + } + } +} diff --git a/nexa-core/n8n-workflows/phase-1/1_2_memos_bridge.json b/nexa-core/n8n-workflows/phase-1/1_2_memos_bridge.json new file mode 100644 index 0000000..cdcf816 --- /dev/null +++ b/nexa-core/n8n-workflows/phase-1/1_2_memos_bridge.json @@ -0,0 +1,58 @@ +{ + "name": "Phase 1.2: Memos Bridge - Context Classifier & Task Router", + "description": "Receives Memos, classifies context (work/personal), and routes to appropriate Nextcloud list via SAIA", + "nodes": [ + { + "parameters": { + "path": "memos" + }, + "name": "Memos Webhook Trigger", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [50, 50] + }, + { + "parameters": { + "jsCode": "// Extract and validate Memos webhook payload\nconst payload = $input.item.json;\n\n// Structure expected from Memos webhook\nconst memo = {\n id: payload.memo?.id || payload.id,\n content: payload.memo?.content || payload.content || '',\n createdAt: payload.memo?.createdAt || payload.createdAt,\n archived: payload.memo?.archived || false,\n // Detect if it's a task (contains - [ ] or #todo)\n isTask: /(- \\[ \\]|-\\s\\[ \\])|(#todo)/i.test(payload.memo?.content || payload.content || ''),\n isPriority: /(#urgent|#important|!important)/i.test(payload.memo?.content || payload.content || '')\n};\n\n// Ignore archived memos\nif (memo.archived) {\n throw new Error('Skipping archived memo');\n}\n\n// Only process if it contains task marker or #todo\nif (!memo.isTask) {\n throw new Error('Memo is not marked as task (missing - [ ] or #todo)');\n}\n\nreturn {\n memo: memo,\n shouldProcess: true,\n timestamp: new Date().toISOString()\n};" + }, + "name": "Validate Memo Payload", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [250, 50] + }, + { + "parameters": { + "method": "POST", + "url": "{{ $env.SAIA_HOST }}/v1/chat/completions", + "authentication": "headerAuth", + "headerAuth": { + "Authorization": "Bearer {{ $env.SAIA_API_KEY }}" + }, + "sendBody": true, + "bodyParametersUi": "json", + "body": "{\n \"model\": \"{{ $env.SAIA_MODEL_CLASSIFICATION }}\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"You are Nexa, a context classifier. Analyze the following task memo and classify it as either 'work' or 'personal'. Consider:\\n- Work context: Professional projects, client work, coding, architecture, development\\n- Personal context: Household, shopping, hobbies, health, leisure\\n\\nRespond with ONLY valid JSON: {\\\"context\\\": \\\"work\\\" | \\\"personal\\\", \\\"urgency\\\": 1-5, \\\"summary\\\": \\\"one-line summary\\\", \\\"confidence\\\": 0.0-1.0}\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{ $input.item.json.memo.content }}\"\n }\n ],\n \"temperature\": 0.3,\n \"max_tokens\": 200\n}" + }, + "name": "SAIA: Classify Context", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [450, 50] + }, + { + "parameters": { + "jsCode": "// Parse SAIA response\nconst response = $input.item.json;\nconst content = response.choices?.[0]?.message?.content || '{}';\n\nlet classification;\ntry {\n // Try to extract JSON from response\n const jsonMatch = content.match(/\\{[^{}]*\\}/);\n classification = JSON.parse(jsonMatch ? jsonMatch[0] : content);\n} catch (e) {\n console.error('Failed to parse SAIA response:', content);\n classification = { context: 'personal', confidence: 0 };\n}\n\nreturn {\n raw_response: content,\n classification: classification,\n timestamp: new Date().toISOString()\n};" + }, + "name": "Parse Classification", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [650, 50] + }, + { + "parameters": { + "conditions": {\n "options": [\n {\n "condition": "nodeOutputProperty",\n "value1": "={{ $node[\\\"Parse Classification\\\"].json.classification.context }}",\n "value2": "work"\n },\n {\n "condition": "nodeOutputProperty",\n "value1": "={{ $node[\\\"Parse Classification\\\"].json.classification.context }}",\n "value2": "personal"\n }\n ]\n }\n },\n "name": "Route by Context",\n "type": "n8n-nodes-base.switch", +n "typeVersion": 1,\n "position": [850, 50]\n },\n {\n "parameters": {\n "method": "POST",\n "url": "{{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_WORK }}/\",\n "authentication": "basicAuth",\n "basicAuth": {\n "user": "{{ $env.NC_USERNAME }}\",\n "password": "{{ $env.NC_APP_PASSWORD }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "raw",\n "body": "BEGIN:VCALENDAR\\nVERSION:2.0\\nPRODID:-//Nexa//v1.0//EN\\nBEGIN:VTODO\\nUID:memo-{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}-work\\nDTSTART:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.createdAt }}\\nDTSTAMP:{{ now().toISOString() }}\\nSUMMARY:{{ ($node[\\\"Parse Classification\\\"].json.classification.summary || $node[\\\"Validate Memo Payload\\\"].json.memo.content.substring(0, 50)).replace(/\\\"/g, \\\"\\\") }}\\nDESCRIPTION:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.content }}\\nPRIORITY:{{ Math.round((6 - $node[\\\"Parse Classification\\\"].json.classification.urgency)) }}\\nSTATUS:NEEDS-ACTION\\nEND:VTODO\\nEND:VCALENDAR\"\n },\n "name": "Create Task - Work List",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1050, 50],\n "onError": "continueErrorOutput"\n },\n {\n "parameters": {\n "method": "POST",\n "url": "{{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_PERSONAL }}/\",\n "authentication": "basicAuth",\n "basicAuth": {\n "user": "{{ $env.NC_USERNAME }}\",\n "password": "{{ $env.NC_APP_PASSWORD }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "raw",\n "body": "BEGIN:VCALENDAR\\nVERSION:2.0\\nPRODID:-//Nexa//v1.0//EN\\nBEGIN:VTODO\\nUID:memo-{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}-personal\\nDTSTART:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.createdAt }}\\nDTSTAMP:{{ now().toISOString() }}\\nSUMMARY:{{ ($node[\\\"Parse Classification\\\"].json.classification.summary || $node[\\\"Validate Memo Payload\\\"].json.memo.content.substring(0, 50)).replace(/\\\"/g, \\\"\\\") }}\\nDESCRIPTION:{{ $node[\\\"Validate Memo Payload\\\"].json.memo.content }}\\nPRIORITY:{{ Math.round((6 - $node[\\\"Parse Classification\\\"].json.classification.urgency)) }}\\nSTATUS:NEEDS-ACTION\\nEND:VTODO\\nEND:VCALENDAR\"\n },\n "name": "Create Task - Personal List",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1050, 200],\n "onError": "continueErrorOutput"\n },\n {\n "parameters": {\n "jsCode": "// Create feedback message based on routing decision\nconst context = $node[\\\"Parse Classification\\\"].json.classification.context;\nconst urgency = $node[\\\"Parse Classification\\\"].json.classification.urgency;\nconst listName = context === 'work' ? 'Work' : 'Personal';\nconst emoji = context === 'work' ? '💼' : '🏠';\n\nreturn {\n feedback_content: `${emoji} Task routed to ${listName} list (Urgency: ${urgency}/5)`,\n classification: $node[\\\"Parse Classification\\\"].json.classification,\n memo_id: $node[\\\"Validate Memo Payload\\\"].json.memo.id\n};" + }, + "name": "Prepare Feedback", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [1250, 50] + },\n {\n "parameters": {\n "method": "PATCH",\n "url": "{{ $env.MEMOS_HOST }}/api/v1/memos/{{ $node[\\\"Validate Memo Payload\\\"].json.memo.id }}\",\n "authentication": "headerAuth",\n "headerAuth": {\n "Authorization": "Bearer {{ $env.MEMOS_API_KEY }}\"\n },\n "sendBody": true,\n "bodyParametersUi": "json",\n "body": "{\n \\\"content\\\": \\\"{{ $node[\\\"Prepare Feedback\\\"].json.feedback_content }}\\n\\n_Processed by Nexa at {{ now().toLocaleString() }}_\\\"\n}"\n },\n "name": "Post Feedback to Memos",\n "type": "n8n-nodes-base.httpRequest",\n "typeVersion": 3,\n "position": [1450, 50],\n "onError": "continueErrorOutput"\n }\n ],\n "connections": {\n "Memos Webhook Trigger": {\n "main": [\n [\n {\n "node": "Validate Memo Payload",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Validate Memo Payload": {\n "main": [\n [\n {\n "node": "SAIA: Classify Context",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "SAIA: Classify Context": {\n "main": [\n [\n {\n "node": "Parse Classification",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Parse Classification": {\n "main": [\n [\n {\n "node": "Route by Context",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Route by Context": {\n "main": [\n [ { "node": "Create Task - Work List", "type": "main", "index": 0 } ],\n [ { "node": "Create Task - Personal List", "type": "main", "index": 0 } ]\n ]\n },\n "Create Task - Work List": {\n "main": [\n [\n {\n "node": "Prepare Feedback",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Create Task - Personal List": {\n "main": [\n [\n {\n "node": "Prepare Feedback",\n "type": "main",\n "index": 0\n }\n ]\n ]\n },\n "Prepare Feedback": {\n "main": [\n [\n {\n "node": "Post Feedback to Memos",\n "type": "main",\n "index": 0\n }\n ]\n ]\n }\n }\n}\n \ No newline at end of file diff --git a/nexa-core/n8n-workflows/phase-1/1_2_memos_bridge_v2.json b/nexa-core/n8n-workflows/phase-1/1_2_memos_bridge_v2.json new file mode 100644 index 0000000..20bc069 --- /dev/null +++ b/nexa-core/n8n-workflows/phase-1/1_2_memos_bridge_v2.json @@ -0,0 +1,143 @@ +{ + "name": "Phase 1.2: Memos Bridge - Context Classifier & Task Router", + "description": "Receives Memos webhook, classifies context (work/personal) via SAIA, routes to appropriate Nextcloud list", + "nodes": [ + { + "parameters": { + "path": "memos" + }, + "name": "Memos Webhook Trigger", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [50, 50] + }, + { + "parameters": { + "jsCode": "const payload = $input.item.json;\nconst memo = {\n id: payload.memo?.id || payload.id,\n content: payload.memo?.content || payload.content || '',\n createdAt: payload.memo?.createdAt || payload.createdAt,\n archived: payload.memo?.archived || false,\n isTask: /(- \\[ \\]|#todo)/i.test(payload.memo?.content || payload.content || ''),\n isPriority: /(#urgent|#important)/i.test(payload.memo?.content || payload.content || '')\n};\nif (memo.archived || !memo.isTask) {\n throw new Error('Memo not a task');\n}\nreturn { memo, timestamp: new Date().toISOString() };" + }, + "name": "Validate Memo Payload", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [250, 50] + }, + { + "parameters": { + "method": "POST", + "url": "={{ $env.SAIA_HOST }}/v1/chat/completions", + "authentication": "headerAuth", + "headerAuth": { + "Authorization": "Bearer {{ $env.SAIA_API_KEY }}" + }, + "sendBody": true, + "bodyParametersUi": "json", + "body": "{\n \"model\": \"{{ $env.SAIA_MODEL_CLASSIFICATION }}\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Classify as work or personal. Respond: {context: work|personal, urgency: 1-5, summary: str}\"\n },\n {\n \"role\": \"user\",\n \"content\": \"{{ $input.item.json.memo.content }}\"\n }\n ],\n \"temperature\": 0.3\n}" + }, + "name": "SAIA: Classify Context", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [450, 50] + }, + { + "parameters": { + "jsCode": "const content = $input.item.json.choices?.[0]?.message?.content || '{}';\nlet classification;\ntry {\n const jsonMatch = content.match(/\\{[^{}]*\\}/);\n classification = JSON.parse(jsonMatch?.[0] || content);\n} catch (e) {\n classification = { context: 'personal', urgency: 2 };\n}\nreturn { classification, raw: content };" + }, + "name": "Parse Classification", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [650, 50] + }, + { + "parameters": { + "conditions": { + "options": [ + { + "condition": "nodeOutputProperty", + "value1": "={{ $node[\"Parse Classification\"].json.classification.context }}", + "value2": "work" + }, + { + "condition": "nodeOutputProperty", + "value1": "={{ $node[\"Parse Classification\"].json.classification.context }}", + "value2": "personal" + } + ] + } + }, + "name": "Route by Context", + "type": "n8n-nodes-base.switch", + "typeVersion": 1, + "position": [850, 50] + }, + { + "parameters": { + "method": "POST", + "url": "={{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_WORK }}/", + "authentication": "basicAuth", + "basicAuth": { + "user": "={{ $env.NC_USERNAME }}", + "password": "={{ $env.NC_APP_PASSWORD }}" + }, + "sendBody": true, + "bodyParametersUi": "raw", + "body": "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Nexa//v1.0//EN\nBEGIN:VTODO\nUID:{{ $node[\"Validate Memo Payload\"].json.memo.id }}\nSUMMARY:{{ $node[\"Parse Classification\"].json.classification.summary || $node[\"Validate Memo Payload\"].json.memo.content.substring(0, 50) }}\nDESCRIPTION:{{ $node[\"Validate Memo Payload\"].json.memo.content }}\nSTATUS:NEEDS-ACTION\nEND:VTODO\nEND:VCALENDAR" + }, + "name": "Create Task - Work List", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [1050, 50] + }, + { + "parameters": { + "method": "POST", + "url": "={{ $env.NC_HOST }}/remote.php/dav/lists/{{ $env.NC_LIST_ID_PERSONAL }}/", + "authentication": "basicAuth", + "basicAuth": { + "user": "={{ $env.NC_USERNAME }}", + "password": "={{ $env.NC_APP_PASSWORD }}" + }, + "sendBody": true, + "bodyParametersUi": "raw", + "body": "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Nexa//v1.0//EN\nBEGIN:VTODO\nUID:{{ $node[\"Validate Memo Payload\"].json.memo.id }}\nSUMMARY:{{ $node[\"Parse Classification\"].json.classification.summary || $node[\"Validate Memo Payload\"].json.memo.content.substring(0, 50) }}\nDESCRIPTION:{{ $node[\"Validate Memo Payload\"].json.memo.content }}\nSTATUS:NEEDS-ACTION\nEND:VTODO\nEND:VCALENDAR" + }, + "name": "Create Task - Personal List", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [1050, 200] + }, + { + "parameters": { + "jsCode": "const context = $node[\"Parse Classification\"].json.classification.context;\nconst emoji = context === 'work' ? '💼' : '🏠';\nreturn { feedback: emoji + ' Task routed to ' + context + ' list', context };" + }, + "name": "Prepare Feedback", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [1250, 50] + } + ], + "connections": { + "Memos Webhook Trigger": { + "main": [[{ "node": "Validate Memo Payload", "type": "main", "index": 0 }]] + }, + "Validate Memo Payload": { + "main": [[{ "node": "SAIA: Classify Context", "type": "main", "index": 0 }]] + }, + "SAIA: Classify Context": { + "main": [[{ "node": "Parse Classification", "type": "main", "index": 0 }]] + }, + "Parse Classification": { + "main": [[{ "node": "Route by Context", "type": "main", "index": 0 }]] + }, + "Route by Context": { + "main": [ + [{ "node": "Create Task - Work List", "type": "main", "index": 0 }], + [{ "node": "Create Task - Personal List", "type": "main", "index": 0 }] + ] + }, + "Create Task - Work List": { + "main": [[{ "node": "Prepare Feedback", "type": "main", "index": 0 }]] + }, + "Create Task - Personal List": { + "main": [[{ "node": "Prepare Feedback", "type": "main", "index": 0 }]] + } + } +} diff --git a/nexa-core/n8n-workflows/phase-2/2_1_email_butler.json b/nexa-core/n8n-workflows/phase-2/2_1_email_butler.json new file mode 100644 index 0000000..ae48f51 --- /dev/null +++ b/nexa-core/n8n-workflows/phase-2/2_1_email_butler.json @@ -0,0 +1,193 @@ +{ + "name": "Phase 2.1: Email Butler - Nextcloud Mail Digest & Summarization", + "description": "Reads private emails from Nextcloud Mail, summarizes daily digest, posts to Memos", + "nodes": [ + { + "parameters": { + "triggerType": "on", + "cronExpression": "0 7 * * *" + }, + "name": "Daily Trigger (7 AM)", + "type": "n8n-nodes-base.cronTrigger", + "typeVersion": 1, + "position": [50, 50] + }, + { + "parameters": { + "method": "GET", + "url": "={{ $env.NC_HOST }}/remote.php/dav/calendars/{{ $env.NC_USERNAME }}/personal/", + "authentication": "basicAuth", + "basicAuth": { + "user": "={{ $env.NC_USERNAME }}", + "password": "={{ $env.NC_APP_PASSWORD }}" + }, + "responseFormat": "json" + }, + "name": "Discover Nextcloud Mail Account", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [250, 50], + "onError": "continueErrorOutput" + }, + { + "parameters": { + "method": "GET", + "url": "={{ $env.NC_HOST }}/ocs/v2.php/apps/mail/api/v1/accounts", + "authentication": "basicAuth", + "basicAuth": { + "user": "={{ $env.NC_USERNAME }}", + "password": "={{ $env.NC_APP_PASSWORD }}" + }, + "responseFormat": "json", + "headers": { + "OCS-APIREQUEST": "true" + } + }, + "name": "Fetch Mail Accounts from Nextcloud", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [450, 50], + "onError": "continueErrorOutput" + }, + { + "parameters": { + "jsCode": "// Extract mail account IDs\nconst accounts = $input.item.json.ocs?.data || [];\nif (!Array.isArray(accounts) || accounts.length === 0) {\n throw new Error('No mail accounts found in Nextcloud');\n}\n\n// Find personal account (first private account)\nconst personalAccount = accounts.find(a => a.name?.toLowerCase().includes('privat') || a.name?.toLowerCase().includes('personal')) || accounts[0];\n\nreturn {\n account_id: personalAccount.id,\n account_name: personalAccount.name,\n email: personalAccount.email,\n total_accounts: accounts.length\n};" + }, + "name": "Extract Personal Mail Account", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [650, 50] + }, + { + "parameters": { + "method": "GET", + "url": "={{ $env.NC_HOST }}/ocs/v2.php/apps/mail/api/v1/accounts/{{ $node['Extract Personal Mail Account'].json.account_id }}/mailboxes", + "authentication": "basicAuth", + "basicAuth": { + "user": "={{ $env.NC_USERNAME }}", + "password": "={{ $env.NC_APP_PASSWORD }}" + }, + "responseFormat": "json", + "headers": { + "OCS-APIREQUEST": "true" + } + }, + "name": "Fetch Mailboxes", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [850, 50], + "onError": "continueErrorOutput" + }, + { + "parameters": { + "jsCode": "// Get INBOX mailbox ID\nconst mailboxes = $input.item.json.ocs?.data || [];\nconst inbox = mailboxes.find(m => m.name === 'INBOX');\nif (!inbox) {\n throw new Error('INBOX not found');\n}\nreturn { mailbox_id: inbox.id };" + }, + "name": "Get INBOX ID", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [1050, 50] + }, + { + "parameters": { + "method": "GET", + "url": "={{ $env.NC_HOST }}/ocs/v2.php/apps/mail/api/v1/accounts/{{ $node['Extract Personal Mail Account'].json.account_id }}/mailboxes/{{ $node['Get INBOX ID'].json.mailbox_id }}/messages", + "authentication": "basicAuth", + "basicAuth": { + "user": "={{ $env.NC_USERNAME }}", + "password": "={{ $env.NC_APP_PASSWORD }}" + }, + "responseFormat": "json", + "headers": { + "OCS-APIREQUEST": "true" + }, + "qs": "from={{ $now.subtract(1, 'day').format('YYYY-MM-DD') }}&limit=20" + }, + "name": "Fetch Today's Messages", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [1250, 50], + "onError": "continueErrorOutput" + }, + { + "parameters": { + "jsCode": "// Prepare email list for summarization\nconst messages = $input.item.json.ocs?.data || [];\nconst emails = messages.map(msg => ({\n from: msg.from?.join(', ') || 'Unknown',\n subject: msg.subject || '(No Subject)',\n preview: msg.preview?.substring(0, 200) || msg.plainTextBody?.substring(0, 200) || '(No preview)',\n date: msg.dateInt ? new Date(msg.dateInt * 1000).toLocaleDateString('de-DE') : 'Unknown'\n}));\n\nconst emailList = emails.map((e, i) => `${i+1}. **${e.subject}** (von ${e.from})\\n ${e.preview}`).join('\\n\\n');\n\nreturn {\n total_emails: emails.length,\n email_list: emailList,\n emails_json: emails\n};" + }, + "name": "Prepare Email Summary", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [1450, 50] + }, + { + "parameters": { + "method": "POST", + "url": "={{ $env.SAIA_HOST }}/v1/chat/completions", + "authentication": "headerAuth", + "headerAuth": { + "Authorization": "Bearer {{ $env.SAIA_API_KEY }}" + }, + "sendBody": true, + "bodyParametersUi": "json", + "body": "{\n \"model\": \"gpt-3.5-turbo\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Write a concise 3-5 line daily email digest summarizing key points and action items.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Summarize these {{ $node['Prepare Email Summary'].json.total_emails }} emails:\\n\\n{{ $node['Prepare Email Summary'].json.email_list }}\"\n }\n ],\n \"temperature\": 0.7,\n \"max_tokens\": 300\n}" + }, + "name": "SAIA: Summarize Emails", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [1650, 50] + }, + { + "parameters": { + "jsCode": "const summary = $input.item.json.choices?.[0]?.message?.content || 'No emails today';\nconst timestamp = new Date().toLocaleTimeString('de-DE', {timeZone: 'Europe/Berlin'});\nreturn {\n summary,\n timestamp,\n email_count: $node['Prepare Email Summary'].json.total_emails\n};" + }, + "name": "Extract Summary", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [1850, 50] + }, + { + "parameters": { + "method": "POST", + "url": "={{ $env.MEMOS_HOST }}/api/v1/memos", + "authentication": "headerAuth", + "headerAuth": { + "Authorization": "Bearer {{ $env.MEMOS_API_KEY }}" + }, + "sendBody": true, + "bodyParametersUi": "json", + "body": "{\n \"content\": \"📧 **Email Digest** ({{ $node['Extract Summary'].json.email_count }} Mails)\\n\\n{{ $node['Extract Summary'].json.summary }}\\n\\n_Generated at {{ $node['Extract Summary'].json.timestamp }}_\",\n \"visibility\": \"PRIVATE\"\n}" + }, + "name": "Post Digest to Memos", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [2050, 50] + } + ], + "connections": { + "Daily Trigger (7 AM)": { + "main": [[{ "node": "Fetch Mail Accounts from Nextcloud", "type": "main", "index": 0 }]] + }, + "Fetch Mail Accounts from Nextcloud": { + "main": [[{ "node": "Extract Personal Mail Account", "type": "main", "index": 0 }]] + }, + "Extract Personal Mail Account": { + "main": [[{ "node": "Fetch Mailboxes", "type": "main", "index": 0 }]] + }, + "Fetch Mailboxes": { + "main": [[{ "node": "Get INBOX ID", "type": "main", "index": 0 }]] + }, + "Get INBOX ID": { + "main": [[{ "node": "Fetch Today's Messages", "type": "main", "index": 0 }]] + }, + "Fetch Today's Messages": { + "main": [[{ "node": "Prepare Email Summary", "type": "main", "index": 0 }]] + }, + "Prepare Email Summary": { + "main": [[{ "node": "SAIA: Summarize Emails", "type": "main", "index": 0 }]] + }, + "SAIA: Summarize Emails": { + "main": [[{ "node": "Extract Summary", "type": "main", "index": 0 }]] + }, + "Extract Summary": { + "main": [[{ "node": "Post Digest to Memos", "type": "main", "index": 0 }]] + } + } +}