wiki/c-work/4-done/2026-05-udb-datasource-settings.md
2026-05-23 09:28:29 +02:00

187 lines
14 KiB
Markdown
Raw 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.

<!-- status: build -->
<!-- started: 2026-05-17 -->
<!-- completed: 2026-05-17 -->
<!-- component: gateway | frontend-nyla -->
# UDB DataSource Settings (⚙️) + konfigurierbare RAG-Limits
> **Implementierungsstand 2026-05-17:** S0S11 abgeschlossen. Wichtige Abweichung
> vom ursprünglichen Plan: das "lazy-fill defaults beim ersten Bootstrap"
> wurde **nicht** im Walker implementiert (würde die Caller-Override-Semantik
> aushebeln und liess Tests rot werden). Stattdessen: `_ragLimits` exponiert
> zwei klare APIs — `getStoredOverrides()` (nur explizite Overrides, für Walker)
> und `getRagLimits()` (Overrides + Defaults gemerged, für API/Cost-Estimate).
> Das Modal zeigt fehlende Werte als Placeholder aus dem Cost-Estimate-Endpoint
> an, damit der User immer eine konkrete Zahl sieht — ohne die Datenbank-Zeile
> dafür anfassen zu müssen. Siehe CHANGELOG 2026-05-17 für die Detail-Liste.
## Beschreibung und Kontext
Heute sind die RAG-Walker-Limits (`maxBytes`, `maxItems`, `maxFileSize`, `maxDepth`, `maxWorkspaces`, `maxListsPerWorkspace`, `maxTasks`) als Modul-Konstanten in den `subConnectorSync*.py`-Dateien hartkodiert. Ein User mit z. B. einem 500 MB grossen SharePoint-Folder erhält nach 200 MB ein stilles Stoppen (`stoppedAtLimit=maxBytes`) und kann das ohne Code-Änderung nicht anpassen. Heute (2026-05-17) sichtbar geworden auf einem Basecamp-Folder mit ~731 Dateien, von denen nur 25 indexiert wurden, bevor der `maxBytes`-Default zugriff.
Gleichzeitig fehlt für Feature-Instanz-DataSources (`FeatureDataSource`) bisher überhaupt ein UI, an dem der Owner Settings anpassen könnte. UserConnections haben den Wizard und das Edit-Modal auf der Connections-Seite für Feature-Daten gibt es keinen analogen Ort.
Beide Lücken lassen sich mit derselben Architektur schliessen: **ein generisches Settings-Icon (⚙️) pro Node in der UDB**, das ein typ- und scope-abhängiges Modal öffnet. Initial wird darin RAG-Limit-Werte und Connection-Master-Switch gepflegt, später kommen weitere Einstellungen (z. B. Custom-Neutralization-Regeln, Re-Sync-Cadence, Polling-Strategie) ohne neue Icons dazu.
Wichtiger Architektur-Constraint:
- **Keine Icon-Inflation in der UDB.** Bereits vorhanden: Scope-Toggle, Neutralize-Toggle, RAG-Index-Toggle (🧠), Chat (💬). Wir fügen genau **ein** zusätzliches Icon (⚙️) hinzu nicht ein Icon pro Setting.
## Fokus und kritische Details
- UDB ist die **einzige** Stelle, wo Settings pro Datenobjekt gemanagt werden. Andere Pfade (Wizard, Connection-Edit-Modal) editieren nur die Connection-Ebene, nie einzelne DataSources.
- **Speicherort = Quelle der Wahrheit. Keine Override-Schichten, keine Resolver-Logik.** Was im UI angezeigt wird, ist exakt das, was im Walker greift. Die Werte werden bei der DataSource-Erstellung mit sinnvollen Defaults vorbefüllt (kopiert aus globalen Konstanten); danach kann der User sie einfach editieren. Vorteil: nachvollziehbar, debuggbar, keine versteckten Vererbungen.
- Walker müssen ihre Default-Konstanten zentralisieren und beim Lesen einer DataSource entweder den dort gespeicherten Wert nehmen oder, falls noch nicht gesetzt, den zentralen Default ausliefern und gleichzeitig auf der DataSource persistieren (lazy initialization → ab dann sind die Werte sichtbar im UI).
- Settings-Werte landen in einer **JSON-Spalte** auf der DataSource bzw. FeatureDataSource (`settings: Optional[Dict[str, Any]]`) damit ist die Erweiterbarkeit garantiert, ohne neue Spalten/Migrationen pro neuem Setting.
- DataSource vs. FeatureDataSource: gleiche Spalte (`settings`), gleiches UI, gleiches API-Pattern (`PATCH /api/datasources/{id}/settings` und `PATCH /api/feature-data-sources/{id}/settings`).
- Audit-Log-Pflicht: jede Änderung an `settings` schreibt einen `AuditCategory.PERMISSION`-Eintrag (auch wenn es nur ein numerischer Limit-Wert ist RAG-Ingest-Grenze ist eine Compliance-relevante Entscheidung).
- Defaults bleiben **konservativ** (200 MB / 10'000 Items / 25 MB / Tiefe 8). Anheben braucht User-Aktion → keine versehentlichen Kostenexplosionen.
- **Connection-Master-Switch im Modal:** Im Settings-Modal einer DataSource ist auch der Connection-weite Master-Switch `knowledgeIngestionEnabled` ("Knowledge database active") togglebar damit ist das Modal die zentrale Stelle für alle relevanten Settings, ohne zwischen Seiten wechseln zu müssen. Dieser Toggle wirkt auf alle DataSources derselben Connection (es ist explizit als "Connection-Setting" gekennzeichnet, nicht als DataSource-Setting).
- **Kosten-Indikator statt Hard-Cap:** Keine harten Obergrenzen. Stattdessen zeigt das Modal eine indikative (nicht-verbindliche) Realtime-Kostenschätzung: basierend auf erwarteter Item-Zahl, Token-Schätzung pro Item und aktuellen Embedding-Preisen. Klar als "indikativ" gekennzeichnet, der User trägt die Verantwortung.
## Ziel und Nicht-Ziele
- **Ziel:**
- Ein Settings-Icon ⚙️ pro Tree-Node in der UDB-Sidebar (`SourcesTab.tsx`), das ein Modal öffnet.
- Im Modal: drei klar abgegrenzte Sektionen:
1. **Connection** (Master-Switch `knowledgeIngestionEnabled`, gemeinsame Mail-/ClickUp-Preferences aus `knowledgePreferences`).
2. **DataSource RAG-Limits** (`maxBytes`, `maxFileSize`, `maxItems`, `maxDepth`).
3. **Kostenschätzung** (indikativer Wert mit Hinweis).
- Backend: `DataSource.settings` und `FeatureDataSource.settings` als JSON-Spalte. Endpunkte zum Lesen/Patchen.
- Walker (`subConnectorSyncSharepoint.py`, `subConnectorSyncKdrive.py`, `subConnectorSyncGdrive.py`, `subConnectorSyncClickup.py`) lesen Limits direkt aus `DataSource.settings.ragLimits`, fallen auf zentrale Defaults zurück, wenn (noch) leer.
- Partial-Banner auf `RagInventoryPage` zeigt zusätzlich Hint: "Limit kann pro DataSource in der UDB unter ⚙️ angehoben werden."
- Owner-Kontrolle: für UserConnection-DataSources nur Owner; für FeatureDataSource Owner oder `workspace-admin`.
- **Nicht-Ziel:**
- Mandate-weite Defaults / Override-Schichten / Resolver-Layer.
- Hard-Caps (User/Admin trägt Verantwortung).
- Settings-Vererbung im Tree (Parent-Folder → Children) aktuell wirkt eine Änderung nur auf die konkrete DataSource.
- Eigene Settings für Mail-Connectors (`Outlook`, `Gmail`) auf DataSource-Ebene die haben keine Folder-Hierarchie. Die Mail-Preferences bleiben Connection-weit und werden im Modal in der "Connection"-Sektion editiert.
## Architektur-Skizze
### Daten-Schicht
`DataSource` und `FeatureDataSource` bekommen je eine neue Spalte:
```python
settings: Optional[Dict[str, Any]] = Field(
default=None,
description="DataSource-scoped settings (JSON). Currently used keys: ragLimits.",
json_schema_extra={"frontend_type": "json", "frontend_readonly": True, "frontend_required": False},
)
```
JSON-Schema-Konvention:
```json
{
"ragLimits": {
"maxBytes": 524288000,
"maxFileSize": 52428800,
"maxItems": 20000,
"maxDepth": 12
}
}
```
**Keine Resolver-Schichten**: was in `settings.ragLimits` steht, gilt. Wenn der Key fehlt, nimmt der Walker den zentralen Default aus `_ragLimits.RAG_LIMITS_DEFAULT` und schreibt ihn beim nächsten Bootstrap **einmalig** in die DataSource zurück (lazy fill), damit der User die Werte auch ohne vorherigen Sync schon im UI sieht und editieren kann.
### Backend-API
```
PATCH /api/datasources/{id}/settings { settings: { ragLimits: {...} } }
PATCH /api/feature-data-sources/{id}/settings { settings: { ragLimits: {...} } }
GET /api/datasources/{id}/cost-estimate → { estimatedTokens, estimatedChf, basis: {...} }
```
`/cost-estimate`: schätzt anhand Item-Count (sofern bekannt aus letztem Sync) × tokens-per-item-heuristik × Embedding-Preis. Liefert auch die Annahmen (`basis`), damit der User die Plausibilität prüfen kann.
### Walker-Refactor
Heute hartkodiert in jedem Walker:
```python
MAX_BYTES_DEFAULT = 200 * 1024 * 1024
MAX_ITEMS_DEFAULT = 10_000
```
Wird zu:
```python
# gateway/modules/serviceCenter/services/serviceKnowledge/_ragLimits.py
RAG_LIMITS_DEFAULT = {
"maxBytes": 200 * 1024 * 1024,
"maxFileSize": 25 * 1024 * 1024,
"maxItems": 10_000,
"maxDepth": 8,
}
def getRagLimits(dataSource: Dict[str, Any]) -> Dict[str, int]:
"""Read limits from DataSource.settings.ragLimits, fall back to defaults
for missing keys. Pure read; lazy persist is the caller's responsibility."""
stored = (dataSource.get("settings") or {}).get("ragLimits") or {}
return {**RAG_LIMITS_DEFAULT, **stored}
def lazyFillRagLimits(rootIf, dataSourceId: str, dataSource: Dict[str, Any]) -> None:
"""Persist defaults to settings if not yet present, so the UI shows them."""
```
Jeder Walker holt seine Limits beim Eintritt in `_bootstrap*` einmal und gibt sie an `_walkFolder()` / `_finalizeResult()` weiter die Konstante existiert nur noch im neuen Modul als Default-Fallback.
### Frontend-UI
In `SourcesTab.tsx` (Tree-Node-Render), zwischen Brain-Icon (🧠) und Chat-Icon (💬):
```typescript
<button
onClick={async (e) => {
e.stopPropagation();
const dsId = ds?.id ?? await onEnsureDs(node);
if (dsId) openSettingsModal(dsId, scope);
}}
title={t('Einstellungen')}
style={{ ... }}
>
⚙️
</button>
```
Settings-Modal (`DataSourceSettingsModal.tsx`):
- **Sektion "Connection"** (oben, mit Connection-Label und Authority-Icon):
- Toggle `knowledgeIngestionEnabled` (Master-Switch). Hinweis: wirkt auf alle DataSources dieser Connection.
- Optional, wenn relevant: `mailContentDepth`, `mailIndexAttachments`, `filesIndexBinaries`, `clickupScope`, `clickupIndexAttachments`, `maxAgeDays` (aus `knowledgePreferences`).
- PATCH → `/api/connections/{id}/knowledge-consent` bzw. `/api/connections/{id}/knowledge-preferences`.
- **Sektion "RAG-Limits"** (nur wenn DataSource den Walker-Typ unterstützt):
- Felder `maxBytes`, `maxFileSize`, `maxItems`, `maxDepth` mit lesbaren Units (MB, Anzahl).
- PATCH → `/api/datasources/{id}/settings`.
- **Sektion "Kostenschätzung"** (read-only):
- "Indikative Kosten: ~X CHF pro Voll-Sync" (mit Basis-Tooltip).
- GET → `/api/datasources/{id}/cost-estimate` beim Modal-Öffnen.
Auf der `RagInventoryPage`-Partial-Banner-Komponente:
```typescript
<span>... Limit {l} erreicht. Weitere Dateien wurden NICHT indexiert. {' '}
<a onClick={() => openSettingsModalForConn(conn)}>Limit anpassen</a> oder
DataSource enger eingrenzen, dann erneut starten.</span>
```
## Erfolgskriterien
- Auf einer SharePoint-DataSource mit > 200 MB kann der Owner über das ⚙️-Icon `maxBytes` auf z. B. 1 GB anheben, einen Re-Index starten und sieht im Inventory > 200 MB indexierte Bytes ohne `stoppedAtLimit`-Banner.
- Auf einer FeatureDataSource ist das gleiche Settings-Modal verfügbar und speichert sauber in `FeatureDataSource.settings`.
- Wenn `settings.ragLimits` leer ist, ist das Verhalten der Walker **bitidentisch** zur Vor-Plan-Version (keine Regression).
- Der Connection-Master-Switch im Modal und der Toggle auf der Connections-Page und auf der RagInventoryPage zeigen immer denselben Wert (alle drei rufen `/knowledge-consent`).
- Audit-Log enthält pro Settings-Change einen `PERMISSION`-Eintrag mit `dataSourceId`, `userId`, `mandateId`, `oldSettings`, `newSettings`.
- Keine zusätzlichen Icons in der UDB ausser dem einen ⚙️.
- Kostenschätzung wird als "indikativ, nicht verbindlich" gekennzeichnet und zeigt die zugrunde liegenden Annahmen.
## Schritte
- [ ] S1 — Datenmodell: `settings: JSON` an `DataSource` und `FeatureDataSource` ergänzen (`gateway/modules/datamodels/datamodelDataSource.py`, `datamodelFeatureDataSource.py`); SQL-Migration in `gateway/scripts/script_db_migrate_datasource_settings.py`.
- [ ] S2 — Zentralisierte Defaults: `gateway/modules/serviceCenter/services/serviceKnowledge/_ragLimits.py` neu mit `RAG_LIMITS_DEFAULT`, `getRagLimits()`, `lazyFillRagLimits()`.
- [ ] S3 — Walker-Refactor: `subConnectorSyncSharepoint.py`, `subConnectorSyncKdrive.py`, `subConnectorSyncGdrive.py`, `subConnectorSyncClickup.py` lesen Limits via `getRagLimits()` und schreiben Defaults via `lazyFillRagLimits()` zurück.
- [ ] S4 — Backend-Endpunkte: `PATCH /api/datasources/{id}/settings`, `PATCH /api/feature-data-sources/{id}/settings`, `GET /api/datasources/{id}/cost-estimate` in `routeDataSources.py` (und `routeFeatureDataSources.py`, falls noch nicht da). RBAC: Owner für DataSource, Owner + `workspace-admin` für FeatureDataSource.
- [ ] S5 — Frontend ⚙️-Button in `SourcesTab.tsx` (Tree-Row, zwischen 🧠 und 💬), Opacity-Logik: voll sichtbar, wenn `settings` befüllt sind, sonst gedimmt.
- [ ] S6 — Frontend `DataSourceSettingsModal.tsx`: drei Sektionen (Connection / RAG-Limits / Kostenschätzung), per-Feld-Validation, Speichern via API.
- [ ] S7 — `RagInventoryPage.tsx` Partial-Banner: Link "Limit anpassen" öffnet Modal für die betroffene DataSource (Heuristik: DS, die `stoppedAtLimit` ausgelöst hat bei mehreren: die mit den meisten verarbeiteten Bytes).
- [ ] S8 — Audit-Logging in den neuen Settings-Endpunkten.
- [ ] S9 — Cost-Estimate-Engine: `gateway/modules/serviceCenter/services/serviceKnowledge/_costEstimate.py` mit Heuristik (Items × Tokens × Embedding-Preis) und Basis-Annahmen-Output.
- [ ] S10 — Tests: Unit-Tests für `getRagLimits()` / `lazyFillRagLimits()`, API-Tests für PATCH-Endpunkte (RBAC), Cost-Estimate-Unit-Test, Frontend-Smoke-Test (Modal öffnet, speichert, refetch, Cost-Anzeige).
- [ ] S11 — Doku: `wiki/b-reference/platform/rag-pipeline.md` Abschnitt "Limits & Settings" ergänzen, plus Eintrag in `wiki/TOPICS.md`. CHANGELOG-Zeile.
## Offene Fragen / Decisions
- **F1 Override-Logik?** → **Entschieden: KEINE Override-Schichten.** Die DataSource speichert ihre eigenen Werte direkt; Walker liest sie 1:1. Lazy-Fill mit zentralen Defaults beim ersten Sync, damit das UI immer einen sinnvollen Startwert zeigt. Nachvollziehbar, debuggbar, keine versteckten Vererbungen.
- **F2 RBAC?** → **Entschieden:** Owner für UserConnection-DataSources, Owner oder `workspace-admin` für FeatureDataSource (analog Scope-/Neutralize-Toggle).
- **F3 Hard-Cap pro Mandat?** → **Entschieden: NEIN.** Stattdessen indikative Realtime-Kostenschätzung im Modal. User/Admin trägt Verantwortung.
- **F4 Mail/ClickUp-Preferences im Modal?** → **Entschieden: JA.** Connection-weite Preferences werden in der Sektion "Connection" des Modals editiert das macht das Modal zur einzigen Stelle für alle relevanten Settings, ohne Seitenwechsel.