From a1e14c64c3beba10a0ce81c505185f715cc4b35c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 4 May 2026 21:26:08 +0000 Subject: [PATCH] =?UTF-8?q?Apply=20Q1=E2=80=93Q3=20decisions:=20GraphDB/SP?= =?UTF-8?q?ARQL,=20reuse=20Qdrant,=20self-host=20embeddings=20via=20TEI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Q1 → Ontotext GraphDB (SPARQL). docs/08 fully rewritten with RDF schema, example SPARQL queries (transitive deps via property paths, time-windowed topic counts, cross-pillar joins via nexa:vectorId). - Q2 → reuse qdrant_scientific with nexa_* collection prefix; docs/09 step 2 now points there explicitly. - Q3 → no OpenAI embeddings. Self-host on the docker host. Use TEI (HuggingFace text-embeddings-inference) — single Rust binary, ~500 MB image, OpenAI-compatible — instead of Ollama, since we only need embeddings. - docs/09 Phase-3.4 add-on simplified to a single Ontotext compose snippet (Neo4j option dropped) plus repo creation curl. - docs/11 Q3 marked resolved; new Q15 picks the model (bge-m3 vs nomic-embed) and adds the open question of whether SAIA already proxies an embedding model that would let us skip TEI entirely. - docs/03 + CLAUDE.md updated with the new decisions so future runs don't re-litigate. --- CLAUDE.md | 5 + docs/03-architecture.md | 13 +- docs/08-graphrag-architecture.md | 405 ++++++++++++++----------------- docs/09-deployment.md | 92 +++++-- docs/11-open-questions.md | 40 ++- 5 files changed, 298 insertions(+), 257 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 95fbf1f..95bd968 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,11 @@ This file tells future automated runs what they need to know about this repo. - VM 100 haos — Home Assistant. - **Already-running services on docker host** (don't redeploy): - Memos `:5230`, n8n `:5678`, LiteLLM `:4000` (UI LobeHub `:3210`), Qdrant (`qdrant_scientific`), ntfy `:7998`, Karakeep/Hoarder, Vaultwarden `:11001`, Pocket-ID `:1411`, Immich, Audiobookshelf, Paperless-ngx, Traccar, Prowlarr, plus MCP containers (`crawl4ai-mcp`, `markitdown-mcp`, `papersearch-mcp`). +- **Decided for Nexa** (don't re-litigate without user input): + - Vector store: **reuse `qdrant_scientific`** with `nexa_*` collection prefix. + - Embeddings: **TEI** (HF text-embeddings-inference), self-hosted, `bge-m3` candidate (Q15 still pending the model name). + - Graph store: **Ontotext GraphDB** (SPARQL/RDF), Phase 3.4. + - Chat model: SAIA via LiteLLM virtual key. ## When working on Nexa diff --git a/docs/03-architecture.md b/docs/03-architecture.md index 544df46..d796f8f 100644 --- a/docs/03-architecture.md +++ b/docs/03-architecture.md @@ -21,11 +21,11 @@ A one-page mental model. For details follow the cross-links. │ SAIA │ │ Qdrant │ │ LiteLLM │ │ vector │ └──────────┘ └────┬─────┘ - │ optional + │ ▼ ┌──────────┐ - │ Graph │ (Neo4j or Ontotext — - │ DB │ see open questions) + │ GraphDB │ Ontotext, SPARQL + │ (RDF) │ Phase 3.4 └──────────┘ │ ▼ writes @@ -42,8 +42,9 @@ A one-page mental model. For details follow the cross-links. | **Memos** | Interface, voice input, webhook source | docker host LXC 104 → `memos.nuclide.systems` | | **n8n** | Workflow / logic engine | docker host LXC 104 → `n8n.nuclide.systems` | | **SAIA / LiteLLM** | Model gateway, embeddings, classification | docker host LXC 104 → `ai.nuclide.systems` (LiteLLM internal :4000) | -| **Qdrant** | Vector memory (semantic recall) | docker host LXC 104 (existing `qdrant_scientific` may be reused — see [11](./11-open-questions.md)) | -| **Graph DB** | Structural memory (deferred) | not yet deployed | +| **Qdrant** | Vector memory (semantic recall) | docker host LXC 104 — reuse existing `qdrant_scientific` with `nexa_*` collection prefix | +| **Ontotext GraphDB** | Structural memory via SPARQL (Phase 3.4) | not yet deployed; see [09-deployment](./09-deployment.md#phase-add-on-graph-db-phase-34) | +| **TEI** (HF text-embeddings-inference) | Self-hosted embeddings (`bge-m3` / TBD — see [11/Q15](./11-open-questions.md)) | docker host LXC 104, CPU only — single-binary embedding server | | **Nextcloud** | Tasks, calendar, mail, files | dedicated LXC 105 → `nc.nuclide.systems` | | **ntfy** | Push channel for system alerts | docker host → `ntfy.nuclide.systems` | | **Backrest** | Backup orchestration | LXC 103 | @@ -54,7 +55,7 @@ A one-page mental model. For details follow the cross-links. ## Two-pillar memory - **Qdrant** answers *"what is similar / relevant?"* (cosine search over embeddings). -- **Graph DB** answers *"who, what depends on what, how is it structured?"* (Cypher / SPARQL). +- **Ontotext GraphDB** answers *"who, what depends on what, how is it structured?"* (SPARQL over RDF). Both pillars are queried in parallel for `#nexa:ask` and merged before SAIA generates the final answer. See [08 — GraphRAG architecture](./08-graphrag-architecture.md). diff --git a/docs/08-graphrag-architecture.md b/docs/08-graphrag-architecture.md index 430b9fa..5cd1ddd 100644 --- a/docs/08-graphrag-architecture.md +++ b/docs/08-graphrag-architecture.md @@ -1,277 +1,246 @@ # 08 — GraphRAG: Structural Knowledge & Relations -> ⚠️ **Open decision (see [11-open-questions](./11-open-questions.md))**: this document describes the design with **Neo4j**. The earlier deployment draft referenced **Ontotext GraphDB** (SPARQL). Pick one before Phase 3.4. - -## 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 -``` +Decision: graph layer = **Ontotext GraphDB** with **SPARQL** (resolved in [11/Q1](./11-open-questions.md)). +Rationale: SPARQL + RDF lets Nexa's memory be browsed and queried with the same standard tooling that's used for any open-data corpus, and it leaves the door open for SHACL / OWL reasoning later. --- -## 📊 Neo4j Schema für Nexa +## Two-pillar memory -### Node-Typen +| Pillar | Question it answers | Backed by | +|--------|---------------------|-----------| +| **Qdrant** (vectors) | *"What is similar / relevant?"* | Cosine search over embeddings | +| **GraphDB** (RDF) | *"What is connected? What depends on what? Who is involved?"* | SPARQL over a typed graph | -```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 | +Both pillars are queried in parallel for `#nexa:ask` and merged before SAIA generates the final answer. --- -## 🔄 Synchronisierungs-Flows +## RDF schema -### 1. **Memos → Neo4j** (automatisch beim Erstellen) +Compact, opinionated. One namespace, one ontology file, no v2/v3 inheritance pain. -Wenn ein Memo mit `- [ ]` erstellt wird: +```turtle +@prefix nexa: . +@prefix rdf: . +@prefix rdfs: . +@prefix xsd: . +@prefix prov: . -``` -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) +# Classes +nexa:Project a rdfs:Class . +nexa:Task a rdfs:Class . +nexa:Person a rdfs:Class . +nexa:Technology a rdfs:Class . +nexa:Topic a rdfs:Class . +nexa:Note a rdfs:Class . # Memos / Obsidian / mail digests +nexa:File a rdfs:Class . + +# Properties +nexa:owns a rdf:Property ; rdfs:domain nexa:Person ; rdfs:range nexa:Task . +nexa:uses a rdf:Property ; rdfs:domain nexa:Task ; rdfs:range nexa:Technology . +nexa:dependsOn a rdf:Property ; rdfs:domain nexa:Task ; rdfs:range nexa:Task . +nexa:childOf a rdf:Property ; rdfs:domain nexa:Task ; rdfs:range nexa:Project . +nexa:mentions a rdf:Property ; rdfs:domain nexa:Note ; rdfs:range nexa:Topic . +nexa:scheduledFor a rdf:Property ; rdfs:domain nexa:Task ; rdfs:range xsd:dateTime . + +# Datatype properties +nexa:status a rdf:Property ; rdfs:range xsd:string . # "needs-action" | "in-progress" | "done" +nexa:context a rdf:Property ; rdfs:range xsd:string . # "work" | "personal" +nexa:urgency a rdf:Property ; rdfs:range xsd:integer . # 1–5 +nexa:vectorId a rdf:Property ; rdfs:range xsd:string . # Qdrant point ID — bridges the two pillars +nexa:contentHash a rdf:Property ; rdfs:range xsd:string . # for de-dup ``` -### 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) -``` +The `nexa:vectorId` property is the **bridge** between graph and vector store. Every node that has semantic content carries the Qdrant point ID, so a SPARQL hit can trigger a vector lookup and vice versa. --- -## 🤖 GraphRAG Queries - Praktische Anwendungen +## Sync flows -### Query 1: "Zeige alle offenen Tasks, die JWT betreffen" +### 1. Memos → GraphDB (real-time) -```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 +``` +Memo content: "Muss JWT-Middleware für Auth-Service refaktorieren" + │ + ▼ SAIA extracts entities + relations as JSON + │ { tasks: [{title, urgency}], technologies: [...], + │ relations: [{type:"uses", from:..., to:...}] } + │ + ▼ n8n turns JSON into a SPARQL UPDATE + │ + └──▶ INSERT DATA { ... } against GraphDB repo "nexa_knowledge" ``` -### Query 2: "Von welchen Tasks/Projects hängt Auth-Service ab?" +### 2. Obsidian → GraphDB (`#nexa:sync-obsidian`) -```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 -``` +For each Obsidian note: parse front-matter + headings → emit `nexa:Project`, `nexa:Task`, `nexa:Note` triples; `nexa:mentions` for `[[wikilinks]]`. -### Query 3: "Wer arbeitet an diesem Projekt und auf welchen Technologien?" +### 3. Nextcloud Tasks ↔ GraphDB (bidirectional) -```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 -``` +n8n trigger on Nextcloud CalDAV/Tasks change → `INSERT/DELETE DATA` to keep `nexa:status` and `nexa:scheduledFor` in sync. --- -## 🔗 GraphRAG-Enhanced Answer Generation +## Example SPARQL queries -Wenn Nutzer `#nexa:ask` fragt: +### Q1 — All open tasks involving JWT, by urgency +```sparql +PREFIX nexa: + +SELECT ?taskTitle ?urgency ?projectName +WHERE { + ?tech rdfs:label "JWT" . + ?task nexa:uses ?tech ; + rdfs:label ?taskTitle ; + nexa:status ?status ; + nexa:urgency ?urgency . + FILTER (?status IN ("needs-action", "in-progress")) + OPTIONAL { ?task nexa:childOf ?project . ?project rdfs:label ?projectName . } +} +ORDER BY DESC(?urgency) ``` -#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 + +### Q2 — What does Auth-Service transitively depend on? + +```sparql +PREFIX nexa: + +SELECT DISTINCT ?dep ?label +WHERE { + ?root rdfs:label "Auth-Service" . + ?root nexa:dependsOn+ ?dep . + ?dep rdfs:label ?label . +} ``` +(`+` is SPARQL property-paths — transitive closure, free.) + +### Q3 — Topics with the most note-mentions in the last day + +```sparql +PREFIX nexa: +PREFIX xsd: + +SELECT ?topic (COUNT(?note) AS ?n) +WHERE { + ?note a nexa:Note ; + prov:generatedAtTime ?ts ; + nexa:mentions ?topic . + FILTER (?ts > NOW() - "P1D"^^xsd:duration) +} +GROUP BY ?topic +ORDER BY DESC(?n) +LIMIT 10 +``` + +### Q4 — Cross-pillar: "find vectors for tasks blocking project X" + +```sparql +PREFIX nexa: + +SELECT ?taskTitle ?vectorId +WHERE { + ?proj rdfs:label "Nexa" . + ?task nexa:childOf ?proj ; + nexa:status "in-progress" ; + nexa:vectorId ?vectorId ; + rdfs:label ?taskTitle . +} +``` + +n8n then takes each `?vectorId`, fetches the embedding from Qdrant, and runs a "more like this" search for richer context. + --- -## 🛠️ n8n Integration (Phase 3) +## GraphRAG answer pipeline (`#nexa:ask`) + +``` + #nexa:ask + │ + ┌──────┴──────┐ + ▼ ▼ + [Qdrant] [GraphDB] + semantic structural + top-k SPARQL — auto-generated + notes paths / dependencies + │ │ + └──────┬──────┘ + ▼ + merge + rank + │ + ▼ + SAIA prompt: + "Given these passages and these relations, answer …" + │ + ▼ + comment under the original memo +``` + +Auto-generation of SPARQL: SAIA is given the ontology (above) as a system prompt and asked to emit a `SELECT`/`CONSTRUCT` query for the user's natural-language question. n8n executes it, falls back to a templated query on parse failure. + +--- + +## n8n integration sketch ### Workflow: Graph-Sync Trigger ``` -[Memos Webhook] - ↓ +[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] + │ +[SAIA: Extract entities + relations as JSON] + │ +[Build SPARQL UPDATE INSERT DATA { ... }] + │ +[HTTP POST → /repositories/nexa_knowledge/statements] + │ +[Index in Qdrant; write Qdrant point id back via second SPARQL UPDATE] ``` ### 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] + │ + ┌─┴────────────────┐ + ▼ ▼ +[SAIA: NL → SPARQL] [Qdrant: kNN] + │ │ +[POST → SPARQL endpoint] + │ │ + └──────┬───────────┘ + ▼ + rank + merge → SAIA answer ``` --- -## 📋 Commands für Graph-Management +## Graph-management commands -### #nexa:graph-status -Zeigt Statistiken der Graph-DB -``` -#nexa:graph-status -``` +### `#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) -``` +Returns triple count, class histogram, most-connected entity. Implemented as one SPARQL `SELECT (COUNT)`. -### #nexa:graph-trace [entity] -Zeigt alle Verbindungen für eine Entität -``` -#nexa:graph-trace JWT-Middleware -``` +### `#nexa:graph-trace [entity]` -### #nexa:graph-rebuild -Rekonstruiert Graph aus Obsidian + Memos (einmalig, danach inkrementell) -``` -#nexa:graph-rebuild --force -``` +Returns the 1-hop (and optionally 2-hop) neighbourhood — a `DESCRIBE ` plus a templated outgoing/incoming query. + +### `#nexa:graph-rebuild` + +Clears the named graph and replays Obsidian + Memos. SPARQL: `CLEAR GRAPH ` followed by the import workflow. --- -## 🚀 Phase 3 Roadmap (Graph Integration) +## Why two stores -- [ ] **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 +| Scenario | Qdrant | GraphDB | Best | +|----------|--------|---------|------| +| "Which note was similar to this one?" | ✅ | ❌ | Qdrant | +| "What blocks this task?" | ❌ | ✅ | GraphDB | +| "Explain this project" | ✅ context | ✅ structure | both | +| "All JWT-related open work" | ✅ semantic | ✅ crisp | both | ---- - -## 💡 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. +Combined: **complete understanding** rather than a search index *or* a structure index. diff --git a/docs/09-deployment.md b/docs/09-deployment.md index 7358e64..298e327 100644 --- a/docs/09-deployment.md +++ b/docs/09-deployment.md @@ -20,19 +20,20 @@ Surveyed from Homepage / Dozzle / Proxmox / Zoraxy: | Backrest | LXC 103 | (internal) | | AdGuard DNS | LXC 102 | (internal) | | Zoraxy reverse proxy | LXC 108 → `192.168.1.4:8000` | TLS for *.nuclide.systems | -| `qdrant_scientific` (existing) | docker LXC 104 | reuse — see [11](./11-open-questions.md) | +| `qdrant_scientific` (existing) | docker LXC 104 | **reused** — Nexa uses `nexa_*` collections in this instance | The deployment task is **not** "spin up the stack" — most of the stack is already up. It is **wire Nexa across these services + add the small bits that are missing**. ## What's missing for Nexa -1. **Qdrant collection** for Nexa (`nexa_knowledge`) — either on the existing `qdrant_scientific` or a dedicated container. -2. **n8n workflows** (`./nexa-core/n8n-workflows/`) imported into the running n8n. -3. **Nextcloud lists & calendars** for Work / Personal / Shopping / Wishes (auto-discovered via `#nexa:config`). -4. **Memos webhook → n8n** wired through the Memos config. -5. **LiteLLM virtual key** for the `nexa` user with embedding + chat models. -6. **A Zoraxy host entry** is *not* needed — Memos / n8n / LiteLLM are already proxied. -7. **(Phase 3.4)** decision on graph DB (Neo4j vs. Ontotext GraphDB) — see [11](./11-open-questions.md). +1. **Qdrant collection** for Nexa (`nexa_knowledge`) inside the existing `qdrant_scientific` instance — vector dim follows Q15 (1024 for `bge-m3`, 768 for `nomic-embed-text`). +2. **TEI** (HF text-embeddings-inference) on the docker host for self-hosted embeddings (LiteLLM key is **not** authorised for OpenAI embeddings — see [11/Q3+Q15](./11-open-questions.md)). Lighter than Ollama: single Rust binary, ~500 MB image, no LLM runtime. +3. **n8n workflows** (`./nexa-core/n8n-workflows/`) imported into the running n8n. +4. **Nextcloud lists & calendars** for Work / Personal / Shopping / Wishes (auto-discovered via `#nexa:config`). +5. **Memos webhook → n8n** wired through the Memos config. +6. **LiteLLM virtual key** for the `nexa` user with chat-only access (no embeddings — handled by Ollama). +7. **A Zoraxy host entry** is *not* needed — Memos / n8n / LiteLLM are already proxied. +8. **(Phase 3.4) Ontotext GraphDB** for the SPARQL pillar — see add-on at the bottom of this doc. --- @@ -60,14 +61,43 @@ curl -X PUT "$QDRANT_HOST/collections/nexa_knowledge" \ -d @nexa-core/config/qdrant_schema.json ``` -> If reusing `qdrant_scientific`, this just adds another collection; no new container needed. +> Targets the existing `qdrant_scientific` instance — just an extra collection, no new container. +> The schema file's `vectors.size` must match the embedding model picked in [Q15](./11-open-questions.md): **1024** for `bge-m3`, **768** for `nomic-embed-text-v1.5`. -## Step 3 — LiteLLM virtual key +## Step 3 — Self-hosted embeddings (TEI) + +Use HuggingFace **text-embeddings-inference** — single Rust binary, ~500 MB image, OpenAI-compatible API, loads exactly one model. Lighter than Ollama because there's no LLM runtime, no GGUF loader, no model registry. + +```bash +# on the docker host (LXC 104) +docker run -d --name nexa-embed \ + --restart unless-stopped \ + -p 127.0.0.1:8080:80 \ + -v tei-data:/data \ + ghcr.io/huggingface/text-embeddings-inference:cpu-1.5 \ + --model-id BAAI/bge-m3 +``` + +Memory budget: ~1.1 GB resident (bge-m3 is ~1 GB + ~100 MB overhead). First start downloads the model into the named volume; subsequent restarts are instant. + +Register it inside LiteLLM (admin UI → Models) with the OpenAI-compatible adapter: + +- model name: `nexa-embed` +- provider: `openai` +- model: `bge-m3` +- api_base: `http://nexa-embed:80/v1` +- api_key: any non-empty string (TEI ignores it) + +Now n8n only ever talks to LiteLLM and the model is swappable without touching workflows. + +## Step 4 — LiteLLM virtual key In the LiteLLM admin UI (`ai.nuclide.systems`): 1. Create user `nexa`. -2. Issue a virtual key with access to one chat model (e.g. `gpt-4o-mini` / a local Sonnet equivalent) and one embedding model (`text-embedding-3-small`). +2. Issue a virtual key with access to: + - one chat model (already-available model from your SAIA gateway). + - the `nexa-embed` model from Step 3. 3. Paste the key into `SAIA_API_KEY` in `.env`. ## Step 4 — n8n workflows @@ -158,31 +188,43 @@ For deeper detail: [10 — Operations](./10-operations.md). --- -## Phase add-on: graph DB (Phase 3.4) +## Phase add-on: Ontotext GraphDB (Phase 3.4) -Defer until 3.1–3.3 ship. When needed, two minimal options — pick one: - -### Option A — Neo4j (lighter, popular for graph-RAG) +Defer until 3.1–3.3 ship. ```yaml # nexa-core/docker-compose.graph.yml services: - neo4j: - image: neo4j:5-community - container_name: nexa-neo4j - ports: ["7687:7687", "7474:7474"] + graphdb: + image: ontotext/graphdb:10.7.0 + container_name: nexa-graphdb + ports: ["127.0.0.1:7200:7200"] environment: - NEO4J_AUTH: "neo4j/${NEO4J_PASSWORD}" - NEO4J_server_memory_heap_max__size: 2G - volumes: ["./data/neo4j:/data"] + GDB_JAVA_OPTS: "-Xmx4g -Xms1g" + volumes: + - ./data/graphdb:/opt/graphdb/home restart: unless-stopped ``` -### Option B — Ontotext GraphDB (SPARQL, RDF native) +After first start, create the repository (one-time): -Keep the snippet from the original `DEPLOYMENT.md` Schritt 1.3 for `graphdb`. Heavier (~4 GB heap), only justified if you want SHACL / OWL reasoning. +```bash +curl -X POST http://localhost:7200/rest/repositories \ + -H 'Content-Type: application/json' \ + -d '{ + "id": "nexa_knowledge", + "title": "Nexa Knowledge Graph", + "type": "graphdb", + "params": { + "ruleset": {"value": "rdfsplus-optimized"}, + "baseURL": {"value": "https://nuclide.systems/nexa/"} + } + }' +``` -Add a Zoraxy host entry `graph.nuclide.systems` → `192.168.1.40:7474` (Neo4j) or `:7200` (GraphDB) **only if** browser access is desired; otherwise n8n talks to it on the docker network. +Optional Zoraxy entry `graph.nuclide.systems` → `192.168.1.40:7200` if you want the SPARQL Workbench in a browser; otherwise n8n talks to it on the docker network at `http://nexa-graphdb:7200`. + +For schema and example queries: [08-graphrag-architecture](./08-graphrag-architecture.md). --- diff --git a/docs/11-open-questions.md b/docs/11-open-questions.md index ff5e7c8..e4ea61f 100644 --- a/docs/11-open-questions.md +++ b/docs/11-open-questions.md @@ -2,13 +2,22 @@ Items that **block progress** and need a human decision before a workflow can be implemented or a service deployed. Tick them off as you decide. +## Resolved + +- [x] **Q1 — Graph DB choice → Ontotext GraphDB (SPARQL).** Rationale: explore Nexa's memory through SPARQL is a stated goal. [08-graphrag-architecture](./08-graphrag-architecture.md) is rewritten accordingly. +- [x] **Q2 — Vector store → reuse `qdrant_scientific`** with a `nexa_*` collection prefix. No dedicated container. +- [x] **Q3 — Embeddings model → not OpenAI.** Self-host on the docker host via **TEI** (HF `text-embeddings-inference`) — Rust single-binary, OpenAI-compatible, ~500 MB image, no LLM runtime overhead. Speed analysis in §"Speed budget" below; concrete model still open as **Q15**. + ## Architectural decisions -- [ ] **Q1 — Graph DB choice.** Phase 3.4 says *Neo4j* in [08](./08-graphrag-architecture.md) but the original deployment draft used *Ontotext GraphDB*. **Recommendation:** Neo4j (smaller footprint, Cypher is friendlier for n8n HTTP nodes, no SHACL/OWL needs identified). Confirm or override. -- [ ] **Q2 — Reuse `qdrant_scientific` or run a dedicated `nexa-qdrant`?** Reuse keeps memory usage flat (~670 MB seen on Homepage); dedicated gives clean isolation and independent snapshot lifecycle. **Recommendation:** reuse with a `nexa_*` collection prefix. -- [ ] **Q3 — Embeddings model.** Default in `infra_manifest.yaml` is `text-embedding-3-small` (OpenAI). Is the LiteLLM virtual key authorised to call it, or should we route to a self-hosted alternative (e.g. `bge-m3` via Ollama on the same host)? - [ ] **Q4 — Obsidian sync mechanism.** `system_prime.txt` references Obsidian Context, but the current setup syncs via Nextcloud (`nc.nuclide.systems` → `Notizen` folder, ~200 MB). Should Nexa watch the **filesystem on LXC 105** (NC data dir) or **the Nextcloud WebDAV API**? FS is cheaper, WebDAV is portable. -- [ ] **Q5 — Karakeep vs. Hoarder naming.** The Zoraxy host is `hoarder.nuclide.systems` but the container is `karakeep-*` and Homepage labels it Karakeep. They're the same product (rename in 2024). Pick one display name to use in docs and prompts. +- [ ] **Q5 — Karakeep vs. Hoarder naming.** Zoraxy host is `hoarder.nuclide.systems` but containers are `karakeep-*` and Homepage labels it Karakeep. Same product (rename 2024). Pick one display name for docs and prompts. +- [ ] **Q15 — Embedding model (served via TEI).** Two viable options on the docker host (CPU only — see speed budget): + - **`BAAI/bge-m3`** (568 M params, ~1 GB RAM, multilingual incl. German, 1024-dim, ~10–20 docs/s on CPU). **Recommended** because the corpus is DE/EN-mixed. Requires `qdrant_schema.json` `vectors.size = 1024`. + - **`nomic-ai/nomic-embed-text-v1.5`** (137 M, ~250 MB RAM, EN-leaning, 768-dim, ~50 docs/s CPU). Lighter / faster but weaker on German. + - **(maybe) does SAIA already proxy any embedding model?** If the SAIA backend offers e.g. `mistral-embed` for free, we can skip TEI entirely. Worth a 1-line check in the LiteLLM admin UI before deploying TEI. + + Decide before Phase 3.1. ## Identifiers needed (auto-discoverable, but list now if known) @@ -20,10 +29,25 @@ Items that **block progress** and need a human decision before a workflow can be ## Hardware / capacity -- [ ] **Q11 — RAM headroom on docker host.** Homepage shows 29.5 GiB free out of ~31 GiB total, ~8.5 GB used. Phase-3 Qdrant indexing of Obsidian + Memos history is fine here, but a Phase-3.4 GraphDB choice changes this (Neo4j ~2 GB heap, Ontotext ~4 GB). Confirm acceptable. -- [ ] **Q12 — S3 archive bucket.** `s3.nuclide.systems` is up. Bucket name + access key for Qdrant snapshots? +- [ ] **Q11 — RAM headroom on docker host.** 29.5 GiB free / ~31 GiB total, ~8.5 GB used. Phase-3 Qdrant indexing + TEI/`bge-m3` (~1.1 GB) + Ontotext GraphDB (~4 GB heap) ⇒ ~14 GB used worst case, still ample. Confirm acceptable. +- [ ] **Q12 — S3 archive bucket.** `s3.nuclide.systems` is up. Bucket name + access key for Qdrant snapshots and GraphDB exports? ## Process -- [ ] **Q13 — Octoprint container is Exited** (Homepage). Out of scope for Nexa, but flagging it because Phase 5 monitoring would alert on it. Suppress, or is it intentional? -- [ ] **Q14 — `Missing Widget Type: zoraxy`** on Homepage — Homepage doesn't have a Zoraxy widget yet. Cosmetic, unrelated to Nexa. +- [ ] **Q13 — Octoprint container is Exited** (Homepage). Out of scope for Nexa, but Phase-5 monitoring would alert on it. Suppress or is it intentional? +- [ ] **Q14 — `Missing Widget Type: zoraxy`** on Homepage. Cosmetic, unrelated to Nexa. + +--- + +## Speed budget (Q3 follow-up) + +Workload on the docker host (16 CPU, ~30 GB free RAM): + +| Task | Volume | Latency target | Achievable on CPU with `bge-m3` | Achievable with `nomic-embed-text` | +|------|--------|----------------|---------------------------------|------------------------------------| +| Real-time memo embed | 1 doc | <500 ms incl. n8n round-trip | ✅ ~50–100 ms | ✅ ~20 ms | +| Daily ingest | ~70 docs | <60 s | ✅ ~5–10 s | ✅ ~2 s | +| Obsidian backfill (one-shot) | ~2 000 docs | <15 min | ✅ ~2–4 min | ✅ <1 min | +| RAG query embed (`#nexa:ask`) | 1 doc | <300 ms | ✅ ~50 ms | ✅ ~20 ms | + +Conclusion: CPU-only Ollama is sufficient — no GPU needed for current scope. Bottleneck is SAIA chat (already remote), not embeddings.