Files
rag-ingestor/docs/superpowers/specs/2026-05-04-rag-ingestor-design.md
Jean-Luc Makiola 5554f25738 docs: initial requirements und design spec
Initial commit fuer den RAG-Ingestor-Microservice. Enthaelt die
urspruengliche REQUIREMENTS.md und die ausgearbeitete Design-Spec
nach Brainstorming-Session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 21:29:13 +02:00

232 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RAG Ingestor Design
Stand: 2026-05-04
## Zweck
Microservice der neue/geänderte/gelöschte Dateien aus einem Nextcloud-Ordner automatisch in eine Qdrant-Collection einliest. Trigger sind Nextcloud-Webhooks oder ein manueller Bulk-Import-Endpoint. Embeddings werden via Ollama erzeugt.
## Scope
- **Beobachteter Root:** `Documents/THB/Studium/` (konfigurierbar via Env). Alles außerhalb wird mit 200 OK ignoriert.
- **Erwartete Pfadstruktur:** `Documents/THB/Studium/<N>.Semester/<Fach>/[<beliebige Unterordner>]/<datei>`
- `<N>.Semester` matcht Regex `^\d+\.Semester$`
- `<Fach>` = direkter Kindordner des Semesters
- Tiefere Pfadsegmente sind erlaubt; das erste Segment darunter wird als optionales Feld `typ` mitgeführt (z. B. `Vorlesungen`, `Uebungen`)
- **Unterstützte Dateitypen:** `.pdf`, `.md`, `.docx`, `.xlsx`. Andere → skip + log.
- **XLSX-Sonderfall:** Inhalt wird **nicht** extrahiert. Stattdessen wird ein Pseudo-Text der Form `Tabelle: <filename>` indexiert, damit die Datei als „existiert" auffindbar ist.
## Architektur
```
Nextcloud ──webhook──▶ FastAPI ──BackgroundTask──▶ Pipeline
│ │
▼ ▼
Auth (X-Webhook-Secret) WebDAV download
Extract (pdf/md/docx/xlsx)
Chunk (≈500w/50w)
Embed (Ollama)
Qdrant: delete_by_filter(file_path)
+ upsert points
```
Verarbeitung **asynchron im Prozess** über `fastapi.BackgroundTasks`. Kein externer Job-Broker. Bei Service-Crash gehen In-Flight-Jobs verloren; Recovery erfolgt durch manuellen Bulk-Import.
## Modulstruktur
```
app/
main.py # FastAPI-App, lifespan (Qdrant-Collection ensure)
config.py # pydantic-settings, alle Env-Vars
webhook/
handler.py # POST /webhook
auth.py # Shared-Secret-Verifikation
models.py # NextcloudEvent (created/updated/deleted)
ingest/
pipeline.py # process_file(path, event) — orchestriert Stages
webdav.py # download_file(path) → bytes (httpx Basic Auth)
extractors.py # PDF (pymupdf), MD (utf-8), DOCX (python-docx), XLSX
chunker.py # word-based mit Sentence-Boundary look-back
embedder.py # Ollama /api/embeddings + Retry
metadata.py # parse_path → {semester, fach, typ?}
qdrant_store.py # ensure_collection, upsert_chunks, delete_by_path
bulk.py # POST /bulk-import — recursive walk + dispatch
logging_setup.py
tests/
fixtures/ # Mini-Sample-Files (sample.pdf, sample.docx, ...)
test_metadata.py
test_chunker.py
test_extractors.py
test_webhook.py
docker/
Dockerfile
.env.example
pyproject.toml
README.md
```
## HTTP-Endpoints
| Method | Pfad | Auth | Zweck |
|--------|----------------|-----------------------|-----------------------------------------|
| POST | `/webhook` | `X-Webhook-Secret` | Nextcloud-Event-Empfang |
| POST | `/bulk-import` | `X-Webhook-Secret` | `{path}` rekursiv ingestieren |
| GET | `/health` | — | Liveness-Probe (Coolify) |
### Webhook Request (NextcloudEvent)
```json
{
"event_type": "created" | "updated" | "deleted",
"file_path": "Documents/THB/Studium/2.Semester/Databases/Vorlesungen/DBS1_02.pdf",
"file_name": "DBS1_02.pdf"
}
```
Response: `202 Accepted` sobald dispatched (Auth + Validierung haben gegriffen). Bei `401`/`422` kein Dispatch.
### Bulk-Import Request
```json
{ "path": "Documents/THB/Studium/2.Semester/Databases" }
```
Walks rekursiv, dispatched pro Datei einen `created`-Event-Job. Einzel-Failures unterbrechen den Walk nicht.
## Pipeline-Logik (pro Datei)
1. **Pfad-Filter:** Pfad muss mit `INGEST_ROOT` beginnen → sonst skip + log.
2. **Event-Branching:**
- `deleted``qdrant.delete_by_filter(file_path=...)`. Fertig.
- `created` / `updated` → weitermachen.
3. **Extension-Whitelist:** `.pdf`, `.md`, `.docx`, `.xlsx`. Sonst → skip.
4. **WebDAV-Download:** `httpx.AsyncClient` mit Basic Auth (User + App-Passwort), 60 s Timeout.
5. **Extraktion** (gibt Liste `[(page_num, text)]` zurück):
- **PDF**: `pymupdf` Seite für Seite
- **MD**: bytes → `utf-8` decode, `page=1`
- **DOCX**: `python-docx` Paragraphs joined mit `\n\n`, `page=1`
- **XLSX**: kein Inhalt — Pseudo-Text `"Tabelle: <filename>"`, `page=1`
6. **Chunking:** ~500 Wörter pro Chunk, ~50 Wörter Overlap. Sliding window mit Look-back zum nächsten Satzende (`. ! ?`) im letzten ~20 % des Chunks. Pro Chunk Felder: `text`, `page` (vom Extractor mitgegeben), `chunk_index` (aufsteigend pro Datei, beginnend bei 0).
7. **Embedding:** Ollama `POST /api/embeddings` pro Chunk, Modell aus Env. Retry **3×** mit Exponential Backoff (1 s, 2 s, 4 s) bei HTTP-Fehlern.
8. **Qdrant-Write:** Erst `delete_by_filter(file_path)` (idempotent für Webhook-Duplikate und für Updates), dann `upsert` aller neuen Points (UUIDs als IDs, Payload mit Metadaten).
## Qdrant-Schema
- **Collection:** `rag_thb_studium` (aus Env)
- **Vector-Dimension:** zur Boot-Zeit über Ollama `POST /api/show` ermittelt. Wenn Ollama oder Qdrant beim Start nicht erreichbar sind → Service crasht (fail-fast, Coolify startet neu). Falls Collection noch nicht existiert → wird neu erstellt. Falls Collection mit **anderer** Vektordimension existiert → Service crasht beim Start mit klarer Fehlermeldung; Operator muss Collection manuell droppen + Bulk-Import laufen lassen.
- **Distance:** `Cosine`
- **Payload-Indexes (beim Ensure):**
- `file_path` (keyword, für `delete_by_filter`)
- `semester` (keyword, für Query-Filter)
- `fach` (keyword, für Query-Filter)
- **Payload-Schema pro Chunk:**
```json
{
"file_path": "Documents/THB/Studium/2.Semester/Databases/Vorlesungen/DBS1_02.pdf",
"file_name": "DBS1_02.pdf",
"file_type": "pdf",
"semester": "2.Semester",
"fach": "Databases",
"typ": "Vorlesungen",
"page": 4,
"chunk_index": 17,
"text": "...",
"ingested_at": "2026-05-04T20:58:00Z"
}
```
`typ` ist `null`, wenn die Datei direkt im Fachordner liegt.
## Konfiguration
Alle Werte über Env-Vars (`pydantic-settings`):
```
NEXTCLOUD_WEBDAV_URL=https://nc.example.com/remote.php/dav/files/<user>
NEXTCLOUD_USER=<user>
NEXTCLOUD_APP_PASSWORD=...
OLLAMA_URL=http://ollama:11434
OLLAMA_EMBED_MODEL=qwen3-embedding:0.6b
QDRANT_URL=http://qdrant:6333
QDRANT_COLLECTION=rag_thb_studium
WEBHOOK_SECRET=<shared-secret>
INGEST_ROOT=Documents/THB/Studium
CHUNK_SIZE_WORDS=500
CHUNK_OVERLAP_WORDS=50
LOG_LEVEL=INFO
```
`.env.example` wird mit Dummy-Werten committet.
## Fehlerbehandlung
- **Webhook-Layer:**
- Auth-Fail → `401`
- Invalides Payload → `422`
- Pfad außerhalb `INGEST_ROOT` → `202` + Skip-Log (nicht als Fehler werten, Nextcloud kann beliebige Events senden)
- **Pipeline (im BackgroundTask):** `try/except` um jede Stage. Fehler werden geloggt mit `file_path`-Kontext, Job wird abgebrochen. **Kein Retry auf Pipeline-Ebene** (außer dem internen Embed-Retry).
- **Crash während In-Flight:** akzeptiert; Recovery via Bulk-Import.
- **Webhook-Duplikate:** Implizit idempotent durch `delete_by_filter` + frischer Insert.
- **Bulk-Import:** Einzel-Failures unterbrechen den Walk nicht (nur Log + continue).
## Logging
- Stdlib `logging` → stdout.
- Default-Format: human-readable Key=Value, z. B.
`INFO event=download file=Documents/THB/Studium/2.Semester/Databases/DBS1.pdf duration_ms=842 status=ok`
- Default-Level: `INFO`. `DEBUG` per Env.
- Pro Pipeline-Stage ein Log-Event mit `file_path`, `event` (= Stage-Name), `status`, ggf. `duration_ms` und `error`.
- Kein JSON-Log-Format zum Start (Coolify-Logs sollen lesbar bleiben). Erweiterbar via Env-Flag, falls später nötig.
## Tests
- `pytest` + `pytest-asyncio`.
- **Unit-Fokus** auf Pure-Logic — keine Integration-Tests gegen echte Ollama-/Qdrant-Instanzen.
- `test_metadata.py`: Pfad-Parser für valide Pfade (alle Semester/Fach-Varianten), invalide Pfade (Studienbescheinigung, außerhalb-Root, Fach ohne Subordner)
- `test_chunker.py`: Chunk-Größen, Overlap, Satz-Boundary-Look-Back, kurze Texte (< 1 Chunk), edge cases
- `test_extractors.py`: Mini-Sample-Files (sehr kleine, eingecheckte Fixtures) für jeden unterstützten Typ
- `test_webhook.py`: Auth-Header-Validierung, Payload-Schema, Skip-Verhalten für Pfade außerhalb `INGEST_ROOT`
## Deployment
- `Dockerfile` (Base: `python:3.12-slim`, Deps via `uv`)
- `docker-compose.yml` als Beispiel mit Service-Stub neben Qdrant + Ollama (für lokale Entwicklung)
- In Coolify wird der Container neben den existierenden Qdrant- und Ollama-Diensten deployed. Webhook-URL ist nur intern erreichbar.
## Tech-Stack-Pin
- Python 3.12
- FastAPI
- httpx (WebDAV + Ollama)
- pymupdf, python-docx
- qdrant-client
- pydantic, pydantic-settings
- pytest, pytest-asyncio
- uv (Dependency-Management)
## Bewusste Auslassungen (YAGNI)
- **Sidecar-Metadaten** (`.rag-meta.json` o. ä.) — geparkt, bis konkreter Filter-Bedarf entsteht.
- **Job-Queue** (Redis/Celery/Arq) — BackgroundTasks reichen für aktuelles Volumen.
- **Persistenter Job-State** — Recovery via Bulk-Import statt eigener State-Store.
- **Sub-Path-Watching** unter `Studium/` — alle Pfade unter dem Root werden ingestiert.
- **DOCX-Bilder/-Tabellen-Extraktion** — nur Paragraphs.
- **XLSX-Inhaltsextraktion** — nur Filename als Pseudo-Indexeintrag.
- **Authentifizierte WebDAV-User-Differenzierung** — ein einziger Service-User, App-Passwort.
- **Reindex-Migrationen** bei Modell-/Dimension-Änderung — manueller Drop + Bulk-Import.
## Voraussetzungen außerhalb des Service
- 1. Semester wird vom Nutzer von „nach Dozent gruppiert" auf „nach Fach gruppiert" umstrukturiert (passend zur 2. Semester-Konvention).
- Studieninhalte werden in einen neuen Unterordner `Documents/THB/Studium/` verschoben. Lose Dateien wie `Studienbescheinigung.pdf` bleiben außerhalb und werden nicht ingestiert.
- Optional: Tippfehler `Algorythm` → `Algorithms` korrigieren (kein Code-Impact, nur Convention).
- Nextcloud-App-Passwort für den Service-User wird angelegt.
- Webhook-Konfiguration in Nextcloud zeigt auf den Service-Endpoint mit korrektem `X-Webhook-Secret`.