initial commit
This commit is contained in:
@@ -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
|
||||
@@ -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.** 🚀
|
||||
@@ -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.
|
||||
|
||||
@@ -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]".
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 = /<d:href>(\\/remote\\.php\\/dav\\/lists\\/[^/]+\\/)<\\/d:href>[\\s\\S]*?<d:displayname>([^<]+)<\\/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
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user